added user-specific rates (#1455)
This commit is contained in:
@@ -11,17 +11,19 @@ namespace App\Controller;
|
||||
|
||||
use App\Configuration\FormConfiguration;
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\ActivityRate;
|
||||
use App\Entity\MetaTableTypeInterface;
|
||||
use App\Entity\Project;
|
||||
use App\Event\ActivityMetaDefinitionEvent;
|
||||
use App\Event\ActivityMetaDisplayEvent;
|
||||
use App\Form\ActivityEditForm;
|
||||
use App\Form\ActivityRateForm;
|
||||
use App\Form\Toolbar\ActivityToolbarForm;
|
||||
use App\Form\Type\ActivityType;
|
||||
use App\Repository\ActivityRateRepository;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\Query\ActivityFormTypeQuery;
|
||||
use App\Repository\Query\ActivityQuery;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
@@ -37,7 +39,7 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
* @Route(path="/admin/activity")
|
||||
* @Security("is_granted('view_activity')")
|
||||
*/
|
||||
class ActivityController extends AbstractController
|
||||
final class ActivityController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @var ActivityRepository
|
||||
@@ -50,7 +52,7 @@ class ActivityController extends AbstractController
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
protected $dispatcher;
|
||||
private $dispatcher;
|
||||
|
||||
public function __construct(ActivityRepository $repository, FormConfiguration $configuration, EventDispatcherInterface $dispatcher)
|
||||
{
|
||||
@@ -59,11 +61,6 @@ class ActivityController extends AbstractController
|
||||
$this->dispatcher = $dispatcher;
|
||||
}
|
||||
|
||||
protected function getRepository(): ActivityRepository
|
||||
{
|
||||
return $this->repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/", defaults={"page": 1}, name="admin_activity", methods={"GET"})
|
||||
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_activity_paginated", methods={"GET"})
|
||||
@@ -88,7 +85,7 @@ class ActivityController extends AbstractController
|
||||
}
|
||||
|
||||
/* @var $entries Pagerfanta */
|
||||
$entries = $this->getRepository()->getPagerfantaForQuery($query);
|
||||
$entries = $this->repository->getPagerfantaForQuery($query);
|
||||
|
||||
return $this->render('activity/index.html.twig', [
|
||||
'entries' => $entries,
|
||||
@@ -110,6 +107,85 @@ class ActivityController extends AbstractController
|
||||
return $event->getFields();
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/details", name="activity_details", methods={"GET", "POST"})
|
||||
* @Security("is_granted('view', activity)")
|
||||
*/
|
||||
public function detailsAction(Activity $activity, ActivityRateRepository $rateRepository)
|
||||
{
|
||||
$event = new ActivityMetaDefinitionEvent($activity);
|
||||
$this->dispatcher->dispatch($event);
|
||||
|
||||
$stats = null;
|
||||
$rates = [];
|
||||
|
||||
if ($this->isGranted('edit', $activity)) {
|
||||
$rates = $rateRepository->getRatesForActivity($activity);
|
||||
}
|
||||
|
||||
if ($this->isGranted('budget', $activity)) {
|
||||
$stats = $this->repository->getActivityStatistics($activity);
|
||||
}
|
||||
|
||||
return $this->render('activity/details.html.twig', [
|
||||
'activity' => $activity,
|
||||
'stats' => $stats,
|
||||
'rates' => $rates
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/rate_delete/{rate}", name="admin_activity_rate_delete", methods={"GET"})
|
||||
* @Security("is_granted('edit', activity)")
|
||||
*/
|
||||
public function deleteRateAction(Activity $activity, ActivityRate $rate, ActivityRateRepository $repository)
|
||||
{
|
||||
if ($rate->getActivity() !== $activity) {
|
||||
$this->flashError('action.delete.error', ['%reason%' => 'Invalid activity']);
|
||||
} else {
|
||||
try {
|
||||
$repository->deleteRate($rate);
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.delete.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('activity_details', ['id' => $activity->getId()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/rate", name="admin_activity_rate_add", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', activity)")
|
||||
*/
|
||||
public function addRateAction(Activity $activity, Request $request, ActivityRateRepository $repository)
|
||||
{
|
||||
$rate = new ActivityRate();
|
||||
$rate->setActivity($activity);
|
||||
|
||||
$form = $this->createForm(ActivityRateForm::class, $rate, [
|
||||
'action' => $this->generateUrl('admin_activity_rate_add', ['id' => $activity->getId()]),
|
||||
'method' => 'POST',
|
||||
]);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
try {
|
||||
$repository->saveRate($rate);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('activity_details', ['id' => $activity->getId()]);
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('activity/rates.html.twig', [
|
||||
'activity' => $activity,
|
||||
'form' => $form->createView()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/create", name="admin_activity_create", methods={"GET", "POST"})
|
||||
* @Route(path="/create/{project}", name="admin_activity_create_with_project", methods={"GET", "POST"})
|
||||
@@ -129,25 +205,6 @@ class ActivityController extends AbstractController
|
||||
return $this->renderActivityForm($activity, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/budget", name="admin_activity_budget", methods={"GET"})
|
||||
* @Security("is_granted('budget', activity)")
|
||||
*
|
||||
* @param Activity $activity
|
||||
* @return Response
|
||||
*/
|
||||
public function budgetAction(Activity $activity)
|
||||
{
|
||||
$stats = $this->getRepository()->getActivityStatistics($activity);
|
||||
|
||||
// TODO sent event with stats
|
||||
|
||||
return $this->render('activity/budget.html.twig', [
|
||||
'activity' => $activity,
|
||||
'stats' => $stats,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/edit", name="admin_activity_edit", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', activity)")
|
||||
@@ -171,7 +228,7 @@ class ActivityController extends AbstractController
|
||||
*/
|
||||
public function deleteAction(Activity $activity, Request $request)
|
||||
{
|
||||
$stats = $this->getRepository()->getActivityStatistics($activity);
|
||||
$stats = $this->repository->getActivityStatistics($activity);
|
||||
|
||||
$deleteForm = $this->createFormBuilder(null, [
|
||||
'attr' => [
|
||||
@@ -199,9 +256,9 @@ class ActivityController extends AbstractController
|
||||
|
||||
if ($deleteForm->isSubmitted() && $deleteForm->isValid()) {
|
||||
try {
|
||||
$this->getRepository()->deleteActivity($activity, $deleteForm->get('activity')->getData());
|
||||
$this->repository->deleteActivity($activity, $deleteForm->get('activity')->getData());
|
||||
$this->flashSuccess('action.delete.success');
|
||||
} catch (ORMException $ex) {
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.delete.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
|
||||
@@ -233,7 +290,7 @@ class ActivityController extends AbstractController
|
||||
|
||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||
try {
|
||||
$this->getRepository()->saveActivity($activity);
|
||||
$this->repository->saveActivity($activity);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
if ($editForm->has('create_more') && $editForm->get('create_more')->getData() === true) {
|
||||
@@ -245,7 +302,7 @@ class ActivityController extends AbstractController
|
||||
} else {
|
||||
return $this->redirectToRoute('admin_activity');
|
||||
}
|
||||
} catch (ORMException $ex) {
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,22 +12,25 @@ namespace App\Controller;
|
||||
use App\Configuration\FormConfiguration;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\CustomerComment;
|
||||
use App\Entity\CustomerRate;
|
||||
use App\Entity\MetaTableTypeInterface;
|
||||
use App\Entity\Rate;
|
||||
use App\Entity\Team;
|
||||
use App\Event\CustomerMetaDefinitionEvent;
|
||||
use App\Event\CustomerMetaDisplayEvent;
|
||||
use App\Form\CustomerCommentForm;
|
||||
use App\Form\CustomerEditForm;
|
||||
use App\Form\CustomerRateForm;
|
||||
use App\Form\CustomerTeamPermissionForm;
|
||||
use App\Form\Toolbar\CustomerToolbarForm;
|
||||
use App\Form\Type\CustomerType;
|
||||
use App\Repository\CustomerRateRepository;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\CustomerFormTypeQuery;
|
||||
use App\Repository\Query\CustomerQuery;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
use App\Repository\TeamRepository;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
@@ -257,7 +260,7 @@ final class CustomerController extends AbstractController
|
||||
* @Route(path="/{id}/details", name="customer_details", methods={"GET", "POST"})
|
||||
* @Security("is_granted('view', customer)")
|
||||
*/
|
||||
public function detailsAction(Customer $customer, TeamRepository $teamRepository)
|
||||
public function detailsAction(Customer $customer, TeamRepository $teamRepository, CustomerRateRepository $rateRepository)
|
||||
{
|
||||
$event = new CustomerMetaDefinitionEvent($customer);
|
||||
$this->dispatcher->dispatch($event);
|
||||
@@ -270,9 +273,13 @@ final class CustomerController extends AbstractController
|
||||
$comments = null;
|
||||
$teams = null;
|
||||
$projects = null;
|
||||
$rates = [];
|
||||
|
||||
if ($this->isGranted('edit', $customer) && $this->isGranted('create_team')) {
|
||||
$defaultTeam = $teamRepository->findOneBy(['name' => $customer->getName()]);
|
||||
if ($this->isGranted('edit', $customer)) {
|
||||
if ($this->isGranted('create_team')) {
|
||||
$defaultTeam = $teamRepository->findOneBy(['name' => $customer->getName()]);
|
||||
}
|
||||
$rates = $rateRepository->getRatesForCustomer($customer);
|
||||
}
|
||||
|
||||
if (null !== $customer->getTimezone()) {
|
||||
@@ -304,6 +311,59 @@ final class CustomerController extends AbstractController
|
||||
'team' => $defaultTeam,
|
||||
'teams' => $teams,
|
||||
'now' => new \DateTime('now', $timezone),
|
||||
'rates' => $rates
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/rate_delete/{rate}", name="admin_customer_rate_delete", methods={"GET"})
|
||||
* @Security("is_granted('edit', customer)")
|
||||
*/
|
||||
public function deleteRateAction(Customer $customer, CustomerRate $rate, CustomerRateRepository $repository)
|
||||
{
|
||||
if ($rate->getCustomer() !== $customer) {
|
||||
$this->flashError('action.delete.error', ['%reason%' => 'Invalid customer']);
|
||||
} else {
|
||||
try {
|
||||
$repository->deleteRate($rate);
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.delete.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('customer_details', ['id' => $customer->getId()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/rate", name="admin_customer_rate_add", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', customer)")
|
||||
*/
|
||||
public function addRateAction(Customer $customer, Request $request, CustomerRateRepository $repository)
|
||||
{
|
||||
$rate = new CustomerRate();
|
||||
$rate->setCustomer($customer);
|
||||
|
||||
$form = $this->createForm(CustomerRateForm::class, $rate, [
|
||||
'action' => $this->generateUrl('admin_customer_rate_add', ['id' => $customer->getId()]),
|
||||
'method' => 'POST',
|
||||
]);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
try {
|
||||
$repository->saveRate($rate);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('customer_details', ['id' => $customer->getId()]);
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('customer/rates.html.twig', [
|
||||
'customer' => $customer,
|
||||
'form' => $form->createView()
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -352,7 +412,7 @@ final class CustomerController extends AbstractController
|
||||
try {
|
||||
$this->repository->deleteCustomer($customer, $deleteForm->get('customer')->getData());
|
||||
$this->flashSuccess('action.delete.success');
|
||||
} catch (ORMException $ex) {
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.delete.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
|
||||
@@ -383,7 +443,7 @@ final class CustomerController extends AbstractController
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('customer_details', ['id' => $customer->getId()]);
|
||||
} catch (ORMException $ex) {
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,15 +14,19 @@ use App\Entity\Customer;
|
||||
use App\Entity\MetaTableTypeInterface;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\ProjectComment;
|
||||
use App\Entity\ProjectRate;
|
||||
use App\Entity\Rate;
|
||||
use App\Entity\Team;
|
||||
use App\Event\ProjectMetaDefinitionEvent;
|
||||
use App\Event\ProjectMetaDisplayEvent;
|
||||
use App\Form\ProjectCommentForm;
|
||||
use App\Form\ProjectEditForm;
|
||||
use App\Form\ProjectRateForm;
|
||||
use App\Form\ProjectTeamPermissionForm;
|
||||
use App\Form\Toolbar\ProjectToolbarForm;
|
||||
use App\Form\Type\ProjectType;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\ProjectRateRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\ActivityQuery;
|
||||
use App\Repository\Query\ProjectFormTypeQuery;
|
||||
@@ -261,7 +265,7 @@ final class ProjectController extends AbstractController
|
||||
* @Route(path="/{id}/details", name="project_details", methods={"GET", "POST"})
|
||||
* @Security("is_granted('view', project)")
|
||||
*/
|
||||
public function detailsAction(Project $project, TeamRepository $teamRepository)
|
||||
public function detailsAction(Project $project, TeamRepository $teamRepository, ProjectRateRepository $rateRepository)
|
||||
{
|
||||
$event = new ProjectMetaDefinitionEvent($project);
|
||||
$this->dispatcher->dispatch($event);
|
||||
@@ -272,9 +276,13 @@ final class ProjectController extends AbstractController
|
||||
$attachments = [];
|
||||
$comments = null;
|
||||
$teams = null;
|
||||
$rates = [];
|
||||
|
||||
if ($this->isGranted('edit', $project) && $this->isGranted('create_team')) {
|
||||
$defaultTeam = $teamRepository->findOneBy(['name' => $project->getName()]);
|
||||
if ($this->isGranted('edit', $project)) {
|
||||
if ($this->isGranted('create_team')) {
|
||||
$defaultTeam = $teamRepository->findOneBy(['name' => $project->getName()]);
|
||||
}
|
||||
$rates = $rateRepository->getRatesForProject($project);
|
||||
}
|
||||
|
||||
if ($this->isGranted('budget', $project)) {
|
||||
@@ -301,6 +309,59 @@ final class ProjectController extends AbstractController
|
||||
'stats' => $stats,
|
||||
'team' => $defaultTeam,
|
||||
'teams' => $teams,
|
||||
'rates' => $rates
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/rate_delete/{rate}", name="admin_project_rate_delete", methods={"GET"})
|
||||
* @Security("is_granted('edit', project)")
|
||||
*/
|
||||
public function deleteRateAction(Project $project, ProjectRate $rate, ProjectRateRepository $repository)
|
||||
{
|
||||
if ($rate->getProject() !== $project) {
|
||||
$this->flashError('action.delete.error', ['%reason%' => 'Invalid project']);
|
||||
} else {
|
||||
try {
|
||||
$repository->deleteRate($rate);
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.delete.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('project_details', ['id' => $project->getId()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/rate", name="admin_project_rate_add", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', project)")
|
||||
*/
|
||||
public function addRateAction(Project $project, Request $request, ProjectRateRepository $repository)
|
||||
{
|
||||
$rate = new ProjectRate();
|
||||
$rate->setProject($project);
|
||||
|
||||
$form = $this->createForm(ProjectRateForm::class, $rate, [
|
||||
'action' => $this->generateUrl('admin_project_rate_add', ['id' => $project->getId()]),
|
||||
'method' => 'POST',
|
||||
]);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
try {
|
||||
$repository->saveRate($rate);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('project_details', ['id' => $project->getId()]);
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('project/rates.html.twig', [
|
||||
'project' => $project,
|
||||
'form' => $form->createView()
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +74,6 @@ class Activity implements EntityWithMetaFields
|
||||
private $visible = true;
|
||||
|
||||
// keep the trait include exactly here, for placing the column at the correct position
|
||||
use RatesTrait;
|
||||
use ColorTrait;
|
||||
use BudgetTrait;
|
||||
|
||||
@@ -138,6 +137,11 @@ class Activity implements EntityWithMetaFields
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isGlobal(): bool
|
||||
{
|
||||
return $this->project === null;
|
||||
}
|
||||
|
||||
public function isVisible(): bool
|
||||
{
|
||||
return $this->visible;
|
||||
|
||||
54
src/Entity/ActivityRate.php
Normal file
54
src/Entity/ActivityRate.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="kimai2_activities_rates",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(columns={"user_id", "activity_id"}),
|
||||
* }
|
||||
* )
|
||||
* @ORM\Entity(repositoryClass="App\Repository\ActivityRateRepository")
|
||||
* @UniqueEntity({"user", "activity"}, ignoreNull=false)
|
||||
*/
|
||||
class ActivityRate implements RateInterface
|
||||
{
|
||||
use Rate;
|
||||
|
||||
/**
|
||||
* @var Activity
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Activity")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $activity;
|
||||
|
||||
public function setActivity(?Activity $activity): ActivityRate
|
||||
{
|
||||
$this->activity = $activity;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getActivity(): ?Activity
|
||||
{
|
||||
return $this->activity;
|
||||
}
|
||||
|
||||
public function getScore(): int
|
||||
{
|
||||
return 5;
|
||||
}
|
||||
}
|
||||
@@ -174,7 +174,6 @@ class Customer implements EntityWithMetaFields
|
||||
private $timezone;
|
||||
|
||||
// keep the trait include exactly here, for placing the column at the correct position
|
||||
use RatesTrait;
|
||||
use ColorTrait;
|
||||
use BudgetTrait;
|
||||
|
||||
|
||||
54
src/Entity/CustomerRate.php
Normal file
54
src/Entity/CustomerRate.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="kimai2_customers_rates",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(columns={"user_id", "customer_id"}),
|
||||
* }
|
||||
* )
|
||||
* @ORM\Entity(repositoryClass="App\Repository\CustomerRateRepository")
|
||||
* @UniqueEntity({"user", "customer"}, ignoreNull=false)
|
||||
*/
|
||||
class CustomerRate implements RateInterface
|
||||
{
|
||||
use Rate;
|
||||
|
||||
/**
|
||||
* @var Customer
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Customer")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $customer;
|
||||
|
||||
public function setCustomer(?Customer $customer): CustomerRate
|
||||
{
|
||||
$this->customer = $customer;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCustomer(): ?Customer
|
||||
{
|
||||
return $this->customer;
|
||||
}
|
||||
|
||||
public function getScore(): int
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -105,7 +105,6 @@ class Project implements EntityWithMetaFields
|
||||
private $visible = true;
|
||||
|
||||
// keep the trait include exactly here, for placing the column at the correct position
|
||||
use RatesTrait;
|
||||
use ColorTrait;
|
||||
use BudgetTrait;
|
||||
|
||||
|
||||
54
src/Entity/ProjectRate.php
Normal file
54
src/Entity/ProjectRate.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="kimai2_projects_rates",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(columns={"user_id", "project_id"}),
|
||||
* }
|
||||
* )
|
||||
* @ORM\Entity(repositoryClass="App\Repository\ProjectRateRepository")
|
||||
* @UniqueEntity({"user", "project"}, ignoreNull=false)
|
||||
*/
|
||||
class ProjectRate implements RateInterface
|
||||
{
|
||||
use Rate;
|
||||
|
||||
/**
|
||||
* @var Project
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Project")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull
|
||||
*/
|
||||
private $project;
|
||||
|
||||
public function setProject(?Project $project): ProjectRate
|
||||
{
|
||||
$this->project = $project;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getProject(): ?Project
|
||||
{
|
||||
return $this->project;
|
||||
}
|
||||
|
||||
public function getScore(): int
|
||||
{
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
92
src/Entity/Rate.php
Normal file
92
src/Entity/Rate.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
trait Rate
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue(strategy="IDENTITY")
|
||||
*/
|
||||
private $id;
|
||||
/**
|
||||
* @var User
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\User")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=true)
|
||||
*/
|
||||
private $user;
|
||||
/**
|
||||
* @var float
|
||||
*
|
||||
* @ORM\Column(name="rate", type="float", nullable=false)
|
||||
* @Assert\GreaterThanOrEqual(0)
|
||||
*/
|
||||
private $rate = 0.00;
|
||||
/**
|
||||
* @var bool
|
||||
*
|
||||
* @ORM\Column(name="fixed", type="boolean", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $isFixed = false;
|
||||
|
||||
/**
|
||||
* Get entry id, returns null for new entities which were not persisted.
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setUser(?User $user): self
|
||||
{
|
||||
$this->user = $user;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function setRate(float $rate): self
|
||||
{
|
||||
$this->rate = $rate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRate(): float
|
||||
{
|
||||
return $this->rate;
|
||||
}
|
||||
|
||||
public function isFixed(): bool
|
||||
{
|
||||
return $this->isFixed;
|
||||
}
|
||||
|
||||
public function setIsFixed(bool $isFixed): self
|
||||
{
|
||||
$this->isFixed = $isFixed;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
21
src/Entity/RateInterface.php
Normal file
21
src/Entity/RateInterface.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
interface RateInterface
|
||||
{
|
||||
public function getUser(): ?User;
|
||||
|
||||
public function getRate(): float;
|
||||
|
||||
public function isFixed(): bool;
|
||||
|
||||
public function getScore(): int;
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
trait RatesTrait
|
||||
{
|
||||
/**
|
||||
* @var float
|
||||
*
|
||||
* @ORM\Column(name="fixed_rate", type="float", nullable=true)
|
||||
* @Assert\GreaterThanOrEqual(0)
|
||||
*/
|
||||
private $fixedRate = null;
|
||||
|
||||
/**
|
||||
* @var float
|
||||
*
|
||||
* @ORM\Column(name="hourly_rate", type="float", nullable=true)
|
||||
* @Assert\GreaterThanOrEqual(0)
|
||||
*/
|
||||
private $hourlyRate = null;
|
||||
|
||||
/**
|
||||
* @return float
|
||||
*/
|
||||
public function getFixedRate(): ?float
|
||||
{
|
||||
return $this->fixedRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param float $fixedRate
|
||||
* @return self
|
||||
*/
|
||||
public function setFixedRate(?float $fixedRate)
|
||||
{
|
||||
$this->fixedRate = $fixedRate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return float
|
||||
*/
|
||||
public function getHourlyRate(): ?float
|
||||
{
|
||||
return $this->hourlyRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param float $hourlyRate
|
||||
* @return self
|
||||
*/
|
||||
public function setHourlyRate(?float $hourlyRate)
|
||||
{
|
||||
$this->hourlyRate = $hourlyRate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -130,8 +130,21 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface
|
||||
*/
|
||||
private $rate = 0.00;
|
||||
|
||||
// keep the trait include exactly here, for placing the column at the correct position
|
||||
use RatesTrait;
|
||||
/**
|
||||
* @var float
|
||||
*
|
||||
* @ORM\Column(name="fixed_rate", type="float", nullable=true)
|
||||
* @Assert\GreaterThanOrEqual(0)
|
||||
*/
|
||||
private $fixedRate = null;
|
||||
|
||||
/**
|
||||
* @var float
|
||||
*
|
||||
* @ORM\Column(name="hourly_rate", type="float", nullable=true)
|
||||
* @Assert\GreaterThanOrEqual(0)
|
||||
*/
|
||||
private $hourlyRate = null;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
@@ -452,6 +465,30 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface
|
||||
return self::CATEGORY_WORK;
|
||||
}
|
||||
|
||||
public function getFixedRate(): ?float
|
||||
{
|
||||
return $this->fixedRate;
|
||||
}
|
||||
|
||||
public function setFixedRate(?float $fixedRate): Timesheet
|
||||
{
|
||||
$this->fixedRate = $fixedRate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getHourlyRate(): ?float
|
||||
{
|
||||
return $this->hourlyRate;
|
||||
}
|
||||
|
||||
public function setHourlyRate(?float $hourlyRate): Timesheet
|
||||
{
|
||||
$this->hourlyRate = $hourlyRate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal only here for symfony forms
|
||||
* @return Collection|MetaTableTypeInterface[]
|
||||
|
||||
@@ -95,19 +95,19 @@ final class MenuSubscriber implements EventSubscriberInterface
|
||||
|
||||
if ($auth->isGranted('view_customer') || $auth->isGranted('view_teamlead_customer') || $auth->isGranted('view_team_customer')) {
|
||||
$customers = new MenuItemModel('customer_admin', 'menu.admin_customer', 'admin_customer', [], $this->getIcon('customer'));
|
||||
$customers->setChildRoutes(['admin_customer_create', 'admin_customer_permissions', 'admin_customer_budget', 'admin_customer_edit', 'admin_customer_delete']);
|
||||
$customers->setChildRoutes(['admin_customer_create', 'admin_customer_permissions', 'customer_details', 'admin_customer_edit', 'admin_customer_delete']);
|
||||
$menu->addChild($customers);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('view_project') || $auth->isGranted('view_teamlead_project') || $auth->isGranted('view_team_project')) {
|
||||
$projects = new MenuItemModel('project_admin', 'menu.admin_project', 'admin_project', [], $this->getIcon('project'));
|
||||
$projects->setChildRoutes(['admin_project_permissions', 'admin_project_create', 'admin_project_budget', 'admin_project_edit', 'admin_project_delete']);
|
||||
$projects->setChildRoutes(['admin_project_permissions', 'admin_project_create', 'project_details', 'admin_project_edit', 'admin_project_delete']);
|
||||
$menu->addChild($projects);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('view_activity')) {
|
||||
$activities = new MenuItemModel('activity_admin', 'menu.admin_activity', 'admin_activity', [], $this->getIcon('activity'));
|
||||
$activities->setChildRoutes(['admin_activity_create', 'admin_activity_budget', 'admin_activity_edit', 'admin_activity_delete']);
|
||||
$activities->setChildRoutes(['admin_activity_create', 'activity_details', 'admin_activity_edit', 'admin_activity_delete']);
|
||||
$menu->addChild($activities);
|
||||
}
|
||||
|
||||
|
||||
72
src/Form/ActivityRateForm.php
Normal file
72
src/Form/ActivityRateForm.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\ActivityRate;
|
||||
use App\Entity\Rate;
|
||||
use App\Form\Type\UserType;
|
||||
use App\Form\Type\YesNoType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class ActivityRateForm extends AbstractType
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$currency = null;
|
||||
|
||||
if ($options['data']) {
|
||||
/** @var ActivityRate $rate */
|
||||
$rate = $options['data'];
|
||||
|
||||
if (null !== $rate->getActivity() && !$rate->getActivity()->isGlobal()) {
|
||||
$currency = $rate->getActivity()->getProject()->getCustomer()->getCurrency();
|
||||
}
|
||||
}
|
||||
|
||||
$builder
|
||||
->add('user', UserType::class, [
|
||||
'required' => false,
|
||||
])
|
||||
->add('rate', MoneyType::class, [
|
||||
'label' => 'label.rate',
|
||||
'attr' => [
|
||||
'autofocus' => 'autofocus'
|
||||
],
|
||||
'currency' => $currency,
|
||||
])
|
||||
->add('isFixed', YesNoType::class, [
|
||||
'label' => 'label.fixedRate'
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => ActivityRate::class,
|
||||
'csrf_protection' => true,
|
||||
'csrf_field_name' => '_token',
|
||||
'expand_users' => true,
|
||||
'csrf_token_id' => 'admin_customer_rate_edit',
|
||||
'attr' => [
|
||||
'data-form-event' => 'kimai.activityUpdate'
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
71
src/Form/CustomerRateForm.php
Normal file
71
src/Form/CustomerRateForm.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\CustomerRate;
|
||||
use App\Entity\Rate;
|
||||
use App\Form\Type\UserType;
|
||||
use App\Form\Type\YesNoType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class CustomerRateForm extends AbstractType
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$currency = null;
|
||||
if ($options['data']) {
|
||||
/** @var CustomerRate $rate */
|
||||
$rate = $options['data'];
|
||||
|
||||
if (null !== $customer = $rate->getCustomer()) {
|
||||
$currency = $customer->getCurrency();
|
||||
}
|
||||
}
|
||||
|
||||
$builder
|
||||
->add('user', UserType::class, [
|
||||
'required' => false,
|
||||
])
|
||||
->add('rate', MoneyType::class, [
|
||||
'label' => 'label.rate',
|
||||
'attr' => [
|
||||
'autofocus' => 'autofocus'
|
||||
],
|
||||
'currency' => $currency,
|
||||
])
|
||||
->add('isFixed', YesNoType::class, [
|
||||
'label' => 'label.fixedRate'
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => CustomerRate::class,
|
||||
'csrf_protection' => true,
|
||||
'csrf_field_name' => '_token',
|
||||
'expand_users' => true,
|
||||
'csrf_token_id' => 'admin_customer_rate_edit',
|
||||
'attr' => [
|
||||
'data-form-event' => 'kimai.customerUpdate'
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,6 @@ namespace App\Form;
|
||||
|
||||
use App\Form\Type\ColorPickerType;
|
||||
use App\Form\Type\DurationType;
|
||||
use App\Form\Type\FixedRateType;
|
||||
use App\Form\Type\HourlyRateType;
|
||||
use App\Form\Type\MetaFieldsCollectionType;
|
||||
use App\Form\Type\YesNoType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
@@ -26,12 +24,6 @@ trait EntityFormTrait
|
||||
$currency = $options['currency'];
|
||||
$builder
|
||||
->add('color', ColorPickerType::class)
|
||||
->add('fixedRate', FixedRateType::class, [
|
||||
'currency' => $currency,
|
||||
])
|
||||
->add('hourlyRate', HourlyRateType::class, [
|
||||
'currency' => $currency,
|
||||
])
|
||||
;
|
||||
|
||||
if ($options['include_budget']) {
|
||||
|
||||
71
src/Form/ProjectRateForm.php
Normal file
71
src/Form/ProjectRateForm.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\ProjectRate;
|
||||
use App\Entity\Rate;
|
||||
use App\Form\Type\UserType;
|
||||
use App\Form\Type\YesNoType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class ProjectRateForm extends AbstractType
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$currency = null;
|
||||
if ($options['data']) {
|
||||
/** @var ProjectRate $rate */
|
||||
$rate = $options['data'];
|
||||
|
||||
if (null !== $customer = $rate->getProject()->getCustomer()) {
|
||||
$currency = $customer->getCurrency();
|
||||
}
|
||||
}
|
||||
|
||||
$builder
|
||||
->add('user', UserType::class, [
|
||||
'required' => false,
|
||||
])
|
||||
->add('rate', MoneyType::class, [
|
||||
'label' => 'label.rate',
|
||||
'attr' => [
|
||||
'autofocus' => 'autofocus'
|
||||
],
|
||||
'currency' => $currency,
|
||||
])
|
||||
->add('isFixed', YesNoType::class, [
|
||||
'label' => 'label.fixedRate'
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => ProjectRate::class,
|
||||
'csrf_protection' => true,
|
||||
'csrf_field_name' => '_token',
|
||||
'expand_users' => true,
|
||||
'csrf_token_id' => 'admin_project_rate_edit',
|
||||
'attr' => [
|
||||
'data-form-event' => 'kimai.projectUpdate'
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -29,12 +29,6 @@ class InvoiceModelActivityHydrator implements InvoiceModelHydrator
|
||||
'activity.id' => $activity->getId(),
|
||||
'activity.name' => $activity->getName(),
|
||||
'activity.comment' => $activity->getComment(),
|
||||
'activity.fixed_rate' => $formatter->getFormattedMoney($activity->getFixedRate(), $currency),
|
||||
'activity.fixed_rate_nc' => $formatter->getFormattedMoney($activity->getFixedRate(), null),
|
||||
'activity.fixed_rate_plain' => $activity->getFixedRate(),
|
||||
'activity.hourly_rate' => $formatter->getFormattedMoney($activity->getHourlyRate(), $currency),
|
||||
'activity.hourly_rate_nc' => $formatter->getFormattedMoney($activity->getHourlyRate(), null),
|
||||
'activity.hourly_rate_plain' => $activity->getHourlyRate(),
|
||||
];
|
||||
|
||||
foreach ($activity->getVisibleMetaFields() as $metaField) {
|
||||
|
||||
@@ -36,12 +36,6 @@ class InvoiceModelCustomerHydrator implements InvoiceModelHydrator
|
||||
'customer.country' => $customer->getCountry(),
|
||||
'customer.homepage' => $customer->getHomepage(),
|
||||
'customer.comment' => $customer->getComment(),
|
||||
'customer.fixed_rate' => $formatter->getFormattedMoney($customer->getFixedRate(), $currency),
|
||||
'customer.fixed_rate_nc' => $formatter->getFormattedMoney($customer->getFixedRate(), null),
|
||||
'customer.fixed_rate_plain' => $customer->getFixedRate(),
|
||||
'customer.hourly_rate' => $formatter->getFormattedMoney($customer->getHourlyRate(), $currency),
|
||||
'customer.hourly_rate_nc' => $formatter->getFormattedMoney($customer->getHourlyRate(), null),
|
||||
'customer.hourly_rate_plain' => $customer->getHourlyRate(),
|
||||
];
|
||||
|
||||
foreach ($customer->getVisibleMetaFields() as $metaField) {
|
||||
|
||||
@@ -33,12 +33,6 @@ class InvoiceModelProjectHydrator implements InvoiceModelHydrator
|
||||
'project.start_date' => null !== $project->getStart() ? $formatter->getFormattedDateTime($project->getStart()) : '',
|
||||
'project.end_date' => null !== $project->getEnd() ? $formatter->getFormattedDateTime($project->getEnd()) : '',
|
||||
'project.order_date' => null !== $project->getOrderDate() ? $formatter->getFormattedDateTime($project->getOrderDate()) : '',
|
||||
'project.fixed_rate' => $formatter->getFormattedMoney($project->getFixedRate(), $currency),
|
||||
'project.fixed_rate_nc' => $formatter->getFormattedMoney($project->getFixedRate(), null),
|
||||
'project.fixed_rate_plain' => $project->getFixedRate(),
|
||||
'project.hourly_rate' => $formatter->getFormattedMoney($project->getHourlyRate(), $currency),
|
||||
'project.hourly_rate_nc' => $formatter->getFormattedMoney($project->getHourlyRate(), null),
|
||||
'project.hourly_rate_plain' => $project->getHourlyRate(),
|
||||
'project.budget_money' => $formatter->getFormattedMoney($project->getBudget(), $currency),
|
||||
'project.budget_money_nc' => $formatter->getFormattedMoney($project->getBudget(), null),
|
||||
'project.budget_money_plain' => $project->getBudget(),
|
||||
|
||||
@@ -14,6 +14,11 @@ namespace DoctrineMigrations;
|
||||
use App\Doctrine\AbstractMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
|
||||
/**
|
||||
* Adds language and decimal_duration column to invoice template table
|
||||
*
|
||||
* @version 1.8
|
||||
*/
|
||||
final class Version20200204124425 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
|
||||
71
src/Migrations/Version20200205115243.php
Normal file
71
src/Migrations/Version20200205115243.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use App\Doctrine\AbstractMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
|
||||
/**
|
||||
* Adds the rate table, which allows to define user specific rate rules
|
||||
*
|
||||
* @version 1.8
|
||||
*/
|
||||
final class Version20200205115243 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Adds the rate table, which allows to define user specific rate rules';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$customerRates = $schema->createTable('kimai2_customers_rates');
|
||||
$customerRates->addColumn('id', 'integer', ['autoincrement' => true, 'notnull' => true]);
|
||||
$customerRates->addColumn('user_id', 'integer', ['length' => 11, 'notnull' => false]);
|
||||
$customerRates->addColumn('customer_id', 'integer', ['length' => 11, 'notnull' => false]);
|
||||
$customerRates->addColumn('rate', 'float', ['notnull' => true]);
|
||||
$customerRates->addColumn('fixed', 'boolean', ['notnull' => true]);
|
||||
$customerRates->addForeignKeyConstraint('kimai2_users', ['user_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_82AB0AECA76ED395');
|
||||
$customerRates->addForeignKeyConstraint('kimai2_customers', ['customer_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_82AB0AEC9395C3F3');
|
||||
$customerRates->addUniqueIndex(['user_id', 'customer_id'], 'UNIQ_82AB0AECA76ED3959395C3F3');
|
||||
$customerRates->setPrimaryKey(['id']);
|
||||
|
||||
$projectRates = $schema->createTable('kimai2_projects_rates');
|
||||
$projectRates->addColumn('id', 'integer', ['autoincrement' => true, 'notnull' => true]);
|
||||
$projectRates->addColumn('user_id', 'integer', ['length' => 11, 'notnull' => false]);
|
||||
$projectRates->addColumn('project_id', 'integer', ['length' => 11, 'notnull' => false]);
|
||||
$projectRates->addColumn('rate', 'float', ['notnull' => true]);
|
||||
$projectRates->addColumn('fixed', 'boolean', ['notnull' => true]);
|
||||
$projectRates->addForeignKeyConstraint('kimai2_users', ['user_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_41535D55A76ED395');
|
||||
$projectRates->addForeignKeyConstraint('kimai2_projects', ['project_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_41535D55166D1F9C');
|
||||
$projectRates->addUniqueIndex(['user_id', 'project_id'], 'UNIQ_41535D55A76ED395166D1F9C');
|
||||
$projectRates->setPrimaryKey(['id']);
|
||||
|
||||
$activityRates = $schema->createTable('kimai2_activities_rates');
|
||||
$activityRates->addColumn('id', 'integer', ['autoincrement' => true, 'notnull' => true]);
|
||||
$activityRates->addColumn('user_id', 'integer', ['length' => 11, 'notnull' => false]);
|
||||
$activityRates->addColumn('activity_id', 'integer', ['length' => 11, 'notnull' => false]);
|
||||
$activityRates->addColumn('rate', 'float', ['notnull' => true]);
|
||||
$activityRates->addColumn('fixed', 'boolean', ['notnull' => true]);
|
||||
$activityRates->addForeignKeyConstraint('kimai2_users', ['user_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_4A7F11BEA76ED395');
|
||||
$activityRates->addForeignKeyConstraint('kimai2_activities', ['activity_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_4A7F11BE81C06096');
|
||||
$activityRates->addUniqueIndex(['user_id', 'activity_id'], 'UNIQ_4A7F11BEA76ED39581C06096');
|
||||
$activityRates->setPrimaryKey(['id']);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$schema->dropTable('kimai2_activities_rates');
|
||||
$schema->dropTable('kimai2_projects_rates');
|
||||
$schema->dropTable('kimai2_customers_rates');
|
||||
}
|
||||
}
|
||||
72
src/Migrations/Version20200205115244.php
Normal file
72
src/Migrations/Version20200205115244.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use App\Doctrine\AbstractMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
|
||||
/**
|
||||
* Migrates the data from entity tables to user specific rate tables
|
||||
*
|
||||
* @version 1.8
|
||||
*/
|
||||
final class Version20200205115244 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Migrates the data from entity tables to user specific rate tables';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$migrates = [
|
||||
['kimai2_activities', 'activity_id', 'kimai2_activities_rates'],
|
||||
['kimai2_projects', 'project_id', 'kimai2_projects_rates'],
|
||||
['kimai2_customers', 'customer_id', 'kimai2_customers_rates'],
|
||||
];
|
||||
|
||||
foreach ($migrates as $migrateOpts) {
|
||||
$tableName = $migrateOpts[0];
|
||||
$fieldName = $migrateOpts[1];
|
||||
$targetTable = $migrateOpts[2];
|
||||
|
||||
$rules = $this->connection->prepare(
|
||||
'SELECT id, fixed_rate, hourly_rate FROM ' . $tableName . ' WHERE fixed_rate IS NOT NULL OR hourly_rate IS NOT NULL'
|
||||
);
|
||||
$rules->execute();
|
||||
|
||||
foreach ($rules->fetchAll() as $rateRule) {
|
||||
$isFixed = $rateRule['fixed_rate'] !== null;
|
||||
$rate = $rateRule['fixed_rate'] ?? $rateRule['hourly_rate'];
|
||||
$params = ['user_id' => null, $fieldName => $rateRule['id'], 'rate' => $rate, 'fixed' => $isFixed];
|
||||
|
||||
$this->connection->insert($targetTable, $params, ['fixed' => \PDO::PARAM_BOOL]);
|
||||
}
|
||||
}
|
||||
|
||||
$schema->getTable('kimai2_customers')->dropColumn('fixed_rate')->dropColumn('hourly_rate');
|
||||
$schema->getTable('kimai2_projects')->dropColumn('fixed_rate')->dropColumn('hourly_rate');
|
||||
$schema->getTable('kimai2_activities')->dropColumn('fixed_rate')->dropColumn('hourly_rate');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE kimai2_customers ADD COLUMN fixed_rate NUMERIC(10, 2) DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE kimai2_customers ADD COLUMN hourly_rate NUMERIC(10, 2) DEFAULT NULL');
|
||||
|
||||
$this->addSql('ALTER TABLE kimai2_projects ADD COLUMN fixed_rate NUMERIC(10, 2) DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE kimai2_projects ADD COLUMN hourly_rate NUMERIC(10, 2) DEFAULT NULL');
|
||||
|
||||
$this->addSql('ALTER TABLE kimai2_activities ADD COLUMN fixed_rate NUMERIC(10, 2) DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE kimai2_activities ADD COLUMN hourly_rate NUMERIC(10, 2) DEFAULT NULL');
|
||||
}
|
||||
}
|
||||
62
src/Repository/ActivityRateRepository.php
Normal file
62
src/Repository/ActivityRateRepository.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\ActivityRate;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\ORMException;
|
||||
|
||||
class ActivityRateRepository extends EntityRepository
|
||||
{
|
||||
public function saveRate(ActivityRate $rate)
|
||||
{
|
||||
$entityManager = $this->getEntityManager();
|
||||
$entityManager->persist($rate);
|
||||
$entityManager->flush();
|
||||
}
|
||||
|
||||
public function deleteRate(ActivityRate $rate)
|
||||
{
|
||||
$em = $this->getEntityManager();
|
||||
$em->beginTransaction();
|
||||
|
||||
try {
|
||||
$em->remove($rate);
|
||||
$em->flush();
|
||||
$em->commit();
|
||||
} catch (ORMException $ex) {
|
||||
$em->rollback();
|
||||
throw $ex;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Activity $activity
|
||||
* @return ActivityRate[]
|
||||
*/
|
||||
public function getRatesForActivity(Activity $activity): array
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
|
||||
$qb->select('r, u, a')
|
||||
->from(ActivityRate::class, 'r')
|
||||
->leftJoin('r.user', 'u')
|
||||
->leftJoin('r.activity', 'a')
|
||||
->andWhere(
|
||||
$qb->expr()->eq('r.activity', ':activity')
|
||||
)
|
||||
->addOrderBy('u.alias')
|
||||
->setParameter('activity', $activity)
|
||||
;
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
}
|
||||
62
src/Repository/CustomerRateRepository.php
Normal file
62
src/Repository/CustomerRateRepository.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\CustomerRate;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\ORMException;
|
||||
|
||||
class CustomerRateRepository extends EntityRepository
|
||||
{
|
||||
public function saveRate(CustomerRate $rate)
|
||||
{
|
||||
$entityManager = $this->getEntityManager();
|
||||
$entityManager->persist($rate);
|
||||
$entityManager->flush();
|
||||
}
|
||||
|
||||
public function deleteRate(CustomerRate $rate)
|
||||
{
|
||||
$em = $this->getEntityManager();
|
||||
$em->beginTransaction();
|
||||
|
||||
try {
|
||||
$em->remove($rate);
|
||||
$em->flush();
|
||||
$em->commit();
|
||||
} catch (ORMException $ex) {
|
||||
$em->rollback();
|
||||
throw $ex;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Customer $customer
|
||||
* @return CustomerRate[]
|
||||
*/
|
||||
public function getRatesForCustomer(Customer $customer): array
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
|
||||
$qb->select('r, u, c')
|
||||
->from(CustomerRate::class, 'r')
|
||||
->leftJoin('r.user', 'u')
|
||||
->leftJoin('r.customer', 'c')
|
||||
->andWhere(
|
||||
$qb->expr()->eq('r.customer', ':customer')
|
||||
)
|
||||
->addOrderBy('u.alias')
|
||||
->setParameter('customer', $customer)
|
||||
;
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
}
|
||||
62
src/Repository/ProjectRateRepository.php
Normal file
62
src/Repository/ProjectRateRepository.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Project;
|
||||
use App\Entity\ProjectRate;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\ORMException;
|
||||
|
||||
class ProjectRateRepository extends EntityRepository
|
||||
{
|
||||
public function saveRate(ProjectRate $rate)
|
||||
{
|
||||
$entityManager = $this->getEntityManager();
|
||||
$entityManager->persist($rate);
|
||||
$entityManager->flush();
|
||||
}
|
||||
|
||||
public function deleteRate(ProjectRate $rate)
|
||||
{
|
||||
$em = $this->getEntityManager();
|
||||
$em->beginTransaction();
|
||||
|
||||
try {
|
||||
$em->remove($rate);
|
||||
$em->flush();
|
||||
$em->commit();
|
||||
} catch (ORMException $ex) {
|
||||
$em->rollback();
|
||||
throw $ex;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Project $project
|
||||
* @return ProjectRate[]
|
||||
*/
|
||||
public function getRatesForProject(Project $project): array
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
|
||||
$qb->select('r, u, p')
|
||||
->from(ProjectRate::class, 'r')
|
||||
->leftJoin('r.user', 'u')
|
||||
->leftJoin('r.project', 'p')
|
||||
->andWhere(
|
||||
$qb->expr()->eq('r.project', ':project')
|
||||
)
|
||||
->addOrderBy('u.alias')
|
||||
->setParameter('project', $project)
|
||||
;
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,10 @@
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\ActivityRate;
|
||||
use App\Entity\CustomerRate;
|
||||
use App\Entity\ProjectRate;
|
||||
use App\Entity\RateInterface;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Model\Statistic\Day;
|
||||
@@ -834,4 +838,73 @@ class TimesheetRepository extends EntityRepository
|
||||
|
||||
return $field;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $timesheet
|
||||
* @return RateInterface[]
|
||||
*/
|
||||
public function findMatchingRates(Timesheet $timesheet): array
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
$qb->select('r, u, a')
|
||||
->from(ActivityRate::class, 'r')
|
||||
->leftJoin('r.user', 'u')
|
||||
->leftJoin('r.activity', 'a')
|
||||
->andWhere(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->eq('r.user', ':user'),
|
||||
$qb->expr()->isNull('r.user')
|
||||
),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->eq('r.activity', ':activity'),
|
||||
$qb->expr()->isNull('r.activity')
|
||||
)
|
||||
)
|
||||
->setParameter('user', $timesheet->getUser())
|
||||
->setParameter('activity', $timesheet->getActivity())
|
||||
;
|
||||
$results = $qb->getQuery()->getResult();
|
||||
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
$qb->select('r, u, p')
|
||||
->from(ProjectRate::class, 'r')
|
||||
->leftJoin('r.user', 'u')
|
||||
->leftJoin('r.project', 'p')
|
||||
->andWhere(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->eq('r.user', ':user'),
|
||||
$qb->expr()->isNull('r.user')
|
||||
),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->eq('r.project', ':project'),
|
||||
$qb->expr()->isNull('r.project')
|
||||
)
|
||||
)
|
||||
->setParameter('user', $timesheet->getUser())
|
||||
->setParameter('project', $timesheet->getProject())
|
||||
;
|
||||
$results = array_merge($results, $qb->getQuery()->getResult());
|
||||
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
$qb->select('r, u, c')
|
||||
->from(CustomerRate::class, 'r')
|
||||
->leftJoin('r.user', 'u')
|
||||
->leftJoin('r.customer', 'c')
|
||||
->andWhere(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->eq('r.user', ':user'),
|
||||
$qb->expr()->isNull('r.user')
|
||||
),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->eq('r.customer', ':customer'),
|
||||
$qb->expr()->isNull('r.customer')
|
||||
)
|
||||
)
|
||||
->setParameter('user', $timesheet->getUser())
|
||||
->setParameter('customer', $timesheet->getProject()->getCustomer())
|
||||
;
|
||||
$results = array_merge($results, $qb->getQuery()->getResult());
|
||||
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,11 @@
|
||||
|
||||
namespace App\Timesheet\Calculator;
|
||||
|
||||
use App\Entity\Rate;
|
||||
use App\Entity\RateInterface;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\UserPreference;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Timesheet\CalculatorInterface;
|
||||
use App\Timesheet\Util;
|
||||
|
||||
@@ -22,15 +25,16 @@ class RateCalculator implements CalculatorInterface
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $rates;
|
||||
|
||||
private $rates;
|
||||
/**
|
||||
* RateCalculator constructor.
|
||||
* @param array $rates
|
||||
* @var TimesheetRepository
|
||||
*/
|
||||
public function __construct(array $rates)
|
||||
private $repository;
|
||||
|
||||
public function __construct(array $rates, TimesheetRepository $repository)
|
||||
{
|
||||
$this->rates = $rates;
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,7 +48,21 @@ class RateCalculator implements CalculatorInterface
|
||||
return;
|
||||
}
|
||||
|
||||
$fixedRate = $this->findFixedRate($record);
|
||||
$fixedRate = $record->getFixedRate();
|
||||
$hourlyRate = $record->getHourlyRate();
|
||||
|
||||
if (null === $fixedRate && null === $hourlyRate) {
|
||||
$rate = $this->getBestFittingRate($record);
|
||||
|
||||
if (null !== $rate) {
|
||||
if ($rate->isFixed()) {
|
||||
$fixedRate = $rate->getRate();
|
||||
} else {
|
||||
$hourlyRate = $rate->getRate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $fixedRate) {
|
||||
$record->setRate($fixedRate);
|
||||
$record->setFixedRate($fixedRate);
|
||||
@@ -52,74 +70,40 @@ class RateCalculator implements CalculatorInterface
|
||||
return;
|
||||
}
|
||||
|
||||
$hourlyRate = $this->findHourlyRate($record);
|
||||
if (null === $hourlyRate) {
|
||||
$hourlyRate = (float) $record->getUser()->getPreferenceValue(UserPreference::HOURLY_RATE, 0.00);
|
||||
}
|
||||
|
||||
$factor = $this->getRateFactor($record);
|
||||
|
||||
$hourlyRate = (float) ($hourlyRate * $factor);
|
||||
$rate = 0;
|
||||
$totalRate = 0;
|
||||
if (null !== $record->getDuration()) {
|
||||
$rate = Util::calculateRate($hourlyRate, $record->getDuration());
|
||||
$totalRate = Util::calculateRate($hourlyRate, $record->getDuration());
|
||||
}
|
||||
|
||||
$record->setHourlyRate($hourlyRate);
|
||||
$record->setRate($rate);
|
||||
$record->setRate($totalRate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $record
|
||||
* @return float
|
||||
*/
|
||||
protected function findHourlyRate(Timesheet $record)
|
||||
private function getBestFittingRate(Timesheet $timesheet): ?RateInterface
|
||||
{
|
||||
if (null !== $record->getHourlyRate()) {
|
||||
return $record->getHourlyRate();
|
||||
}
|
||||
|
||||
$activity = $record->getActivity();
|
||||
if (null !== $activity->getHourlyRate()) {
|
||||
return $activity->getHourlyRate();
|
||||
}
|
||||
|
||||
$project = $record->getProject();
|
||||
if (null !== $project) {
|
||||
if (null !== $project->getHourlyRate()) {
|
||||
return $project->getHourlyRate();
|
||||
$rates = $this->repository->findMatchingRates($timesheet);
|
||||
/** @var RateInterface[] $sorted */
|
||||
$sorted = [];
|
||||
foreach ($rates as $rate) {
|
||||
$score = $rate->getScore();
|
||||
if (null !== $rate->getUser() && $timesheet->getUser() === $rate->getUser()) {
|
||||
++$score;
|
||||
}
|
||||
|
||||
$customer = $project->getCustomer();
|
||||
if (null !== $customer->getHourlyRate()) {
|
||||
return $customer->getHourlyRate();
|
||||
}
|
||||
$sorted[$score] = $rate;
|
||||
}
|
||||
|
||||
return (float) $record->getUser()->getPreferenceValue(UserPreference::HOURLY_RATE, 0);
|
||||
}
|
||||
if (!empty($sorted)) {
|
||||
ksort($sorted);
|
||||
|
||||
/**
|
||||
* @param Timesheet $record
|
||||
* @return float|null
|
||||
*/
|
||||
protected function findFixedRate(Timesheet $record)
|
||||
{
|
||||
if (null !== $record->getFixedRate()) {
|
||||
return $record->getFixedRate();
|
||||
}
|
||||
|
||||
$activity = $record->getActivity();
|
||||
if (null !== $activity->getFixedRate()) {
|
||||
return $activity->getFixedRate();
|
||||
}
|
||||
|
||||
$project = $record->getProject();
|
||||
if (null !== $project) {
|
||||
if (null !== $project->getFixedRate()) {
|
||||
return $project->getFixedRate();
|
||||
}
|
||||
|
||||
$customer = $project->getCustomer();
|
||||
if (null !== $customer->getFixedRate()) {
|
||||
return $customer->getFixedRate();
|
||||
}
|
||||
return end($sorted);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user