diff --git a/config/packages/kimai.yaml b/config/packages/kimai.yaml index 61c9d847..42703db2 100644 --- a/config/packages/kimai.yaml +++ b/config/packages/kimai.yaml @@ -101,6 +101,7 @@ kimai: RATE: ['view_rate_own_timesheet','edit_rate_own_timesheet'] RATE_OTHER: ['view_rate_other_timesheet','edit_rate_other_timesheet'] EXPORT: ['create_export','edit_export_own_timesheet','edit_export_other_timesheet'] + BILLABLE: ['edit_billable_own_timesheet','edit_billable_other_timesheet'] TEAMS: ['view_team','create_team','edit_team','delete_team'] LOCKDOWN: ['lockdown_grace_timesheet','lockdown_override_timesheet'] REPORTING: ['view_reporting','view_other_reporting'] @@ -111,9 +112,9 @@ kimai: SINGLE_SUPER_ADMIN: ['hourly-rate_own_profile','hourly-rate_other_profile','roles_own_profile','system_information','system_configuration','plugins','edit_exported_timesheet','teams_own_profile','view_team_member','upload_invoice_template','view_all_data'] # link above sets to one complete set for each user role ROLE_USER: ['@TIMESHEET','@PROFILE','@REPORTING','@SINGLE_USER'] - ROLE_TEAMLEAD: ['@ACTIVITIES_TEAMLEAD','@PROJECTS_TEAMLEAD','@CUSTOMERS_TEAMLEAD','@TIMESHEET_OTHER','@INVOICE','@TIMESHEET','@PROFILE','@EXPORT','@TAGS','@REPORTING','@SINGLE_TEAMLEAD'] - ROLE_ADMIN: ['@ACTIVITIES','@PROJECTS','@CUSTOMERS','@INVOICE','@INVOICE_ADMIN','@TIMESHEET','@TIMESHEET_OTHER','@PROFILE','@TEAMS','@RATE','@RATE_OTHER','@EXPORT','@TAGS','@LOCKDOWN','@REPORTING','@SINGLE_ADMIN'] - ROLE_SUPER_ADMIN: ['@ACTIVITIES','@PROJECTS','@CUSTOMERS','@INVOICE','@INVOICE_ADMIN','@TIMESHEET','@TIMESHEET_OTHER','@PROFILE','@PROFILE_OTHER','@USER','@TEAMS','@RATE','@RATE_OTHER','@EXPORT','@TAGS','@LOCKDOWN','@REPORTING','@SINGLE_SUPER_ADMIN'] + ROLE_TEAMLEAD: ['@ACTIVITIES_TEAMLEAD','@PROJECTS_TEAMLEAD','@CUSTOMERS_TEAMLEAD','@TIMESHEET_OTHER','@INVOICE','@TIMESHEET','@PROFILE','@EXPORT','@BILLABLE','@TAGS','@REPORTING','@SINGLE_TEAMLEAD'] + ROLE_ADMIN: ['@ACTIVITIES','@PROJECTS','@CUSTOMERS','@INVOICE','@INVOICE_ADMIN','@TIMESHEET','@TIMESHEET_OTHER','@PROFILE','@TEAMS','@RATE','@RATE_OTHER','@EXPORT','@BILLABLE','@TAGS','@LOCKDOWN','@REPORTING','@SINGLE_ADMIN'] + ROLE_SUPER_ADMIN: ['@ACTIVITIES','@PROJECTS','@CUSTOMERS','@INVOICE','@INVOICE_ADMIN','@TIMESHEET','@TIMESHEET_OTHER','@PROFILE','@PROFILE_OTHER','@USER','@TEAMS','@RATE','@RATE_OTHER','@EXPORT','@BILLABLE','@TAGS','@LOCKDOWN','@REPORTING','@SINGLE_SUPER_ADMIN'] # mapping "sets" or permissions to user roles ("role name" = [array of "set names"]) maps: ROLE_USER: ['ROLE_USER'] diff --git a/src/API/TimesheetController.php b/src/API/TimesheetController.php index e2ce8124..5dc589b4 100644 --- a/src/API/TimesheetController.php +++ b/src/API/TimesheetController.php @@ -317,6 +317,7 @@ class TimesheetController extends BaseApiController $form = $this->createForm(TimesheetApiEditForm::class, $timesheet, [ 'include_rate' => $this->isGranted('edit_rate', $timesheet), 'include_exported' => $this->isGranted('edit_export', $timesheet), + 'include_billable' => $this->isGranted('edit_billable', $timesheet), 'include_user' => $this->isGranted('create_other_timesheet'), 'allow_begin_datetime' => $mode->canUpdateTimesWithAPI(), 'allow_end_datetime' => $mode->canUpdateTimesWithAPI(), @@ -394,6 +395,7 @@ class TimesheetController extends BaseApiController $form = $this->createForm(TimesheetApiEditForm::class, $timesheet, [ 'include_rate' => $this->isGranted('edit_rate', $timesheet), 'include_exported' => $this->isGranted('edit_export', $timesheet), + 'include_billable' => $this->isGranted('edit_billable', $timesheet), 'include_user' => $this->isGranted('edit', $timesheet), 'allow_begin_datetime' => $mode->canUpdateTimesWithAPI(), 'allow_end_datetime' => $mode->canUpdateTimesWithAPI(), @@ -614,12 +616,10 @@ class TimesheetController extends BaseApiController @trigger_error('Setting the "copy" attribute in "restart timesheet" API to something else then "all" is deprecated', E_USER_DEPRECATED); } - $copyTimesheet - ->setHourlyRate($timesheet->getHourlyRate()) - ->setFixedRate($timesheet->getFixedRate()) - ->setDescription($timesheet->getDescription()) - ->setBillable($timesheet->isBillable()) - ; + $copyTimesheet->setHourlyRate($timesheet->getHourlyRate()); + $copyTimesheet->setFixedRate($timesheet->getFixedRate()); + $copyTimesheet->setDescription($timesheet->getDescription()); + $copyTimesheet->setBillable($timesheet->isBillable()); foreach ($timesheet->getTags() as $tag) { $copyTimesheet->addTag($tag); diff --git a/src/Configuration/SystemConfiguration.php b/src/Configuration/SystemConfiguration.php index 32f1dc54..f7fc9002 100644 --- a/src/Configuration/SystemConfiguration.php +++ b/src/Configuration/SystemConfiguration.php @@ -232,11 +232,6 @@ class SystemConfiguration implements SystemBundleConfiguration return (string) $this->find('timesheet.default_begin'); } - public function getTimesheetDefaultBillable(): bool - { - return (bool) $this->find('defaults.timesheet.billable'); - } - public function isTimesheetAllowFutureTimes(): bool { return (bool) $this->find('timesheet.rules.allow_future_times'); diff --git a/src/Controller/SystemConfigurationController.php b/src/Controller/SystemConfigurationController.php index f07a26fd..b5e3b9c4 100644 --- a/src/Controller/SystemConfigurationController.php +++ b/src/Controller/SystemConfigurationController.php @@ -383,10 +383,6 @@ final class SystemConfigurationController extends AbstractController ->setConstraints([ new GreaterThanOrEqual(['value' => 0]) ]), - (new Configuration()) - ->setName('defaults.timesheet.billable') - ->setType(YesNoType::class) - ->setOptions(['help' => 'default_value_new', 'label' => 'label.billable']), ]), (new SystemConfigurationModel('quick_entry')) ->setTranslation('quick_entry.title') diff --git a/src/Controller/TimesheetAbstractController.php b/src/Controller/TimesheetAbstractController.php index a536a1f2..d2c213f9 100644 --- a/src/Controller/TimesheetAbstractController.php +++ b/src/Controller/TimesheetAbstractController.php @@ -455,6 +455,7 @@ abstract class TimesheetAbstractController extends AbstractController 'action' => $this->generateUrl($this->getMultiUpdateRoute(), []), 'method' => 'POST', 'include_exported' => $this->isGranted($this->getPermissionEditExport()), + 'include_billable' => $this->isGranted($this->getPermissionEditBillable()), 'include_rate' => $this->isGranted($this->getPermissionEditRate()), 'include_user' => $this->includeUserInForms('multi'), ]); @@ -482,6 +483,7 @@ abstract class TimesheetAbstractController extends AbstractController 'action' => $action, 'include_rate' => $this->isGranted('edit_rate', $entry), 'include_exported' => $this->isGranted('edit_export', $entry), + 'include_billable' => $this->isGranted('edit_billable', $entry), 'include_user' => $this->includeUserInForms('create'), 'allow_begin_datetime' => $mode->canEditBegin(), 'allow_end_datetime' => $mode->canEditEnd(), @@ -510,6 +512,7 @@ abstract class TimesheetAbstractController extends AbstractController ]), 'include_rate' => $this->isGranted('edit_rate', $entry), 'include_exported' => $this->isGranted('edit_export', $entry), + 'include_billable' => $this->isGranted('edit_billable', $entry), 'include_user' => $this->includeUserInForms('edit'), 'allow_begin_datetime' => $mode->canEditBegin(), 'allow_end_datetime' => $mode->canEditEnd(), @@ -549,6 +552,11 @@ abstract class TimesheetAbstractController extends AbstractController return 'edit_export_own_timesheet'; } + protected function getPermissionEditBillable(): string + { + return 'edit_billable_own_timesheet'; + } + protected function getPermissionEditRate(): string { return 'edit_rate_own_timesheet'; diff --git a/src/Controller/TimesheetTeamController.php b/src/Controller/TimesheetTeamController.php index 9cd14726..fcae6876 100644 --- a/src/Controller/TimesheetTeamController.php +++ b/src/Controller/TimesheetTeamController.php @@ -153,8 +153,9 @@ class TimesheetTeamController extends TimesheetAbstractController return $this->createForm(TimesheetMultiUserEditForm::class, $entry, [ 'action' => $this->generateUrl('admin_timesheet_create_multiuser'), - 'include_rate' => $this->isGranted('edit_rate', $entry), - 'include_exported' => $this->isGranted('edit_export', $entry), + 'include_rate' => $this->isGranted($this->getPermissionEditRate()), + 'include_exported' => $this->isGranted($this->getPermissionEditExport()), + 'include_billable' => $this->isGranted($this->getPermissionEditBillable()), 'include_user' => $this->includeUserInForms('create'), 'allow_begin_datetime' => $mode->canEditBegin(), 'allow_end_datetime' => $mode->canEditEnd(), @@ -205,6 +206,11 @@ class TimesheetTeamController extends TimesheetAbstractController return 'edit_export_other_timesheet'; } + protected function getPermissionEditBillable(): string + { + return 'edit_billable_other_timesheet'; + } + protected function getPermissionEditRate(): string { return 'edit_rate_other_timesheet'; diff --git a/src/Entity/Activity.php b/src/Entity/Activity.php index b47f81e1..fc6d2b90 100644 --- a/src/Entity/Activity.php +++ b/src/Entity/Activity.php @@ -47,7 +47,7 @@ use Symfony\Component\Validator\Constraints as Assert; * } * ) * - * @Exporter\Order({"id", "name", "project", "budget", "timeBudget", "budgetType", "color", "visible", "comment"}) + * @Exporter\Order({"id", "name", "project", "budget", "timeBudget", "budgetType", "color", "visible", "comment", "billable"}) * @Exporter\Expose("project", label="label.project", exp="object.getProject() === null ? null : object.getProject().getName()") */ class Activity implements EntityWithMetaFields, EntityWithBudget @@ -123,6 +123,18 @@ class Activity implements EntityWithMetaFields, EntityWithBudget * @Assert\NotNull() */ private $visible = true; + /** + * @var bool + * + * @Serializer\Expose() + * @Serializer\Groups({"Default"}) + * + * @Exporter\Expose(label="label.billable", type="boolean") + * + * @ORM\Column(name="billable", type="boolean", nullable=false) + * @Assert\NotNull() + */ + private $billable = true; /** * Meta fields * @@ -227,6 +239,16 @@ class Activity implements EntityWithMetaFields, EntityWithBudget return $this->visible; } + public function setBillable(bool $billable): void + { + $this->billable = $billable; + } + + public function isBillable(): bool + { + return $this->billable; + } + /** * @return Collection|MetaTableTypeInterface[] */ diff --git a/src/Entity/Customer.php b/src/Entity/Customer.php index 17da4b26..27a25534 100644 --- a/src/Entity/Customer.php +++ b/src/Entity/Customer.php @@ -27,7 +27,7 @@ use Symfony\Component\Validator\Constraints as Assert; * * @Serializer\ExclusionPolicy("all") * - * @Exporter\Order({"id", "name", "company", "number", "vatId", "address", "contact","email", "phone", "mobile", "fax", "homepage", "country", "currency", "timezone", "budget", "timeBudget", "budgetType", "color", "visible", "teams", "comment"}) + * @Exporter\Order({"id", "name", "company", "number", "vatId", "address", "contact","email", "phone", "mobile", "fax", "homepage", "country", "currency", "timezone", "budget", "timeBudget", "budgetType", "color", "visible", "teams", "comment", "billable"}) * @ Exporter\Expose("teams", label="label.team", exp="object.getTeams().toArray()", type="array") */ class Customer implements EntityWithMetaFields, EntityWithBudget @@ -98,6 +98,18 @@ class Customer implements EntityWithMetaFields, EntityWithBudget * @Assert\NotNull() */ private $visible = true; + /** + * @var bool + * + * @Serializer\Expose() + * @Serializer\Groups({"Default"}) + * + * @Exporter\Expose(label="label.billable", type="boolean") + * + * @ORM\Column(name="billable", type="boolean", nullable=false) + * @Assert\NotNull() + */ + private $billable = true; /** * @var string|null * @@ -353,6 +365,16 @@ class Customer implements EntityWithMetaFields, EntityWithBudget return $this->visible; } + public function setBillable(bool $billable): void + { + $this->billable = $billable; + } + + public function isBillable(): bool + { + return $this->billable; + } + public function setCompany(?string $company): Customer { $this->company = $company; diff --git a/src/Entity/Project.php b/src/Entity/Project.php index d46c0617..bdd47b95 100644 --- a/src/Entity/Project.php +++ b/src/Entity/Project.php @@ -48,7 +48,7 @@ use Symfony\Component\Validator\Constraints as Assert; * } * ) * - * @Exporter\Order({"id", "name", "customer", "orderNumber", "orderDate", "start", "end", "budget", "timeBudget", "budgetType", "color", "visible", "teams", "comment"}) + * @Exporter\Order({"id", "name", "customer", "orderNumber", "orderDate", "start", "end", "budget", "timeBudget", "budgetType", "color", "visible", "teams", "comment", "billable"}) * @Exporter\Expose("customer", label="label.customer", exp="object.getCustomer() === null ? null : object.getCustomer().getName()") * @ Exporter\Expose("teams", label="label.team", exp="object.getTeams().toArray()", type="array") */ @@ -195,6 +195,18 @@ class Project implements EntityWithMetaFields, EntityWithBudget * @Assert\NotNull() */ private $visible = true; + /** + * @var bool + * + * @Serializer\Expose() + * @Serializer\Groups({"Default"}) + * + * @Exporter\Expose(label="label.billable", type="boolean") + * + * @ORM\Column(name="billable", type="boolean", nullable=false) + * @Assert\NotNull() + */ + private $billable = true; /** * Meta fields * @@ -294,6 +306,16 @@ class Project implements EntityWithMetaFields, EntityWithBudget return $this->visible; } + public function setBillable(bool $billable): void + { + $this->billable = $billable; + } + + public function isBillable(): bool + { + return $this->billable; + } + public function getOrderNumber(): ?string { return $this->orderNumber; diff --git a/src/Entity/Timesheet.php b/src/Entity/Timesheet.php index e54e8130..097b2e6e 100644 --- a/src/Entity/Timesheet.php +++ b/src/Entity/Timesheet.php @@ -98,6 +98,11 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface */ public const OVERTIME = 'overtime'; + public const BILLABLE_AUTOMATIC = 'auto'; + public const BILLABLE_YES = 'yes'; + public const BILLABLE_NO = 'no'; + public const BILLABLE_DEFAULT = 'default'; + /** * @var int|null * @@ -269,6 +274,11 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface * @Assert\NotNull() */ private $billable = true; + /** + * Internal property used to determine whether the billable field should be calculated automatically. + * @var string + */ + private $billableMode = self::BILLABLE_DEFAULT; /** * @var string * @@ -649,6 +659,11 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface return $this->billable; } + public function getBillable(): bool + { + return $this->billable; + } + public function setBillable(bool $billable): Timesheet { $this->billable = $billable; @@ -656,6 +671,16 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface return $this; } + public function getBillableMode(): string + { + return $this->billableMode; + } + + public function setBillableMode(string $billableMode): void + { + $this->billableMode = $billableMode; + } + public function getFixedRate(): ?float { return $this->fixedRate; diff --git a/src/Form/API/TimesheetApiEditForm.php b/src/Form/API/TimesheetApiEditForm.php index ef411c10..e4def9c1 100644 --- a/src/Form/API/TimesheetApiEditForm.php +++ b/src/Form/API/TimesheetApiEditForm.php @@ -10,12 +10,22 @@ namespace App\Form\API; use App\Form\TimesheetEditForm; +use App\Form\Type\BillableType; use App\Form\Type\TagsInputType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; class TimesheetApiEditForm extends TimesheetEditForm { + protected function addBillable(FormBuilderInterface $builder, array $options) + { + if (!$options['include_billable']) { + return; + } + + $builder->add('billable', BillableType::class, []); + } + /** * {@inheritdoc} */ @@ -49,6 +59,7 @@ class TimesheetApiEditForm extends TimesheetEditForm // because the docs are cached without these fields otherwise 'include_user' => true, 'include_exported' => true, + 'include_billable' => true, 'include_rate' => true, ]); } diff --git a/src/Form/EntityFormTrait.php b/src/Form/EntityFormTrait.php index 7ed89064..e7705381 100644 --- a/src/Form/EntityFormTrait.php +++ b/src/Form/EntityFormTrait.php @@ -9,6 +9,7 @@ namespace App\Form; +use App\Form\Type\BillableType; use App\Form\Type\BudgetType; use App\Form\Type\DurationType; use App\Form\Type\MetaFieldsCollectionType; @@ -48,7 +49,9 @@ trait EntityFormTrait $builder ->add('visible', YesNoType::class, [ 'label' => 'label.visible', - ]); + ]) + ->add('billable', BillableType::class) + ; } /** diff --git a/src/Form/MultiUpdate/TimesheetMultiUpdate.php b/src/Form/MultiUpdate/TimesheetMultiUpdate.php index c6db2994..5414d6aa 100644 --- a/src/Form/MultiUpdate/TimesheetMultiUpdate.php +++ b/src/Form/MultiUpdate/TimesheetMultiUpdate.php @@ -265,7 +265,7 @@ class TimesheetMultiUpdate extends AbstractType 'include_user' => false, 'include_rate' => false, 'include_exported' => false, - 'include_billable' => true, + 'include_billable' => false, ]); } } diff --git a/src/Form/TimesheetEditForm.php b/src/Form/TimesheetEditForm.php index 14b7bce2..659bdb7e 100644 --- a/src/Form/TimesheetEditForm.php +++ b/src/Form/TimesheetEditForm.php @@ -10,7 +10,6 @@ namespace App\Form; use App\Entity\Timesheet; -use App\Form\Type\BillableType; use App\Form\Type\DateTimePickerType; use App\Form\Type\DescriptionType; use App\Form\Type\DurationType; @@ -18,10 +17,12 @@ use App\Form\Type\FixedRateType; use App\Form\Type\HourlyRateType; use App\Form\Type\MetaFieldsCollectionType; use App\Form\Type\TagsType; +use App\Form\Type\TimesheetBillableType; use App\Form\Type\UserType; use App\Form\Type\YesNoType; use App\Repository\CustomerRepository; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\CallbackTransformer; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; @@ -169,7 +170,7 @@ class TimesheetEditForm extends AbstractType { $durationOptions = [ 'required' => false, - 'docu_chapter' => 'timesheet.html#duration-format', + 'docu_chapter' => 'duration-format.html', 'attr' => [ 'placeholder' => '0:00', ], @@ -261,11 +262,57 @@ class TimesheetEditForm extends AbstractType protected function addBillable(FormBuilderInterface $builder, array $options) { - if (!$options['include_billable']) { - return; + if ($options['include_billable']) { + $builder->add('billableMode', TimesheetBillableType::class, []); } - $builder->add('billable', BillableType::class, []); + $builder->addModelTransformer(new CallbackTransformer( + function (Timesheet $record) { + if ($record->getBillableMode() === Timesheet::BILLABLE_DEFAULT) { + if ($record->isBillable()) { + $record->setBillableMode(Timesheet::BILLABLE_YES); + } else { + $record->setBillableMode(Timesheet::BILLABLE_NO); + } + } + + return $record; + }, + function (Timesheet $record) { + switch ($record->getBillableMode()) { + case Timesheet::BILLABLE_NO: + $record->setBillable(false); + break; + case Timesheet::BILLABLE_YES: + $record->setBillable(true); + break; + case Timesheet::BILLABLE_AUTOMATIC: + $billable = true; + + $activity = $record->getActivity(); + if ($activity !== null && !$activity->isBillable()) { + $billable = false; + } + + $project = $record->getProject(); + if ($billable && $project !== null && !$project->isBillable()) { + $billable = false; + } + + if ($billable && $project !== null) { + $customer = $project->getCustomer(); + if ($customer !== null && !$customer->isBillable()) { + $billable = false; + } + } + + $record->setBillable($billable); + break; + } + + return $record; + } + )); } /** diff --git a/src/Form/Type/BillableType.php b/src/Form/Type/BillableType.php index d58db76e..224e1cb4 100644 --- a/src/Form/Type/BillableType.php +++ b/src/Form/Type/BillableType.php @@ -14,7 +14,6 @@ use Symfony\Component\OptionsResolver\OptionsResolver; /** * Custom form field type to select if something is billable. - * To be used in combination with the invoicing system. */ class BillableType extends AbstractType { diff --git a/src/Form/Type/TimesheetBillableType.php b/src/Form/Type/TimesheetBillableType.php new file mode 100644 index 00000000..6f078a7e --- /dev/null +++ b/src/Form/Type/TimesheetBillableType.php @@ -0,0 +1,44 @@ +setDefaults([ + 'label' => 'label.billable', + 'choices' => [ + 'automatic' => Timesheet::BILLABLE_AUTOMATIC, + 'yes' => Timesheet::BILLABLE_YES, + 'no' => Timesheet::BILLABLE_NO, + ], + ]); + } + + /** + * {@inheritdoc} + */ + public function getParent() + { + return ChoiceType::class; + } +} diff --git a/src/Migrations/Version20220315224645.php b/src/Migrations/Version20220315224645.php new file mode 100644 index 00000000..1df28040 --- /dev/null +++ b/src/Migrations/Version20220315224645.php @@ -0,0 +1,49 @@ +getTable('kimai2_customers'); + $customers->addColumn('billable', 'boolean', ['notnull' => true, 'default' => true]); + + $projects = $schema->getTable('kimai2_projects'); + $projects->addColumn('billable', 'boolean', ['notnull' => true, 'default' => true]); + + $activities = $schema->getTable('kimai2_activities'); + $activities->addColumn('billable', 'boolean', ['notnull' => true, 'default' => true]); + + $this->addSql('DELETE from kimai2_configuration WHERE `name` = "defaults.timesheet.billable"'); + } + + public function down(Schema $schema): void + { + $customers = $schema->getTable('kimai2_customers'); + $customers->dropColumn('billable'); + + $projects = $schema->getTable('kimai2_projects'); + $projects->dropColumn('billable'); + + $activities = $schema->getTable('kimai2_activities'); + $activities->dropColumn('billable'); + } +} diff --git a/src/Timesheet/TimesheetService.php b/src/Timesheet/TimesheetService.php index 44e80eff..0cba0e20 100644 --- a/src/Timesheet/TimesheetService.php +++ b/src/Timesheet/TimesheetService.php @@ -110,7 +110,7 @@ final class TimesheetService $mode = $this->trackingModeService->getActiveMode(); $mode->create($timesheet, $request); - $timesheet->setBillable($this->configuration->getTimesheetDefaultBillable()); + $timesheet->setBillableMode(Timesheet::BILLABLE_AUTOMATIC); return $timesheet; } @@ -118,6 +118,7 @@ final class TimesheetService /** * @param Timesheet $timesheet * @param Timesheet $copyFrom + * @return Timesheet * @throws ValidationFailedException for invalid timesheets or running timesheets that should be stopped * @throws InvalidArgumentException for already persisted timesheets * @throws AccessDeniedException if user is not allowed to start timesheet diff --git a/src/Voter/TimesheetVoter.php b/src/Voter/TimesheetVoter.php index 0c3b12c6..33dc4a01 100644 --- a/src/Voter/TimesheetVoter.php +++ b/src/Voter/TimesheetVoter.php @@ -44,6 +44,7 @@ final class TimesheetVoter extends Voter self::VIEW_RATE, self::EDIT_RATE, self::EDIT_EXPORT, + 'edit_billable', 'duplicate' ]; @@ -130,6 +131,7 @@ final class TimesheetVoter extends Voter case self::VIEW: case self::EXPORT: case self::EDIT_EXPORT: + case 'edit_billable': $permission .= $attribute; break; diff --git a/templates/activity/details.html.twig b/templates/activity/details.html.twig index fbd4acf6..8e1c56c0 100644 --- a/templates/activity/details.html.twig +++ b/templates/activity/details.html.twig @@ -41,6 +41,14 @@ {% endif %} + {% if not activity.billable %} +