From b46fdcefadaa7dbe93a37b72a00edf64f8b79d95 Mon Sep 17 00:00:00 2001 From: Kevin Papst Date: Sat, 11 Jun 2022 12:23:02 +0200 Subject: [PATCH] added new permission to separate time and money budget (#3352) --- config/packages/kimai.yaml | 28 +- src/API/ActivityController.php | 2 + src/API/CustomerController.php | 2 + src/API/ProjectController.php | 2 + src/Controller/ActivityController.php | 5 +- src/Controller/CustomerController.php | 5 +- src/Controller/ProjectController.php | 3 +- .../Reporting/ProjectDateRangeController.php | 2 +- .../Reporting/ProjectDetailsController.php | 2 +- .../Reporting/ProjectInactiveController.php | 2 +- .../Reporting/ProjectViewController.php | 2 +- .../Actions/CustomerSubscriber.php | 2 +- src/Form/ActivityEditForm.php | 1 + src/Form/CustomerEditForm.php | 1 + src/Form/EntityFormTrait.php | 40 ++- src/Form/ProjectEditForm.php | 1 + src/Reporting/ReportingService.php | 13 +- .../Constraints/TimesheetBudgetUsed.php | 1 + .../TimesheetBudgetUsedValidator.php | 19 +- src/Voter/ActivityVoter.php | 1 + src/Voter/CustomerVoter.php | 1 + src/Voter/EntityMultiRoleVoter.php | 121 ++++++++ src/Voter/ProjectVoter.php | 1 + src/Widget/Type/UserTeamProjects.php | 5 +- templates/activity/edit.html.twig | 26 +- templates/activity/index.html.twig | 16 +- templates/customer/edit.html.twig | 18 +- templates/customer/embed_projects.html.twig | 2 +- templates/customer/index.html.twig | 16 +- templates/embeds/budgets.html.twig | 290 +++++++++--------- templates/project/details.html.twig | 8 +- templates/project/edit.html.twig | 6 +- templates/project/index.html.twig | 16 +- .../reporting/project_daterange.html.twig | 61 ++-- templates/reporting/project_details.html.twig | 13 +- templates/reporting/project_view.html.twig | 154 +++++----- .../widget/widget-userteamprojects.html.twig | 6 +- tests/Controller/PermissionControllerTest.php | 2 +- tests/Form/ActivityEditFormTest.php | 38 ++- tests/Form/CustomerEditFormTest.php | 73 +++++ tests/Form/ProjectEditFormTest.php | 74 +++++ .../TimesheetBudgetUsedValidatorTest.php | 7 +- tests/Voter/AbstractVoterTest.php | 13 +- tests/Voter/EntityMultiRoleVoterTest.php | 86 ++++++ translations/validators.de.xlf | 4 + translations/validators.de_CH.xlf | 4 + translations/validators.en.xlf | 4 + 47 files changed, 853 insertions(+), 346 deletions(-) create mode 100644 src/Voter/EntityMultiRoleVoter.php create mode 100644 tests/Form/CustomerEditFormTest.php create mode 100644 tests/Form/ProjectEditFormTest.php create mode 100644 tests/Voter/EntityMultiRoleVoterTest.php diff --git a/config/packages/kimai.yaml b/config/packages/kimai.yaml index d3ba4d08..981a7e65 100644 --- a/config/packages/kimai.yaml +++ b/config/packages/kimai.yaml @@ -75,20 +75,20 @@ kimai: # PERMISSIONS # -------------------------------------------------------------------------------- permissions: - # mapping complex rule sets of single permissions to named "sets" ("set name" = [array of "permissions and sets"]) + # mapping complex rule sets of single permissions to named "sets" ("set name" = [array of "permissions and @SETS"]) sets: - ACTIVITIES: ['view_activity','create_activity','edit_activity','budget_activity','delete_activity','permissions_activity'] - ACTIVITIES_ALL_TEAMLEAD: ['view_teamlead_activity','edit_teamlead_activity','budget_teamlead_activity','permissions_teamlead_activity'] - ACTIVITIES_ALL_TEAM: ['view_team_activity','edit_team_activity','budget_team_activity'] - ACTIVITIES_TEAMLEAD: ['view_teamlead_activity','create_activity','edit_teamlead_activity','budget_teamlead_activity'] - PROJECTS: ['view_project','create_project','edit_project','budget_project','delete_project','permissions_project','comments_project','comments_create_project','details_project'] - PROJECTS_ALL_TEAMLEAD: ['view_teamlead_project','edit_teamlead_project','budget_teamlead_project','permissions_teamlead_project','comments_teamlead_project','comments_create_teamlead_project','details_teamlead_project'] - PROJECTS_ALL_TEAM: ['view_team_project','edit_team_project','budget_team_project','comments_team_project','comments_create_team_project','details_team_project'] - PROJECTS_TEAMLEAD: ['view_teamlead_project','budget_teamlead_project','comments_teamlead_project','comments_create_teamlead_project','details_teamlead_project'] - CUSTOMERS: ['view_customer','create_customer','edit_customer','budget_customer','delete_customer','permissions_customer','comments_customer','comments_create_customer','details_customer'] - CUSTOMERS_ALL_TEAMLEAD: ['view_teamlead_customer','edit_teamlead_customer','budget_teamlead_customer','permissions_teamlead_customer','comments_teamlead_customer','comments_create_teamlead_customer','details_teamlead_customer'] - CUSTOMERS_ALL_TEAM: ['view_team_customer','edit_team_customer','budget_team_customer','comments_team_customer','comments_create_team_customer','details_team_customer'] - CUSTOMERS_TEAMLEAD: ['view_teamlead_customer','budget_teamlead_customer','comments_teamlead_customer','comments_create_teamlead_customer','details_teamlead_customer'] + ACTIVITIES: ['view_activity','create_activity','edit_activity','budget_activity','time_activity','delete_activity','permissions_activity'] + ACTIVITIES_ALL_TEAMLEAD: ['view_teamlead_activity','edit_teamlead_activity','budget_teamlead_activity','time_teamlead_activity','permissions_teamlead_activity'] + ACTIVITIES_ALL_TEAM: ['view_team_activity','edit_team_activity','budget_team_activity','time_team_activity'] + ACTIVITIES_TEAMLEAD: ['view_teamlead_activity','create_activity','edit_teamlead_activity','budget_teamlead_activity','time_teamlead_activity'] + PROJECTS: ['view_project','create_project','edit_project','budget_project','time_project','delete_project','permissions_project','comments_project','comments_create_project','details_project'] + PROJECTS_ALL_TEAMLEAD: ['view_teamlead_project','edit_teamlead_project','budget_teamlead_project','time_teamlead_project','permissions_teamlead_project','comments_teamlead_project','comments_create_teamlead_project','details_teamlead_project'] + PROJECTS_ALL_TEAM: ['view_team_project','edit_team_project','budget_team_project','time_team_project','comments_team_project','comments_create_team_project','details_team_project'] + PROJECTS_TEAMLEAD: ['view_teamlead_project','budget_teamlead_project','time_teamlead_project','comments_teamlead_project','comments_create_teamlead_project','details_teamlead_project'] + CUSTOMERS: ['view_customer','create_customer','edit_customer','budget_customer','time_customer','delete_customer','permissions_customer','comments_customer','comments_create_customer','details_customer'] + CUSTOMERS_ALL_TEAMLEAD: ['view_teamlead_customer','edit_teamlead_customer','budget_teamlead_customer','time_teamlead_customer','permissions_teamlead_customer','comments_teamlead_customer','comments_create_teamlead_customer','details_teamlead_customer'] + CUSTOMERS_ALL_TEAM: ['view_team_customer','edit_team_customer','budget_team_customer','time_team_customer','comments_team_customer','comments_create_team_customer','details_team_customer'] + CUSTOMERS_TEAMLEAD: ['view_teamlead_customer','budget_teamlead_customer','time_teamlead_customer','comments_teamlead_customer','comments_create_teamlead_customer','details_teamlead_customer'] INVOICE: ['view_invoice','create_invoice'] INVOICE_ADMIN: ['manage_invoice_template'] INVOICE_ALL: ['delete_invoice'] @@ -106,7 +106,7 @@ kimai: LOCKDOWN: ['lockdown_grace_timesheet','lockdown_override_timesheet'] REPORTING: ['view_reporting','view_other_reporting'] # some single default definitions for roles - SINGLE_USER: ['view_team_member','budget_team_project'] + SINGLE_USER: ['view_team_member','time_team_project'] SINGLE_TEAMLEAD: ['view_rate_own_timesheet','view_rate_other_timesheet','hourly-rate_own_profile','view_team_member'] SINGLE_ADMIN: ['hourly-rate_own_profile','edit_exported_timesheet','teams_own_profile','view_team_member','view_all_data'] 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'] diff --git a/src/API/ActivityController.php b/src/API/ActivityController.php index 25b3ffd4..69571e06 100644 --- a/src/API/ActivityController.php +++ b/src/API/ActivityController.php @@ -212,6 +212,7 @@ class ActivityController extends BaseApiController $form = $this->createForm(ActivityApiEditForm::class, $activity, [ 'include_budget' => $this->isGranted('budget', $activity), + 'include_time' => $this->isGranted('time', $activity), ]); $form->submit($request->request->all()); @@ -276,6 +277,7 @@ class ActivityController extends BaseApiController $form = $this->createForm(ActivityApiEditForm::class, $activity, [ 'include_budget' => $this->isGranted('budget', $activity), + 'include_time' => $this->isGranted('time', $activity), ]); $form->setData($activity); diff --git a/src/API/CustomerController.php b/src/API/CustomerController.php index 5c324702..912d8f9d 100644 --- a/src/API/CustomerController.php +++ b/src/API/CustomerController.php @@ -182,6 +182,7 @@ class CustomerController extends BaseApiController $form = $this->createForm(CustomerApiEditForm::class, $customer, [ 'include_budget' => $this->isGranted('budget', $customer), + 'include_time' => $this->isGranted('time', $customer), ]); $form->submit($request->request->all()); @@ -246,6 +247,7 @@ class CustomerController extends BaseApiController $form = $this->createForm(CustomerApiEditForm::class, $customer, [ 'include_budget' => $this->isGranted('budget', $customer), + 'include_time' => $this->isGranted('time', $customer), ]); $form->setData($customer); diff --git a/src/API/ProjectController.php b/src/API/ProjectController.php index 64b1acb6..610e0f5e 100644 --- a/src/API/ProjectController.php +++ b/src/API/ProjectController.php @@ -226,6 +226,7 @@ class ProjectController extends BaseApiController 'timezone' => $this->getDateTimeFactory()->getTimezone()->getName(), 'date_format' => self::DATE_FORMAT, 'include_budget' => $this->isGranted('budget', $project), + 'include_time' => $this->isGranted('time', $project), ]); $form->submit($request->request->all()); @@ -292,6 +293,7 @@ class ProjectController extends BaseApiController 'timezone' => $this->getDateTimeFactory()->getTimezone()->getName(), 'date_format' => self::DATE_FORMAT, 'include_budget' => $this->isGranted('budget', $project), + 'include_time' => $this->isGranted('time', $project), ]); $form->setData($project); diff --git a/src/Controller/ActivityController.php b/src/Controller/ActivityController.php index f50d36be..5349d06d 100644 --- a/src/Controller/ActivityController.php +++ b/src/Controller/ActivityController.php @@ -133,7 +133,7 @@ final class ActivityController extends AbstractController $rates = $rateRepository->getRatesForActivity($activity); } - if ($this->isGranted('budget', $activity)) { + if ($this->isGranted('budget', $activity) || $this->isGranted('time', $activity)) { $stats = $statisticService->getBudgetStatisticModel($activity, $now); } @@ -424,7 +424,8 @@ final class ActivityController extends AbstractController 'action' => $url, 'method' => 'POST', 'currency' => $currency, - 'include_budget' => $this->isGranted('budget', $activity) + 'include_budget' => $this->isGranted('budget', $activity), + 'include_time' => $this->isGranted('time', $activity), ]); } } diff --git a/src/Controller/CustomerController.php b/src/Controller/CustomerController.php index 4f33f83a..736caca5 100644 --- a/src/Controller/CustomerController.php +++ b/src/Controller/CustomerController.php @@ -315,7 +315,7 @@ final class CustomerController extends AbstractController $timezone = new \DateTimeZone($customer->getTimezone()); } - if ($this->isGranted('budget', $customer)) { + if ($this->isGranted('budget', $customer) || $this->isGranted('time', $customer)) { $stats = $statisticService->getBudgetStatisticModel($customer, $now); } @@ -530,7 +530,8 @@ final class CustomerController extends AbstractController return $this->createForm(CustomerEditForm::class, $customer, [ 'action' => $url, 'method' => 'POST', - 'include_budget' => $this->isGranted('budget', $customer) + 'include_budget' => $this->isGranted('budget', $customer), + 'include_time' => $this->isGranted('time', $customer), ]); } } diff --git a/src/Controller/ProjectController.php b/src/Controller/ProjectController.php index 3902926c..b1918e3c 100644 --- a/src/Controller/ProjectController.php +++ b/src/Controller/ProjectController.php @@ -333,7 +333,7 @@ final class ProjectController extends AbstractController $rates = $rateRepository->getRatesForProject($project); } - if ($this->isGranted('budget', $project)) { + if ($this->isGranted('budget', $project) || $this->isGranted('time', $project)) { $stats = $statisticService->getBudgetStatisticModel($project, $now); } @@ -563,6 +563,7 @@ final class ProjectController extends AbstractController 'currency' => $currency, 'timezone' => $this->getDateTimeFactory()->getTimezone()->getName(), 'include_budget' => $this->isGranted('budget', $project), + 'include_time' => $this->isGranted('time', $project), 'time_increment' => 15, ]); } diff --git a/src/Controller/Reporting/ProjectDateRangeController.php b/src/Controller/Reporting/ProjectDateRangeController.php index b0802f32..57d52a8c 100644 --- a/src/Controller/Reporting/ProjectDateRangeController.php +++ b/src/Controller/Reporting/ProjectDateRangeController.php @@ -22,7 +22,7 @@ final class ProjectDateRangeController extends AbstractController { /** * @Route(path="/reporting/project_daterange", name="report_project_daterange", methods={"GET","POST"}) - * @Security("is_granted('view_reporting') and is_granted('budget_project')") + * @Security("is_granted('view_reporting') and is_granted('budget_any', 'project')") */ public function __invoke(Request $request, ProjectStatisticService $service) { diff --git a/src/Controller/Reporting/ProjectDetailsController.php b/src/Controller/Reporting/ProjectDetailsController.php index c0438fa2..9085a22d 100644 --- a/src/Controller/Reporting/ProjectDetailsController.php +++ b/src/Controller/Reporting/ProjectDetailsController.php @@ -22,7 +22,7 @@ final class ProjectDetailsController extends AbstractController { /** * @Route(path="/reporting/project_details", name="report_project_details", methods={"GET"}) - * @Security("is_granted('view_reporting') and (is_granted('details_project') or is_granted('details_teamlead_project') or is_granted('details_team_project'))") + * @Security("is_granted('view_reporting') and is_granted('details', 'project')") */ public function __invoke(Request $request, ProjectStatisticService $service) { diff --git a/src/Controller/Reporting/ProjectInactiveController.php b/src/Controller/Reporting/ProjectInactiveController.php index fe6451c6..fd6d82ae 100644 --- a/src/Controller/Reporting/ProjectInactiveController.php +++ b/src/Controller/Reporting/ProjectInactiveController.php @@ -21,7 +21,7 @@ final class ProjectInactiveController extends AbstractController { /** * @Route(path="/reporting/project_inactive", name="report_project_inactive", methods={"GET","POST"}) - * @Security("is_granted('view_reporting') and is_granted('budget_project')") + * @Security("is_granted('view_reporting') and is_granted('budget_any', 'project')") */ public function __invoke(Request $request, ProjectStatisticService $service) { diff --git a/src/Controller/Reporting/ProjectViewController.php b/src/Controller/Reporting/ProjectViewController.php index 4a0f36de..f1c9b4a9 100644 --- a/src/Controller/Reporting/ProjectViewController.php +++ b/src/Controller/Reporting/ProjectViewController.php @@ -21,7 +21,7 @@ final class ProjectViewController extends AbstractController { /** * @Route(path="/reporting/project_view", name="report_project_view", methods={"GET","POST"}) - * @Security("is_granted('view_reporting') and is_granted('budget_project')") + * @Security("is_granted('view_reporting') and is_granted('budget_any', 'project')") */ public function __invoke(Request $request, ProjectStatisticService $service) { diff --git a/src/EventSubscriber/Actions/CustomerSubscriber.php b/src/EventSubscriber/Actions/CustomerSubscriber.php index fd892033..1b911798 100644 --- a/src/EventSubscriber/Actions/CustomerSubscriber.php +++ b/src/EventSubscriber/Actions/CustomerSubscriber.php @@ -78,7 +78,7 @@ class CustomerSubscriber extends AbstractActionsSubscriber $event->addDelete($this->path('admin_customer_delete', ['id' => $customer->getId()])); } - if ($this->isGranted('view_reporting') && $this->isGranted('budget_project')) { + if ($this->isGranted('view_reporting') && $this->isGranted('budget_any', 'project')) { $event->addAction('report_project_view', ['url' => $this->path('report_project_view', ['customer' => $customer->getId()]), 'icon' => 'reporting', 'translation_domain' => 'reporting']); } } diff --git a/src/Form/ActivityEditForm.php b/src/Form/ActivityEditForm.php index d4d16bf4..7a0aec6a 100644 --- a/src/Form/ActivityEditForm.php +++ b/src/Form/ActivityEditForm.php @@ -97,6 +97,7 @@ class ActivityEditForm extends AbstractType 'customer' => false, 'currency' => Customer::DEFAULT_CURRENCY, 'include_budget' => false, + 'include_time' => false, 'attr' => [ 'data-form-event' => 'kimai.activityUpdate' ], diff --git a/src/Form/CustomerEditForm.php b/src/Form/CustomerEditForm.php index 3cb57bdb..e25cb97a 100644 --- a/src/Form/CustomerEditForm.php +++ b/src/Form/CustomerEditForm.php @@ -114,6 +114,7 @@ class CustomerEditForm extends AbstractType 'csrf_token_id' => 'admin_customer_edit', 'currency' => Customer::DEFAULT_CURRENCY, 'include_budget' => false, + 'include_time' => false, 'attr' => [ 'data-form-event' => 'kimai.customerUpdate' ], diff --git a/src/Form/EntityFormTrait.php b/src/Form/EntityFormTrait.php index e7705381..8b3331e1 100644 --- a/src/Form/EntityFormTrait.php +++ b/src/Form/EntityFormTrait.php @@ -26,22 +26,30 @@ trait EntityFormTrait { $this->addColor($builder); - if ($options['include_budget']) { - $builder - ->add('budget', MoneyType::class, [ - 'empty_data' => '0.00', - 'label' => 'label.budget', - 'required' => false, - 'currency' => $options['currency'], - ]) - ->add('timeBudget', DurationType::class, [ - 'empty_data' => 0, - 'label' => 'label.timeBudget', - 'icon' => 'clock', - 'required' => false, - ]) - ->add('budgetType', BudgetType::class) - ; + $showMoney = $options['include_budget']; + $showTime = $options['include_time']; + $showBudget = $showMoney || $showTime; + + if ($showMoney) { + $builder->add('budget', MoneyType::class, [ + 'empty_data' => '0.00', + 'label' => 'label.budget', + 'required' => false, + 'currency' => $options['currency'], + ]); + } + + if ($showTime) { + $builder->add('timeBudget', DurationType::class, [ + 'empty_data' => 0, + 'label' => 'label.timeBudget', + 'icon' => 'clock', + 'required' => false, + ]); + } + + if ($showBudget) { + $builder->add('budgetType', BudgetType::class); } $builder->add('metaFields', MetaFieldsCollectionType::class); diff --git a/src/Form/ProjectEditForm.php b/src/Form/ProjectEditForm.php index 602c2109..d12eae95 100644 --- a/src/Form/ProjectEditForm.php +++ b/src/Form/ProjectEditForm.php @@ -112,6 +112,7 @@ class ProjectEditForm extends AbstractType 'currency' => Customer::DEFAULT_CURRENCY, 'date_format' => null, 'include_budget' => false, + 'include_time' => false, 'timezone' => date_default_timezone_get(), 'time_increment' => 1, 'attr' => [ diff --git a/src/Reporting/ReportingService.php b/src/Reporting/ReportingService.php index be8dcbaa..97f4a15d 100644 --- a/src/Reporting/ReportingService.php +++ b/src/Reporting/ReportingService.php @@ -42,25 +42,28 @@ final class ReportingService $event = new ReportingEvent($user); if ($this->security->isGranted('view_reporting')) { + $showBudget = $this->security->isGranted('budget_any', 'project'); + $details = $this->security->isGranted('details', 'project'); + $viewOther = $this->security->isGranted('view_other_reporting') && $this->security->isGranted('view_other_timesheet'); $event->addReport(new Report('week_by_user', 'report_user_week', 'report_user_week', 'user')); $event->addReport(new Report('month_by_user', 'report_user_month', 'report_user_month', 'user')); $event->addReport(new Report('year_by_user', 'report_user_year', 'report_user_year', 'user')); - if ($this->security->isGranted('view_other_reporting') && $this->security->isGranted('view_other_timesheet')) { + if ($viewOther) { $event->addReport(new Report('weekly_users_list', 'report_weekly_users', 'report_weekly_users', 'users')); $event->addReport(new Report('monthly_users_list', 'report_monthly_users', 'report_monthly_users', 'users')); $event->addReport(new Report('yearly_users_list', 'report_yearly_users', 'report_yearly_users', 'users')); } - if ($this->security->isGranted('budget_project')) { + if ($showBudget) { $event->addReport(new Report('project_view', 'report_project_view', 'report_project_view', 'project')); } - if ($this->security->isGranted('details_project') || $this->security->isGranted('details_teamlead_project') || $this->security->isGranted('details_team_project')) { + if ($details) { $event->addReport(new Report('project_details', 'report_project_details', 'report_project_details', 'project')); } - if ($this->security->isGranted('budget_project')) { + if ($showBudget) { $event->addReport(new Report('daterange_projects', 'report_project_daterange', 'report_project_daterange', 'project')); $event->addReport(new Report('inactive_projects', 'report_project_inactive', 'report_inactive_project', 'project')); } - if ($this->security->isGranted('view_other_reporting') && $this->security->isGranted('view_other_timesheet')) { + if ($viewOther) { $event->addReport(new Report('report_customer_monthly_projects', 'report_customer_monthly_projects', 'report_customer_monthly_projects', 'customer')); } diff --git a/src/Validator/Constraints/TimesheetBudgetUsed.php b/src/Validator/Constraints/TimesheetBudgetUsed.php index 8ff7bc96..2dcdebb2 100644 --- a/src/Validator/Constraints/TimesheetBudgetUsed.php +++ b/src/Validator/Constraints/TimesheetBudgetUsed.php @@ -14,4 +14,5 @@ final class TimesheetBudgetUsed extends TimesheetConstraint // same messages, so we can re-use the validation translation! public $messageRate = 'The budget is completely used.'; public $messageTime = 'The budget is completely used.'; + public $messagePermission = 'Sorry, the budget is used up.'; } diff --git a/src/Validator/Constraints/TimesheetBudgetUsedValidator.php b/src/Validator/Constraints/TimesheetBudgetUsedValidator.php index 8a7e7d3b..f94bec0b 100644 --- a/src/Validator/Constraints/TimesheetBudgetUsedValidator.php +++ b/src/Validator/Constraints/TimesheetBudgetUsedValidator.php @@ -20,6 +20,7 @@ use App\Timesheet\RateServiceInterface; use App\Utils\Duration; use App\Utils\LocaleHelper; use DateTime; +use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; @@ -32,8 +33,9 @@ final class TimesheetBudgetUsedValidator extends ConstraintValidator private $timesheetRepository; private $rateService; private $configuration; + private $security; - public function __construct(SystemConfiguration $configuration, CustomerStatisticService $customerStatisticService, ProjectStatisticService $projectStatisticService, ActivityStatisticService $activityStatisticService, TimesheetRepository $timesheetRepository, RateServiceInterface $rateService) + public function __construct(SystemConfiguration $configuration, CustomerStatisticService $customerStatisticService, ProjectStatisticService $projectStatisticService, ActivityStatisticService $activityStatisticService, TimesheetRepository $timesheetRepository, RateServiceInterface $rateService, AuthorizationCheckerInterface $security) { $this->configuration = $configuration; $this->customerStatisticService = $customerStatisticService; @@ -41,6 +43,7 @@ final class TimesheetBudgetUsedValidator extends ConstraintValidator $this->activityStatisticService = $activityStatisticService; $this->timesheetRepository = $timesheetRepository; $this->rateService = $rateService; + $this->security = $security; } /** @@ -181,7 +184,12 @@ final class TimesheetBudgetUsedValidator extends ConstraintValidator $free = $budget - $rate; $free = max($free, 0); - $this->context->buildViolation($constraint->messageRate) + $message = $constraint->messageRate; + if (!$this->security->isGranted('budget_money', $field)) { + $message = $constraint->messagePermission; + } + + $this->context->buildViolation($message) ->atPath($field) ->setTranslationDomain('validators') ->setParameters([ @@ -200,7 +208,12 @@ final class TimesheetBudgetUsedValidator extends ConstraintValidator $free = $budget - $duration; $free = max($free, 0); - $this->context->buildViolation($constraint->messageTime) + $message = $constraint->messageTime; + if (!$this->security->isGranted('budget_time', $field)) { + $message = $constraint->messagePermission; + } + + $this->context->buildViolation($message) ->atPath($field) ->setTranslationDomain('validators') ->setParameters([ diff --git a/src/Voter/ActivityVoter.php b/src/Voter/ActivityVoter.php index 57bd2cbe..5cc1f946 100644 --- a/src/Voter/ActivityVoter.php +++ b/src/Voter/ActivityVoter.php @@ -28,6 +28,7 @@ final class ActivityVoter extends Voter 'view', 'edit', 'budget', + 'time', 'delete', 'permissions', ]; diff --git a/src/Voter/CustomerVoter.php b/src/Voter/CustomerVoter.php index dedcd832..a6f5072d 100644 --- a/src/Voter/CustomerVoter.php +++ b/src/Voter/CustomerVoter.php @@ -29,6 +29,7 @@ final class CustomerVoter extends Voter 'create', 'edit', 'budget', + 'time', 'delete', 'permissions', 'comments', diff --git a/src/Voter/EntityMultiRoleVoter.php b/src/Voter/EntityMultiRoleVoter.php new file mode 100644 index 00000000..77cbed14 --- /dev/null +++ b/src/Voter/EntityMultiRoleVoter.php @@ -0,0 +1,121 @@ +permissionManager = $permissionManager; + } + + /** + * @param string $attribute + * @param Activity|Project|Customer|string $subject + * @return bool + */ + protected function supports($attribute, $subject) + { + if (!\in_array($attribute, self::ALLOWED_ATTRIBUTES)) { + return false; + } + + if (\is_string($subject) && \in_array($subject, self::ALLOWED_SUBJECTS)) { + return true; + } + + if ($subject instanceof Activity || $subject instanceof Project || $subject instanceof Customer) { + return true; + } + + return false; + } + + /** + * @param string $attribute + * @param Activity|Project|Customer|string $subject + * @param TokenInterface $token + * @return bool + */ + protected function voteOnAttribute($attribute, $subject, TokenInterface $token) + { + $user = $token->getUser(); + + if (!$user instanceof User) { + return false; + } + + $suffix = null; + + if (\is_string($subject) && \in_array($subject, self::ALLOWED_SUBJECTS)) { + $suffix = $subject; + } elseif ($subject instanceof Activity) { + $suffix = 'activity'; + } elseif ($subject instanceof Project) { + $suffix = 'project'; + } elseif ($subject instanceof Customer) { + $suffix = 'customer'; + } + + if ($suffix === null) { + return false; + } + + $permissions = []; + + if ($attribute === 'details') { + $permissions[] = 'details'; + } + + if ($attribute === 'budget_money' || $attribute === 'budget_any') { + $permissions[] = 'budget'; + $permissions[] = 'budget_teamlead'; + $permissions[] = 'budget_team'; + } + + if ($attribute === 'budget_time' || $attribute === 'budget_any') { + $permissions[] = 'time'; + $permissions[] = 'time_teamlead'; + $permissions[] = 'time_team'; + } + foreach ($permissions as $permission) { + if ($this->permissionManager->hasRolePermission($user, $permission . '_' . $suffix)) { + return true; + } + } + + return false; + } +} diff --git a/src/Voter/ProjectVoter.php b/src/Voter/ProjectVoter.php index 66a24ef0..40902827 100644 --- a/src/Voter/ProjectVoter.php +++ b/src/Voter/ProjectVoter.php @@ -28,6 +28,7 @@ final class ProjectVoter extends Voter 'view', 'edit', 'budget', + 'time', 'delete', 'permissions', 'comments', diff --git a/src/Widget/Type/UserTeamProjects.php b/src/Widget/Type/UserTeamProjects.php index 510b3cb4..1fd27ede 100644 --- a/src/Widget/Type/UserTeamProjects.php +++ b/src/Widget/Type/UserTeamProjects.php @@ -83,7 +83,10 @@ class UserTeamProjects extends SimpleWidget implements AuthorizedWidget, UserWid */ public function getPermissions(): array { - return ['budget_team_project', 'budget_teamlead_project', 'budget_project']; + return [ + 'budget_team_project', 'budget_teamlead_project', 'budget_project', + 'time_team_project', 'time_teamlead_project', 'time_project', + ]; } public function setUser(User $user): void diff --git a/templates/activity/edit.html.twig b/templates/activity/edit.html.twig index f15bb27d..5cb55124 100644 --- a/templates/activity/edit.html.twig +++ b/templates/activity/edit.html.twig @@ -28,18 +28,22 @@ {% if form.project is defined %} {{ form_row(form.project) }} {% endif %} - {% if form.budget is defined %} -
-
- {{ form_row(form.budget) }} + {% if form.budgetType is defined %} +
+ {% if form.budget is defined %} +
+ {{ form_row(form.budget) }} +
+ {% endif %} + {% if form.timeBudget is defined %} +
+ {{ form_row(form.timeBudget) }} +
+ {% endif %} +
+ {{ form_row(form.budgetType) }} +
-
- {{ form_row(form.timeBudget) }} -
-
- {{ form_row(form.budgetType) }} -
-
{% endif %} {% if form.invoiceText is defined %} {{ form_row(form.invoiceText) }} diff --git a/templates/activity/index.html.twig b/templates/activity/index.html.twig index dcae1c37..09774397 100644 --- a/templates/activity/index.html.twig +++ b/templates/activity/index.html.twig @@ -14,9 +14,17 @@ ('mf_' ~ field.name): {'title': field.label|trans, 'class': 'hidden-xs hidden-sm mf_' ~ field.name, 'orderBy': false} }) %} {% endfor %} +{% if is_granted('budget_money', 'activity') %} + {% set columns = columns|merge({ + 'budget': {'class': 'hidden-xs hidden-sm hidden text-right w-min', 'title': 'label.budget'|trans}, + }) %} +{% endif %} +{% if is_granted('budget_time', 'activity') %} + {% set columns = columns|merge({ + 'timeBudget': {'class': 'hidden-xs hidden-sm hidden text-right w-min', 'title': 'label.timeBudget'|trans}, + }) %} +{% endif %} {% set columns = columns|merge({ - 'budget': {'class': 'hidden-xs hidden-sm hidden text-right w-min', 'title': 'label.budget'|trans}, - 'timeBudget': {'class': 'hidden-xs hidden-sm hidden text-right w-min', 'title': 'label.timeBudget'|trans}, 'team': {'class': 'text-center w-min', 'orderBy': false}, 'visible': {'class': 'text-center hidden w-min'}, 'actions': {'class': 'actions alwaysVisible'}, @@ -63,14 +71,14 @@ {% endfor %} - {% if entry.hasBudget() %} + {% if entry.hasBudget() and is_granted('budget', entry) %} {{ entry.budget|money((entry.project is null ? defaultCurrency : entry.project.customer.currency)) }} {% else %} – {% endif %} - {% if entry.hasTimeBudget() %} + {% if entry.hasTimeBudget() and is_granted('time', entry) %} {{ entry.timeBudget|duration }} {% else %} – diff --git a/templates/customer/edit.html.twig b/templates/customer/edit.html.twig index 3ff3ea68..1befc1ba 100644 --- a/templates/customer/edit.html.twig +++ b/templates/customer/edit.html.twig @@ -67,14 +67,18 @@ {{ form_row(form.fax) }}
- {% if form.budget is defined %} + {% if form.budgetType is defined %}
-
- {{ form_row(form.budget) }} -
-
- {{ form_row(form.timeBudget) }} -
+ {% if form.budget is defined %} +
+ {{ form_row(form.budget) }} +
+ {% endif %} + {% if form.timeBudget is defined %} +
+ {{ form_row(form.timeBudget) }} +
+ {% endif %}
{{ form_row(form.budgetType) }}
diff --git a/templates/customer/embed_projects.html.twig b/templates/customer/embed_projects.html.twig index 8e975558..e532a846 100644 --- a/templates/customer/embed_projects.html.twig +++ b/templates/customer/embed_projects.html.twig @@ -6,7 +6,7 @@ id="project_list_box" data-href="{{ path('customer_projects', {'id': customer.id}) }}" data-reload="kimai.projectUpdate kimai.projectDelete" {% endblock %} {% block box_tools %} - {% if is_granted('view_reporting') and is_granted('budget_project') %} + {% if is_granted('view_reporting') and is_granted('budget_any', 'project') %} {% endif %} {% if customer.visible and is_granted('create_project') %} diff --git a/templates/customer/index.html.twig b/templates/customer/index.html.twig index 29233b08..82bcd0ff 100644 --- a/templates/customer/index.html.twig +++ b/templates/customer/index.html.twig @@ -25,9 +25,17 @@ ('mf_' ~ field.name): {'title': field.label|trans, 'class': 'hidden-xs hidden-sm mf_' ~ field.name, 'orderBy': false} }) %} {% endfor %} +{% if is_granted('budget_money', 'customer') %} + {% set columns = columns|merge({ + 'budget': {'class': 'hidden-xs hidden-sm hidden text-right w-min', 'title': 'label.budget'|trans}, + }) %} +{% endif %} +{% if is_granted('budget_time', 'customer') %} + {% set columns = columns|merge({ + 'timeBudget': {'class': 'hidden-xs hidden-sm hidden text-right w-min', 'title': 'label.timeBudget'|trans}, + }) %} +{% endif %} {% set columns = columns|merge({ - 'budget': {'class': 'hidden-xs hidden-sm hidden text-right w-min'}, - 'timeBudget': {'class': 'hidden-xs hidden-sm hidden text-right w-min'}, 'team': {'class': 'text-center w-min', 'orderBy': false}, 'visible': {'class': 'text-center hidden w-min'}, 'actions': {'class': 'actions alwaysVisible'}, @@ -74,14 +82,14 @@ {% endfor %} - {% if entry.hasBudget() %} + {% if entry.hasBudget() and is_granted('budget', entry) %} {{ entry.budget|money(entry.currency) }} {% else %} – {% endif %} - {% if entry.hasTimeBudget() %} + {% if entry.hasTimeBudget() and is_granted('time', entry) %} {{ entry.timeBudget|duration }} {% else %} – diff --git a/templates/embeds/budgets.html.twig b/templates/embeds/budgets.html.twig index 41491db8..0dfbab8c 100644 --- a/templates/embeds/budgets.html.twig +++ b/templates/embeds/budgets.html.twig @@ -1,154 +1,158 @@ -{% embed '@AdminLTE/Widgets/box-widget.html.twig' %} - {% import "macros/progressbar.html.twig" as progress %} - {% import "macros/widgets.html.twig" as widgets %} - {% block box_title %} - {{ 'label.timeBudget'|trans }} {% if entity.isMonthlyBudget() %}({{ 'label.budgetType_month'|trans }}){% endif %} - {% endblock %} - {% block box_body_class %}no-padding{% endblock %} - {% block box_attributes %}id="time_budget_box"{% endblock %} - {% block box_body %} - {% set durationTrans = entity.isMonthlyBudget() ? 'stats.durationMonth' : 'stats.durationTotal' %} -
-
- - - - - - - {% set totalPercentReached = 100 %} - {% if entity.timeBudget > 0 %} - {% set totalPercentReached = (stats.durationBillable / (entity.timeBudget / 100)) %} - {% endif %} - - - - - - {% set percentReached = 0 %} - {% if stats.duration > 0 %} - {% set percentReached = (stats.durationBillable / (stats.duration / 100)) %} - {% endif %} - - - - - - {% if entity.timeBudget > 0 %} - - - - {% endif %} -
{{ 'label.timeBudget'|trans }} - {% if entity.timeBudget > 0 %} - {{ entity.timeBudget|duration }} - {% else %} - - - {% endif %} - - {% if entity.timeBudget > 0 %} - 100% - {% else %} - - - {% endif %} -
{{ durationTrans|trans }}{{ stats.duration|duration }}{{ totalPercentReached|number_format(2) }}%
{{ 'label.billable'|trans }}{{ stats.durationBillable|duration }}{{ percentReached|number_format(2) }}%
- {% if totalPercentReached < 100 %} - {{ 'stats.percentUsedLeft'|trans({'%percent%': totalPercentReached|number_format(2), '%left%': (entity.timeBudget - stats.durationBillable)|duration}) }} - {% else %} - {{ 'stats.percentUsed'|trans({'%percent%': totalPercentReached|number_format(2)}) }} - {% endif %} -
-
-
-
- {% if entity.timeBudget > 0 %} - {{ progress.progressbar(entity.timeBudget, stats.durationBillable, durationTrans|trans, stats.durationBillable|duration ~ ' / ' ~ entity.timeBudget|duration) }} - {% endif %} - {{ progress.progressbar(stats.duration, stats.durationBillable, 'label.billable'|trans, stats.durationBillable|duration ~ ' / ' ~ stats.duration|duration, true) }} -
-
-
- {% endblock %} -{% endembed %} -{% embed '@AdminLTE/Widgets/box-widget.html.twig' %} - {% import "macros/progressbar.html.twig" as progress %} - {% import "macros/widgets.html.twig" as widgets %} - {% block box_title %} - {{ 'label.budget'|trans }} {% if entity.isMonthlyBudget() %}({{ 'label.budgetType_month'|trans }}){% endif %} - {% endblock %} - {% block box_body_class %}no-padding{% endblock %} - {% block box_attributes %}id="budget_box"{% endblock %} - {% block box_body %} - {% set rateTrans = entity.isMonthlyBudget() ? 'stats.amountMonth' : 'stats.amountTotal' %} -
-
- - - - -
{{ 'label.budget'|trans }} - {% if entity.budget > 0 %} - {{ entity.budget|money(currency) }} - {% else %} - - - {% endif %} - - {% if entity.budget > 0 %} +{% if is_granted('time', entity) %} + {% embed '@AdminLTE/Widgets/box-widget.html.twig' %} + {% import "macros/progressbar.html.twig" as progress %} + {% import "macros/widgets.html.twig" as widgets %} + {% block box_title %} + {{ 'label.timeBudget'|trans }} {% if entity.isMonthlyBudget() %}({{ 'label.budgetType_month'|trans }}){% endif %} + {% endblock %} + {% block box_body_class %}no-padding{% endblock %} + {% block box_attributes %}id="time_budget_box"{% endblock %} + {% block box_body %} + {% set durationTrans = entity.isMonthlyBudget() ? 'stats.durationMonth' : 'stats.durationTotal' %} +
+
+ + + + + - - {% set totalPercentReached = 100 %} - {% if entity.budget > 0 %} - {% set totalPercentReached = (stats.rateBillable / (entity.budget / 100)) %} - {% endif %} - - - - - - {% set percentReached = 0 %} - {% if stats.rate > 0 %} - {% set percentReached = (stats.rateBillable / (stats.rate / 100)) %} - {% endif %} - - - - - - {% set percentReached = 0 %} - {% if stats.rateBillable > 0 %} - {% set percentReached = (stats.internalRate / (stats.rateBillable / 100)) %} - {% endif %} - - - - - - {% if entity.budget > 0 %} + {% else %} + - + {% endif %} + + + {% set totalPercentReached = 100 %} + {% if entity.timeBudget > 0 %} + {% set totalPercentReached = (stats.durationBillable / (entity.timeBudget / 100)) %} + {% endif %} + + + + + + {% set percentReached = 0 %} + {% if stats.duration > 0 %} + {% set percentReached = (stats.durationBillable / (stats.duration / 100)) %} + {% endif %} + + + + + + {% if entity.timeBudget > 0 %} - {% endif %} -
{{ 'label.timeBudget'|trans }} + {% if entity.timeBudget > 0 %} + {{ entity.timeBudget|duration }} + {% else %} + - + {% endif %} + + {% if entity.timeBudget > 0 %} 100% - {% else %} - - - {% endif %} -
{{ rateTrans|trans }}{{ stats.rate|money(currency) }}{{ totalPercentReached|number_format(2) }}%
{{ 'label.billable'|trans }}{{ stats.rateBillable|money(currency) }}{{ percentReached|number_format(2) }}%
{{ 'label.rate_internal'|trans }}{{ stats.internalRate|money(currency) }}{{ percentReached|number_format(2) }}%
{{ durationTrans|trans }}{{ stats.duration|duration }}{{ totalPercentReached|number_format(2) }}%
{{ 'label.billable'|trans }}{{ stats.durationBillable|duration }}{{ percentReached|number_format(2) }}%
{% if totalPercentReached < 100 %} - {{ 'stats.percentUsedLeft'|trans({'%percent%': totalPercentReached|number_format(2), '%left%': (entity.budget - stats.rateBillable)|money(currency)}) }} + {{ 'stats.percentUsedLeft'|trans({'%percent%': totalPercentReached|number_format(2), '%left%': (entity.timeBudget - stats.durationBillable)|duration}) }} {% else %} {{ 'stats.percentUsed'|trans({'%percent%': totalPercentReached|number_format(2)}) }} {% endif %}
-
-
-
- {% if entity.budget > 0 %} - {{ progress.progressbar(entity.budget, stats.rateBillable, rateTrans|trans, stats.rateBillable|money(currency) ~ ' / ' ~ entity.budget|money(currency)) }} - {% endif %} - {{ progress.progressbar(stats.rate, stats.rateBillable, 'label.billable'|trans, stats.rateBillable|money(currency) ~ ' / ' ~ stats.rate|money(currency), true) }} - {{ progress.progressbar(stats.rateBillable, stats.internalRate, 'label.rate_internal'|trans, stats.internalRate|money(currency) ~ ' / ' ~ stats.rateBillable|money(currency)) }} + {% endif %} +
+
+
+
+ {% if entity.timeBudget > 0 %} + {{ progress.progressbar(entity.timeBudget, stats.durationBillable, durationTrans|trans, stats.durationBillable|duration ~ ' / ' ~ entity.timeBudget|duration) }} + {% endif %} + {{ progress.progressbar(stats.duration, stats.durationBillable, 'label.billable'|trans, stats.durationBillable|duration ~ ' / ' ~ stats.duration|duration, true) }} +
-
- {% endblock %} -{% endembed %} + {% endblock %} + {% endembed %} +{% endif %} +{% if is_granted('budget', entity) %} + {% embed '@AdminLTE/Widgets/box-widget.html.twig' %} + {% import "macros/progressbar.html.twig" as progress %} + {% import "macros/widgets.html.twig" as widgets %} + {% block box_title %} + {{ 'label.budget'|trans }} {% if entity.isMonthlyBudget() %}({{ 'label.budgetType_month'|trans }}){% endif %} + {% endblock %} + {% block box_body_class %}no-padding{% endblock %} + {% block box_attributes %}id="budget_box"{% endblock %} + {% block box_body %} + {% set rateTrans = entity.isMonthlyBudget() ? 'stats.amountMonth' : 'stats.amountTotal' %} +
+
+ + + + + + + {% set totalPercentReached = 100 %} + {% if entity.budget > 0 %} + {% set totalPercentReached = (stats.rateBillable / (entity.budget / 100)) %} + {% endif %} + + + + + + {% set percentReached = 0 %} + {% if stats.rate > 0 %} + {% set percentReached = (stats.rateBillable / (stats.rate / 100)) %} + {% endif %} + + + + + + {% set percentReached = 0 %} + {% if stats.rateBillable > 0 %} + {% set percentReached = (stats.internalRate / (stats.rateBillable / 100)) %} + {% endif %} + + + + + + {% if entity.budget > 0 %} + + + + {% endif %} +
{{ 'label.budget'|trans }} + {% if entity.budget > 0 %} + {{ entity.budget|money(currency) }} + {% else %} + - + {% endif %} + + {% if entity.budget > 0 %} + 100% + {% else %} + - + {% endif %} +
{{ rateTrans|trans }}{{ stats.rate|money(currency) }}{{ totalPercentReached|number_format(2) }}%
{{ 'label.billable'|trans }}{{ stats.rateBillable|money(currency) }}{{ percentReached|number_format(2) }}%
{{ 'label.rate_internal'|trans }}{{ stats.internalRate|money(currency) }}{{ percentReached|number_format(2) }}%
+ {% if totalPercentReached < 100 %} + {{ 'stats.percentUsedLeft'|trans({'%percent%': totalPercentReached|number_format(2), '%left%': (entity.budget - stats.rateBillable)|money(currency)}) }} + {% else %} + {{ 'stats.percentUsed'|trans({'%percent%': totalPercentReached|number_format(2)}) }} + {% endif %} +
+
+
+
+ {% if entity.budget > 0 %} + {{ progress.progressbar(entity.budget, stats.rateBillable, rateTrans|trans, stats.rateBillable|money(currency) ~ ' / ' ~ entity.budget|money(currency)) }} + {% endif %} + {{ progress.progressbar(stats.rate, stats.rateBillable, 'label.billable'|trans, stats.rateBillable|money(currency) ~ ' / ' ~ stats.rate|money(currency), true) }} + {{ progress.progressbar(stats.rateBillable, stats.internalRate, 'label.rate_internal'|trans, stats.internalRate|money(currency) ~ ' / ' ~ stats.rateBillable|money(currency)) }} +
+
+
+ {% endblock %} + {% endembed %} +{% endif %} diff --git a/templates/project/details.html.twig b/templates/project/details.html.twig index 3ec6543e..6c4d9fe4 100644 --- a/templates/project/details.html.twig +++ b/templates/project/details.html.twig @@ -90,8 +90,7 @@ {% endif %} {% endif %} - {% if is_granted('budget', project) %} - {% if project.hasBudget() %} + {% if project.hasBudget() and is_granted('budget', project) %} {{ 'label.budget'|trans }} @@ -103,8 +102,8 @@ {{ project.getBudget()|money(project.customer.currency) }} - {% endif %} - {% if project.hasTimeBudget() %} + {% endif %} + {% if project.hasTimeBudget() and is_granted('time', project) %} {{ 'label.timeBudget'|trans }} @@ -116,7 +115,6 @@ {{ project.getTimeBudget()|duration }} - {% endif %} {% endif %} {% for metaField in project.visibleMetaFields|sort((a, b) => a.order <=> b.order) %} diff --git a/templates/project/edit.html.twig b/templates/project/edit.html.twig index 6a0d9af4..1210ab50 100644 --- a/templates/project/edit.html.twig +++ b/templates/project/edit.html.twig @@ -39,14 +39,18 @@ {{ form_row(form.end) }} - {% if form.budget is defined %} + {% if form.budgetType is defined %}
+ {% if form.budget is defined %}
{{ form_row(form.budget) }}
+ {% endif %} + {% if form.timeBudget is defined %}
{{ form_row(form.timeBudget) }}
+ {% endif %}
{{ form_row(form.budgetType) }}
diff --git a/templates/project/index.html.twig b/templates/project/index.html.twig index 28c468f4..e7eb56e5 100644 --- a/templates/project/index.html.twig +++ b/templates/project/index.html.twig @@ -18,9 +18,17 @@ ('mf_' ~ field.name): {'title': field.label|trans, 'class': 'hidden-xs hidden-sm mf_' ~ field.name, 'orderBy': false} }) %} {% endfor %} +{% if is_granted('budget_money', 'project') %} + {% set columns = columns|merge({ + 'budget': {'class': 'hidden-xs hidden-sm hidden text-right w-min', 'title': 'label.budget'|trans}, + }) %} +{% endif %} +{% if is_granted('budget_time', 'project') %} + {% set columns = columns|merge({ + 'timeBudget': {'class': 'hidden-xs hidden-sm hidden text-right w-min', 'title': 'label.timeBudget'|trans}, + }) %} +{% endif %} {% set columns = columns|merge({ - 'budget': {'class': 'hidden-xs hidden-sm hidden text-right w-min', 'title': 'label.budget'|trans}, - 'timeBudget': {'class': 'hidden-xs hidden-sm hidden text-right w-min', 'title': 'label.timeBudget'|trans}, 'team': {'class': 'text-center w-min', 'orderBy': false}, 'visible': {'class': 'text-center hidden w-min'}, 'actions': {'class': 'actions alwaysVisible'}, @@ -60,14 +68,14 @@ {% endfor %} - {% if entry.hasBudget() %} + {% if entry.hasBudget() and is_granted('budget', entry) %} {{ entry.budget|money(entry.customer.currency) }} {% else %} – {% endif %} - {% if entry.hasTimeBudget() %} + {% if entry.hasTimeBudget() and is_granted('time', entry) %} {{ entry.timeBudget|duration }} {% else %} – diff --git a/templates/reporting/project_daterange.html.twig b/templates/reporting/project_daterange.html.twig index a3718fe5..7e43fda5 100644 --- a/templates/reporting/project_daterange.html.twig +++ b/templates/reporting/project_daterange.html.twig @@ -3,15 +3,28 @@ {% block report_title %}{{ 'report_project_daterange'|trans({}, 'reporting') }}{% endblock %} +{% set showMoneyBudget = is_granted('budget_money', 'project') %} +{% set showTimeBudget = is_granted('budget_time', 'project') %} + {% set columns = { 'name': {'class': 'alwaysVisible'}, - 'timeBudget': {'class': 'hidden-xs', 'title': 'label.timeBudget'|trans}, - 'budget': {'class': 'hidden-xs', 'title': 'label.budget'|trans}, +} %} +{% if showTimeBudget %} + {% set columns = columns|merge({ + 'timeBudget': {'class': 'hidden-xs', 'title': 'label.timeBudget'|trans}, + }) %} +{% endif %} +{% if showMoneyBudget %} + {% set columns = columns|merge({ + 'budget': {'class': 'hidden-xs', 'title': 'label.budget'|trans}, + }) %} +{% endif %} +{% set columns = columns|merge({ 'duration': {'class': 'text-center hw-min', 'title': 'stats.durationMonth'|trans, 'columnClass': 'w-min'}, 'rate': {'class': 'text-center hw-min', 'title': 'stats.amountMonth'|trans, 'columnClass': 'w-min'}, 'billable': {'class': 'text-center hw-min', 'columnClass': 'w-min'}, 'actions': {'class': 'actions alwaysVisible'}, -} %} +}) %} {% set tableName = 'project_daterange_reporting' %} {% block main_before %} @@ -72,29 +85,31 @@ {% for entry in mapping.projects|sort((a, b) => a.entity.name <=> b.entity.name) %} {% set project = entry.entity %} {% set currency = project.customer.currency %} - {% if is_granted('budget', project) %} - - {% for name, column_config in columns %} - - {% if name == 'name' %} + + {% for name, column_config in columns %} + + {% if name == 'name' %} {{ widgets.label_project(project) }} - {% elseif name == 'duration' %} - {{ entry.statistic.duration|duration }} - {% elseif name == 'rate' %} - {{ entry.statistic.rate|money(currency) }} - {% elseif name == 'billable' %} - {{ widgets.percent(entry.statistic.rate, entry.statistic.rateBillable) }} - {% elseif name == 'timeBudget' %} + {% elseif name == 'duration' %} + {{ entry.statistic.duration|duration }} + {% elseif name == 'rate' %} + {{ entry.statistic.rate|money(currency) }} + {% elseif name == 'billable' %} + {{ widgets.percent(entry.statistic.rate, entry.statistic.rateBillable) }} + {% elseif name == 'timeBudget' %} + {% if is_granted('time', project) %} {{ progress.progressbar_timebudget(entry) }} - {% elseif name == 'budget' %} - {{ progress.progressbar_budget(entry, currency) }} - {% elseif name == 'actions' %} - {{ projectActions.project(project, 'custom') }} {% endif %} - - {% endfor %} - - {% endif %} + {% elseif name == 'budget' %} + {% if is_granted('budget', project) %} + {{ progress.progressbar_budget(entry, currency) }} + {% endif %} + {% elseif name == 'actions' %} + {{ projectActions.project(project, 'custom') }} + {% endif %} + + {% endfor %} + {% endfor %} {% endfor %} {{ tables.data_table_footer(entries) }} diff --git a/templates/reporting/project_details.html.twig b/templates/reporting/project_details.html.twig index a5be3180..f79e3254 100644 --- a/templates/reporting/project_details.html.twig +++ b/templates/reporting/project_details.html.twig @@ -6,7 +6,8 @@ {% set tableName = tableName|default('project_details_reporting') %} {% set tableId = 'project-details-form' %} -{% set view_budget = project_details is not null and is_granted('budget', project_details.project) %} +{% set showMoneyBudget = project_details is not null and is_granted('budget', project_details.project) %} +{% set showTimeBudget = project_details is not null and is_granted('time', project_details.project) %} {% set view_revenue = project_details is not null and is_granted('view_rate_other_timesheet') %} {% set see_users = is_granted('view_other_timesheet') or is_granted('view_other_reporting') %} @@ -79,7 +80,7 @@ {% endif %} {% if hasData %} - {{ _self.project_details(project, project_view, project_details, view_budget, view_revenue, see_users) }} + {{ _self.project_details(project, project_view, project_details, showMoneyBudget, showTimeBudget, view_revenue, see_users) }} {% set currency = project.customer.currency %} {%- for yearStat in project_details.years|reverse %} @@ -245,7 +246,7 @@ project_view = ProjectViewModel project_details = ProjectDetailsModel #} -{% macro project_details(project, project_view, project_details, view_budget, view_revenue, see_users) %} +{% macro project_details(project, project_view, project_details, showMoneyBudget, showTimeBudget, view_revenue, see_users) %} {% set activities = project_details.activities %} {% set years = project_details.years %} {% import "macros/progressbar.html.twig" as progress %} @@ -352,12 +353,12 @@
- {% if view_budget and (project.timeBudget > 0 or project.budget > 0) %} + {% if (showMoneyBudget and project.budget > 0) or (showTimeBudget and project.timeBudget > 0) %} {% set budgetStats = project_details.budgetStatisticModel %}
- {% if project.timeBudget > 0 %} + {% if showTimeBudget and project.timeBudget > 0 %} {% endif %} - {% if project.budget > 0 %} + {% if showMoneyBudget and project.budget > 0 %} - {% for name, column_config in columns %} - {% if name == 'name' %} - - {% elseif name == 'lastRecord' %} - - {% elseif name == 'today' %} - - {% elseif name == 'week' %} - - {% elseif name == 'month' %} - - {% elseif name == 'durationTotal' %} - - {% elseif name == 'timeBudget' %} - - {% elseif name == 'budget' %} - - {% elseif name == 'exported' %} - - {% elseif name == 'invoiced' %} - - {% elseif name == 'projectStart' %} - - {% elseif name == 'projectEnd' %} - - {% elseif name == 'comment' %} - - {% elseif name == 'actions' %} - + + {% for name, column_config in columns %} + {% if name == 'name' %} + + {% elseif name == 'lastRecord' %} + - {% endif %} + + {% elseif name == 'today' %} + + {% elseif name == 'week' %} + + {% elseif name == 'month' %} + + {% elseif name == 'durationTotal' %} + + {% elseif name == 'timeBudget' %} + + {% elseif name == 'budget' %} + + {% elseif name == 'exported' %} + + {% elseif name == 'invoiced' %} + + {% elseif name == 'projectStart' %} + + {% elseif name == 'projectEnd' %} + + {% elseif name == 'comment' %} + + {% elseif name == 'actions' %} + + {% endif %} + {% endfor %} + {% endfor %} {% endfor %} {{ tables.data_table_footer(entries) }} diff --git a/templates/widget/widget-userteamprojects.html.twig b/templates/widget/widget-userteamprojects.html.twig index 0d00868b..e1b9afe2 100644 --- a/templates/widget/widget-userteamprojects.html.twig +++ b/templates/widget/widget-userteamprojects.html.twig @@ -25,10 +25,10 @@ {{ widgets.label_customer(project.customer) }} diff --git a/tests/Controller/PermissionControllerTest.php b/tests/Controller/PermissionControllerTest.php index 30546f31..7826a605 100644 --- a/tests/Controller/PermissionControllerTest.php +++ b/tests/Controller/PermissionControllerTest.php @@ -35,7 +35,7 @@ class PermissionControllerTest extends ControllerBaseTest $client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN); $this->assertAccessIsGranted($client, '/admin/permissions'); $this->assertHasDataTable($client); - $this->assertDataTableRowCount($client, 'datatable_user_admin_permissions', 123); + $this->assertDataTableRowCount($client, 'datatable_user_admin_permissions', 132); $this->assertPageActions($client, [ //'back' => $this->createUrl('/admin/user/'), 'create modal-ajax-form' => $this->createUrl('/admin/permissions/roles/create'), diff --git a/tests/Form/ActivityEditFormTest.php b/tests/Form/ActivityEditFormTest.php index 11ac0181..abe3ce99 100644 --- a/tests/Form/ActivityEditFormTest.php +++ b/tests/Form/ActivityEditFormTest.php @@ -35,16 +35,43 @@ class ActivityEditFormTest extends TypeTestCase self::assertTrue($form->has('color')); self::assertTrue($form->has('metaFields')); self::assertTrue($form->has('visible')); + self::assertFalse($form->has('budget')); + self::assertFalse($form->has('timeBudget')); + self::assertFalse($form->has('budgetType')); } - public function testWithGlobalNewActivityAndOptions() + public function testWithGlobalNewActivityAndOptionsBudget() { $model = new Activity(); $form = $this->factory->createBuilder(ActivityEditForm::class, $model, [ - 'include_budget' => true + 'include_budget' => true, + ]); + self::assertTrue($form->has('budget')); + self::assertFalse($form->has('timeBudget')); + self::assertTrue($form->has('budgetType')); + } + + public function testWithGlobalNewActivityAndOptionsTimeBudget() + { + $model = new Activity(); + $form = $this->factory->createBuilder(ActivityEditForm::class, $model, [ + 'include_time' => true, + ]); + self::assertFalse($form->has('budget')); + self::assertTrue($form->has('timeBudget')); + self::assertTrue($form->has('budgetType')); + } + + public function testWithGlobalNewActivityAndOptionsAllBudget() + { + $model = new Activity(); + $form = $this->factory->createBuilder(ActivityEditForm::class, $model, [ + 'include_budget' => true, + 'include_time' => true, ]); self::assertTrue($form->has('budget')); self::assertTrue($form->has('timeBudget')); + self::assertTrue($form->has('budgetType')); } public function testWithGlobalExistingActivityAndOptions() @@ -52,11 +79,11 @@ class ActivityEditFormTest extends TypeTestCase $model = $this->createMock(Activity::class); $model->expects($this->once())->method('getId')->willReturn(1); $form = $this->factory->createBuilder(ActivityEditForm::class, $model, [ - 'include_budget' => true + 'include_budget' => true, ]); self::assertFalse($form->has('project')); self::assertTrue($form->has('budget')); - self::assertTrue($form->has('timeBudget')); + self::assertFalse($form->has('timeBudget')); } public function testWithNonGlobalExistingActivityAndOptions() @@ -69,7 +96,8 @@ class ActivityEditFormTest extends TypeTestCase $model->expects($this->any())->method('getId')->willReturn(1); $model->expects($this->any())->method('getProject')->willReturn($project); $form = $this->factory->createBuilder(ActivityEditForm::class, $model, [ - 'include_budget' => true + 'include_budget' => true, + 'include_time' => true, ]); self::assertTrue($form->has('name')); self::assertTrue($form->has('comment')); diff --git a/tests/Form/CustomerEditFormTest.php b/tests/Form/CustomerEditFormTest.php new file mode 100644 index 00000000..77ad9828 --- /dev/null +++ b/tests/Form/CustomerEditFormTest.php @@ -0,0 +1,73 @@ +factory->createBuilder(CustomerEditForm::class, $model); + + $attr = $form->getFormConfig()->getOption('attr'); + self::assertArrayHasKey('data-form-event', $attr); + self::assertEquals('kimai.customerUpdate', $attr['data-form-event']); + + self::assertTrue($form->has('name')); + self::assertTrue($form->has('comment')); + self::assertTrue($form->has('color')); + self::assertTrue($form->has('metaFields')); + self::assertTrue($form->has('visible')); + self::assertFalse($form->has('budget')); + self::assertFalse($form->has('timeBudget')); + self::assertFalse($form->has('budgetType')); + } + + public function testWithBudget() + { + $model = new Customer(); + $form = $this->factory->createBuilder(CustomerEditForm::class, $model, [ + 'include_budget' => true, + ]); + self::assertTrue($form->has('budget')); + self::assertFalse($form->has('timeBudget')); + self::assertTrue($form->has('budgetType')); + } + + public function testWithTimeBudget() + { + $model = new Customer(); + $form = $this->factory->createBuilder(CustomerEditForm::class, $model, [ + 'include_time' => true, + ]); + self::assertFalse($form->has('budget')); + self::assertTrue($form->has('timeBudget')); + self::assertTrue($form->has('budgetType')); + } + + public function testWithBudgetAndTimeBudget() + { + $model = new Customer(); + $form = $this->factory->createBuilder(CustomerEditForm::class, $model, [ + 'include_budget' => true, + 'include_time' => true, + ]); + self::assertTrue($form->has('budget')); + self::assertTrue($form->has('timeBudget')); + self::assertTrue($form->has('budgetType')); + } +} diff --git a/tests/Form/ProjectEditFormTest.php b/tests/Form/ProjectEditFormTest.php new file mode 100644 index 00000000..8a2f1865 --- /dev/null +++ b/tests/Form/ProjectEditFormTest.php @@ -0,0 +1,74 @@ +factory->createBuilder(ProjectEditForm::class, $model); + + $attr = $form->getFormConfig()->getOption('attr'); + self::assertArrayHasKey('data-form-event', $attr); + self::assertEquals('kimai.projectUpdate', $attr['data-form-event']); + + self::assertTrue($form->has('name')); + self::assertTrue($form->has('comment')); + self::assertTrue($form->has('customer')); + self::assertTrue($form->has('color')); + self::assertTrue($form->has('metaFields')); + self::assertTrue($form->has('visible')); + self::assertFalse($form->has('budget')); + self::assertFalse($form->has('timeBudget')); + self::assertFalse($form->has('budgetType')); + } + + public function testWithBudget() + { + $model = new Project(); + $form = $this->factory->createBuilder(ProjectEditForm::class, $model, [ + 'include_budget' => true, + ]); + self::assertTrue($form->has('budget')); + self::assertFalse($form->has('timeBudget')); + self::assertTrue($form->has('budgetType')); + } + + public function testWithTimeBudget() + { + $model = new Project(); + $form = $this->factory->createBuilder(ProjectEditForm::class, $model, [ + 'include_time' => true, + ]); + self::assertFalse($form->has('budget')); + self::assertTrue($form->has('timeBudget')); + self::assertTrue($form->has('budgetType')); + } + + public function testWithBudgetAndTimeBudget() + { + $model = new Project(); + $form = $this->factory->createBuilder(ProjectEditForm::class, $model, [ + 'include_budget' => true, + 'include_time' => true, + ]); + self::assertTrue($form->has('budget')); + self::assertTrue($form->has('timeBudget')); + self::assertTrue($form->has('budgetType')); + } +} diff --git a/tests/Validator/Constraints/TimesheetBudgetUsedValidatorTest.php b/tests/Validator/Constraints/TimesheetBudgetUsedValidatorTest.php index 25bedff6..2d50250b 100644 --- a/tests/Validator/Constraints/TimesheetBudgetUsedValidatorTest.php +++ b/tests/Validator/Constraints/TimesheetBudgetUsedValidatorTest.php @@ -31,6 +31,7 @@ use App\Timesheet\RateServiceInterface; use App\Validator\Constraints\TimesheetBudgetUsed; use App\Validator\Constraints\TimesheetBudgetUsedValidator; use DateTime; +use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; @@ -88,7 +89,9 @@ class TimesheetBudgetUsedValidatorTest extends ConstraintValidatorTestCase $rateService = new RateService([], $timesheetRepository); } - return new TimesheetBudgetUsedValidator($configuration, $customerRepository, $projectRepository, $activityRepository, $timesheetRepository, $rateService); + $auth = $this->createMock(AuthorizationCheckerInterface::class); + + return new TimesheetBudgetUsedValidator($configuration, $customerRepository, $projectRepository, $activityRepository, $timesheetRepository, $rateService, $auth); } public function testConstraintIsInvalid() @@ -466,7 +469,7 @@ class TimesheetBudgetUsedValidatorTest extends ConstraintValidatorTestCase if (null === $used && null === $budget && null === $free && $path === null) { $this->assertNoViolation(); } else { - $this->buildViolation('The budget is completely used.') + $this->buildViolation('Sorry, the budget is used up.') ->atPath('property.path.' . $path) ->setParameters([ '%used%' => $used, diff --git a/tests/Voter/AbstractVoterTest.php b/tests/Voter/AbstractVoterTest.php index 854f0165..55c64151 100644 --- a/tests/Voter/AbstractVoterTest.php +++ b/tests/Voter/AbstractVoterTest.php @@ -41,6 +41,7 @@ abstract class AbstractVoterTest extends TestCase $user = new User(); $user->setRoles($roles); + $user->setUsername($id); $reflection = new \ReflectionClass($user); $property = $reflection->getProperty('id'); @@ -58,12 +59,12 @@ abstract class AbstractVoterTest extends TestCase protected function getRolePermissionManager(array $permissions = [], bool $overwrite = false) { if (!$overwrite) { - $activities = ['view_activity', 'edit_activity', 'budget_activity', 'delete_activity', 'create_activity']; - $activitiesTeam = ['view_activity', 'create_activity', 'edit_teamlead_activity', 'budget_teamlead_activity']; - $projects = ['view_project', 'create_project', 'edit_project', 'budget_project', 'delete_project', 'permissions_project', 'comments_project', 'details_project']; - $projectsTeam = ['view_teamlead_project', 'edit_teamlead_project', 'budget_teamlead_project', 'permissions_teamlead_project', 'comments_teamlead_project', 'details_teamlead_project']; - $customers = ['view_customer', 'create_customer', 'edit_customer', 'budget_customer', 'delete_customer', 'permissions_customer', 'comments_customer', 'details_customer']; - $customersTeam = ['view_teamlead_customer', 'edit_teamlead_customer', 'budget_teamlead_customer', 'comments_teamlead_customer', 'details_teamlead_customer']; + $activities = ['view_activity', 'edit_activity', 'budget_activity', 'time_activity', 'delete_activity', 'create_activity']; + $activitiesTeam = ['view_activity', 'create_activity', 'edit_teamlead_activity', 'budget_teamlead_activity', 'time_teamlead_activity']; + $projects = ['view_project', 'create_project', 'edit_project', 'budget_project', 'time_project', 'delete_project', 'permissions_project', 'comments_project', 'details_project']; + $projectsTeam = ['view_teamlead_project', 'edit_teamlead_project', 'budget_teamlead_project', 'time_teamlead_project', 'permissions_teamlead_project', 'comments_teamlead_project', 'details_teamlead_project']; + $customers = ['view_customer', 'create_customer', 'edit_customer', 'budget_customer', 'time_customer', 'delete_customer', 'permissions_customer', 'comments_customer', 'details_customer']; + $customersTeam = ['view_teamlead_customer', 'edit_teamlead_customer', 'budget_teamlead_customer', 'time_teamlead_customer', 'comments_teamlead_customer', 'details_teamlead_customer']; $invoice = ['view_invoice', 'create_invoice']; $invoiceTemplate = ['manage_invoice_template']; $timesheet = ['view_own_timesheet', 'start_own_timesheet', 'stop_own_timesheet', 'create_own_timesheet', 'edit_own_timesheet', 'export_own_timesheet', 'delete_own_timesheet']; diff --git a/tests/Voter/EntityMultiRoleVoterTest.php b/tests/Voter/EntityMultiRoleVoterTest.php new file mode 100644 index 00000000..b29237d6 --- /dev/null +++ b/tests/Voter/EntityMultiRoleVoterTest.php @@ -0,0 +1,86 @@ +getRoles()); + $sut = $this->getVoter(EntityMultiRoleVoter::class); + + $this->assertEquals($result, $sut->vote($token, $subject, [$attribute]), 'Failed on permission "' . $attribute . '" for User ' . $user->getUsername()); + } + + public function getTestData() + { + $user0 = $this->getUser(0, null); + $user1 = $this->getUser(1, User::ROLE_USER); + $user2 = $this->getUser(2, User::ROLE_TEAMLEAD); + $user3 = $this->getUser(3, User::ROLE_ADMIN); + $user4 = $this->getUser(4, User::ROLE_SUPER_ADMIN); + + $result = VoterInterface::ACCESS_GRANTED; + $allPermissions = ['budget_money', 'budget_time', 'budget_any', 'details']; + $allSubjects = ['project', 'customer', new Project(), new Customer()]; + + foreach ($allPermissions as $permission) { + foreach ($allSubjects as $subject) { + yield [$user3, $subject, $permission, $result]; + yield [$user4, $subject, $permission, $result]; + } + } + + $result = VoterInterface::ACCESS_GRANTED; + $allPermissions = ['budget_money', 'budget_time', 'budget_any']; + $allSubjects = ['activity', new Activity()]; + + foreach ($allPermissions as $permission) { + foreach ($allSubjects as $subject) { + yield [$user3, $subject, $permission, $result]; + yield [$user4, $subject, $permission, $result]; + } + } + + $result = VoterInterface::ACCESS_DENIED; + yield [$user4, 'activity', 'details', $result]; // there is no details permission for activity + + $result = VoterInterface::ACCESS_ABSTAIN; + yield [$user0, 'team', 'view', $result]; + yield [$user0, 'team', 'edit', $result]; + yield [$user0, 'team', 'delete', $result]; + yield [$user1, 'team', 'view', $result]; + yield [$user1, 'team', 'edit', $result]; + yield [$user1, 'team', 'delete', $result]; + yield [$user2, 'team', 'view', $result]; + yield [$user2, 'team', 'edit', $result]; + yield [$user2, 'team', 'delete', $result]; + yield [$user3, 'team', 'view', $result]; + yield [$user3, 'team', 'edit', $result]; + yield [$user3, 'team', 'delete', $result]; + yield [$user4, 'team', 'view', $result]; + yield [$user4, 'team', 'edit', $result]; + yield [$user4, 'team', 'delete', $result]; + } +} diff --git a/translations/validators.de.xlf b/translations/validators.de.xlf index 9079d10a..95b3cacb 100644 --- a/translations/validators.de.xlf +++ b/translations/validators.de.xlf @@ -66,6 +66,10 @@ The budget is completely used.Das Budget ist aufgebraucht. Von den vorhandenen %budget% wurden bisher %used% gebucht, noch nutzbar sind %free%. + + Sorry, the budget is used up. + Entschuldigung, das Budget ist aufgebraucht. + Maximum duration of {{ value }} hours exceeded. Erlaubt sind max. {{ value }} Stunden. diff --git a/translations/validators.de_CH.xlf b/translations/validators.de_CH.xlf index 24e14458..8e1fe857 100644 --- a/translations/validators.de_CH.xlf +++ b/translations/validators.de_CH.xlf @@ -62,6 +62,10 @@ The budget is completely used. Das Budget ist aufgebraucht. Von den vorhandenen %budget% wurden bisher %used% gebucht, noch nutzbar sind %free%. + + Sorry, the budget is used up. + Entschuldigung, das Budget ist aufgebraucht. + Maximum duration of {{ value }} hours exceeded. Erlaubt sind max. {{ value }} Stunden. diff --git a/translations/validators.en.xlf b/translations/validators.en.xlf index 331cad3d..ab9a53ed 100644 --- a/translations/validators.en.xlf +++ b/translations/validators.en.xlf @@ -66,6 +66,10 @@ The budget is completely used. The budget is used up. Of the available %budget%, %used% has been booked so far, %free% can still be used. + + Sorry, the budget is used up. + Sorry, the budget is used up. + Maximum duration of {{ value }} hours exceeded. Maximum {{ value }} hours allowed.
{{ 'label.timeBudget'|trans }} @@ -370,7 +371,7 @@
{{ 'label.budget'|trans }} diff --git a/templates/reporting/project_view.html.twig b/templates/reporting/project_view.html.twig index 3e69c139..60067297 100644 --- a/templates/reporting/project_view.html.twig +++ b/templates/reporting/project_view.html.twig @@ -3,22 +3,36 @@ {% block report_title %}{{ (title|default('report_project_view'))|trans({}, 'reporting') }}{% endblock %} +{% set showMoneyBudget = is_granted('budget_money', 'project') %} +{% set showTimeBudget = is_granted('budget_time', 'project') %} +{% set showBudgets = showMoneyBudget or showTimeBudget %} + {% set availableColumns = { 'name': {'class': 'alwaysVisible'}, - 'timeBudget': {'class': 'hidden-xs', 'title': 'label.timeBudget'|trans}, - 'budget': {'class': 'hidden-xs', 'title': 'label.budget'|trans}, +} %} +{% if showMoneyBudget %} + {% set availableColumns = availableColumns|merge({ + 'budget': {'class': 'hidden-xs', 'title': 'label.budget'|trans}, + 'invoiced': {'class': 'hidden-sm hidden-xs hidden text-center hw-min', 'title': 'label.not_invoiced'|trans, 'columnClass': 'w-min'}, + }) %} +{% endif %} +{% if showTimeBudget %} + {% set availableColumns = availableColumns|merge({ + 'timeBudget': {'class': 'hidden-xs', 'title': 'label.timeBudget'|trans}, + 'exported': {'class': 'hidden-sm hidden-xs hidden text-center hw-min', 'title': 'label.not_exported'|trans, 'columnClass': 'w-min'}, + }) %} +{% endif %} +{% set availableColumns = availableColumns|merge({ 'lastRecord': {'class': 'text-center hw-min', 'title': 'label.last_record'|trans, 'columnClass': 'w-min'}, 'today': {'class': 'hidden hidden-md hidden-sm hidden-xs text-center hw-min', 'title': 'stats.durationToday'|trans}, 'week': {'class': 'hidden hidden-md hidden-sm hidden-xs text-center hw-min', 'title': 'stats.durationWeek'|trans}, 'month': {'class': 'hidden hidden-md hidden-sm hidden-xs text-center hw-min', 'title': 'stats.durationMonth'|trans}, 'durationTotal': {'class': 'text-center hw-min', 'title': 'stats.durationTotal'|trans, 'columnClass': 'w-min'}, - 'exported': {'class': 'hidden-sm hidden-xs hidden text-center hw-min', 'title': 'label.not_exported'|trans, 'columnClass': 'w-min'}, - 'invoiced': {'class': 'hidden-sm hidden-xs hidden text-center hw-min', 'title': 'label.not_invoiced'|trans, 'columnClass': 'w-min'}, 'projectStart': {'class': 'hidden-md hidden-sm hidden-xs hidden text-center w-min', 'title': 'label.project_start'|trans}, 'projectEnd': {'class': 'hidden-md hidden-sm hidden-xs hidden text-center w-min', 'title': 'label.project_end'|trans}, 'comment': {'class': 'hidden-md hidden-sm hidden-xs hidden', 'title': 'label.comment'|trans}, 'actions': {'class': 'actions alwaysVisible'}, -} %} +}) %} {% set tableName = tableName|default('project_view_reporting') %} {% set skipColumns = skipColumns is defined ? skipColumns : {} %} {% set columns = {} %} @@ -93,73 +107,71 @@ {% set project = entry.project %} {% set budgetStats = entry.getBudgetStatisticModel() %} {% set currency = project.customer.currency %} - {% if is_granted('budget', project) %} -
{{ widgets.label_project(project) }} - {% if entry.lastRecord is not null %} - {{ entry.lastRecord|date_short }} - {% else %} - – - {% endif %} - {{ entry.durationDay|duration }}{{ entry.durationWeek|duration }}{{ entry.durationMonth|duration }}{{ entry.durationTotal|duration }} - {% if budgetStats.hasTimeBudget() %} - {{ progress.progressbar_timebudget(budgetStats) }} - {% endif %} - - {% if project.hasBudget() %} - {{ progress.progressbar_budget(budgetStats, project.customer.currency) }} - {% endif %} - - {% if is_granted('create_export') %} - - {{ entry.notExportedDuration|duration }} - - {% else %} - {{ entry.notExportedDuration|duration }} - {% endif %} - - {% if is_granted('view_invoice') %} - - {{ entry.notBilledRate|money(currency) }} - - {% else %} - {{ entry.notBilledRate|money(currency) }} - {% endif %} - {% if project.start is not null %}{{ project.start|date_short }}{% endif %}{% if project.end is not null %}{{ project.end|date_short }}{% endif %}{{ project.comment }} - {{ projectActions.project(project, 'custom') }} -
{{ widgets.label_project(project) }} + {% if entry.lastRecord is not null %} + {{ entry.lastRecord|date_short }} + {% else %} + – {% endif %} - {% endfor %} -
{{ entry.durationDay|duration }}{{ entry.durationWeek|duration }}{{ entry.durationMonth|duration }}{{ entry.durationTotal|duration }} + {% if budgetStats.hasTimeBudget() and is_granted('time', project) %} + {{ progress.progressbar_timebudget(budgetStats) }} + {% endif %} + + {% if project.hasBudget() and is_granted('budget', project) %} + {{ progress.progressbar_budget(budgetStats, project.customer.currency) }} + {% endif %} + + {% if is_granted('create_export') %} + + {{ entry.notExportedDuration|duration }} + + {% else %} + {{ entry.notExportedDuration|duration }} + {% endif %} + + {% if is_granted('view_invoice') %} + + {{ entry.notBilledRate|money(currency) }} + + {% else %} + {{ entry.notBilledRate|money(currency) }} + {% endif %} + {% if project.start is not null %}{{ project.start|date_short }}{% endif %}{% if project.end is not null %}{{ project.end|date_short }}{% endif %}{{ project.comment }} + {{ projectActions.project(project, 'custom') }} +
- {% if row.hasTimeBudget() %} - {{ progress.progressbar_timebudget(row) }} - {% elseif row.hasBudget() %} + {% if row.hasBudget() and is_granted('budget', row.entity) %} {{ progress.progressbar_budget(row, row.entity.customer.currency) }} + {% elseif row.hasTimeBudget() and is_granted('time', row.entity) %} + {{ progress.progressbar_timebudget(row) }} {% endif %}