added new permission to separate time and money budget (#3352)
This commit is contained in:
@@ -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']
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
],
|
||||
|
||||
@@ -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'
|
||||
],
|
||||
|
||||
@@ -26,22 +26,30 @@ trait EntityFormTrait
|
||||
{
|
||||
$this->addColor($builder);
|
||||
|
||||
if ($options['include_budget']) {
|
||||
$builder
|
||||
->add('budget', MoneyType::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'],
|
||||
])
|
||||
->add('timeBudget', DurationType::class, [
|
||||
]);
|
||||
}
|
||||
|
||||
if ($showTime) {
|
||||
$builder->add('timeBudget', DurationType::class, [
|
||||
'empty_data' => 0,
|
||||
'label' => 'label.timeBudget',
|
||||
'icon' => 'clock',
|
||||
'required' => false,
|
||||
])
|
||||
->add('budgetType', BudgetType::class)
|
||||
;
|
||||
]);
|
||||
}
|
||||
|
||||
if ($showBudget) {
|
||||
$builder->add('budgetType', BudgetType::class);
|
||||
}
|
||||
|
||||
$builder->add('metaFields', MetaFieldsCollectionType::class);
|
||||
|
||||
@@ -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' => [
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
|
||||
|
||||
@@ -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.';
|
||||
}
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -28,6 +28,7 @@ final class ActivityVoter extends Voter
|
||||
'view',
|
||||
'edit',
|
||||
'budget',
|
||||
'time',
|
||||
'delete',
|
||||
'permissions',
|
||||
];
|
||||
|
||||
@@ -29,6 +29,7 @@ final class CustomerVoter extends Voter
|
||||
'create',
|
||||
'edit',
|
||||
'budget',
|
||||
'time',
|
||||
'delete',
|
||||
'permissions',
|
||||
'comments',
|
||||
|
||||
121
src/Voter/EntityMultiRoleVoter.php
Normal file
121
src/Voter/EntityMultiRoleVoter.php
Normal file
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Voter;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\User;
|
||||
use App\Security\RolePermissionManager;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
|
||||
final class EntityMultiRoleVoter extends Voter
|
||||
{
|
||||
/**
|
||||
* support rules based on the given activity
|
||||
*/
|
||||
private const ALLOWED_ATTRIBUTES = [
|
||||
'budget_money',
|
||||
'budget_time',
|
||||
'budget_any',
|
||||
'details',
|
||||
];
|
||||
private const ALLOWED_SUBJECTS = [
|
||||
'customer',
|
||||
'project',
|
||||
'activity',
|
||||
];
|
||||
|
||||
private $permissionManager;
|
||||
|
||||
public function __construct(RolePermissionManager $permissionManager)
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ final class ProjectVoter extends Voter
|
||||
'view',
|
||||
'edit',
|
||||
'budget',
|
||||
'time',
|
||||
'delete',
|
||||
'permissions',
|
||||
'comments',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -28,14 +28,18 @@
|
||||
{% if form.project is defined %}
|
||||
{{ form_row(form.project) }}
|
||||
{% endif %}
|
||||
{% if form.budget is defined %}
|
||||
{% if form.budgetType is defined %}
|
||||
<div class="row">
|
||||
{% if form.budget is defined %}
|
||||
<div class="col-md-4">
|
||||
{{ form_row(form.budget) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form.timeBudget is defined %}
|
||||
<div class="col-md-4">
|
||||
{{ form_row(form.timeBudget) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="col-md-4">
|
||||
{{ form_row(form.budgetType) }}
|
||||
</div>
|
||||
|
||||
@@ -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({
|
||||
'team': {'class': 'text-center w-min', 'orderBy': false},
|
||||
'visible': {'class': 'text-center hidden w-min'},
|
||||
'actions': {'class': 'actions alwaysVisible'},
|
||||
@@ -63,14 +71,14 @@
|
||||
</td>
|
||||
{% endfor %}
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'budget') }}">
|
||||
{% 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 %}
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'timeBudget') }}">
|
||||
{% if entry.hasTimeBudget() %}
|
||||
{% if entry.hasTimeBudget() and is_granted('time', entry) %}
|
||||
{{ entry.timeBudget|duration }}
|
||||
{% else %}
|
||||
–
|
||||
|
||||
@@ -67,14 +67,18 @@
|
||||
{{ form_row(form.fax) }}
|
||||
</div>
|
||||
</div>
|
||||
{% if form.budget is defined %}
|
||||
{% if form.budgetType is defined %}
|
||||
<div class="row">
|
||||
{% if form.budget is defined %}
|
||||
<div class="col-md-4">
|
||||
{{ form_row(form.budget) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form.timeBudget is defined %}
|
||||
<div class="col-md-4">
|
||||
{{ form_row(form.timeBudget) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="col-md-4">
|
||||
{{ form_row(form.budgetType) }}
|
||||
</div>
|
||||
|
||||
@@ -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') %}
|
||||
<a class="btn btn-default btn-sm btn-pager" href="{{ path('report_project_view', {'customer': customer.id}) }}" data-toggle="tooltip" data-placement="top" title="{{ 'report_project_view'|trans({}, 'reporting') }}"><i class="{{ 'reporting'|icon }}"></i></a>
|
||||
{% endif %}
|
||||
{% if customer.visible and is_granted('create_project') %}
|
||||
|
||||
@@ -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 @@
|
||||
</td>
|
||||
{% endfor %}
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'budget') }}">
|
||||
{% if entry.hasBudget() %}
|
||||
{% if entry.hasBudget() and is_granted('budget', entry) %}
|
||||
{{ entry.budget|money(entry.currency) }}
|
||||
{% else %}
|
||||
–
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'timeBudget') }}">
|
||||
{% if entry.hasTimeBudget() %}
|
||||
{% if entry.hasTimeBudget() and is_granted('time', entry) %}
|
||||
{{ entry.timeBudget|duration }}
|
||||
{% else %}
|
||||
–
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{% 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 %}
|
||||
@@ -70,6 +71,8 @@
|
||||
</div>
|
||||
{% 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 %}
|
||||
@@ -152,3 +155,4 @@
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
{% endif %}
|
||||
|
||||
@@ -90,8 +90,7 @@
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if is_granted('budget', project) %}
|
||||
{% if project.hasBudget() %}
|
||||
{% if project.hasBudget() and is_granted('budget', project) %}
|
||||
<tr>
|
||||
<th>
|
||||
{{ 'label.budget'|trans }}
|
||||
@@ -104,7 +103,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if project.hasTimeBudget() %}
|
||||
{% if project.hasTimeBudget() and is_granted('time', project) %}
|
||||
<tr>
|
||||
<th>
|
||||
{{ 'label.timeBudget'|trans }}
|
||||
@@ -117,7 +116,6 @@
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% for metaField in project.visibleMetaFields|sort((a, b) => a.order <=> b.order) %}
|
||||
<tr>
|
||||
<th>{{ metaField.label|trans }}</th>
|
||||
|
||||
@@ -39,14 +39,18 @@
|
||||
{{ form_row(form.end) }}
|
||||
</div>
|
||||
</div>
|
||||
{% if form.budget is defined %}
|
||||
{% if form.budgetType is defined %}
|
||||
<div class="row">
|
||||
{% if form.budget is defined %}
|
||||
<div class="col-md-4">
|
||||
{{ form_row(form.budget) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form.timeBudget is defined %}
|
||||
<div class="col-md-4">
|
||||
{{ form_row(form.timeBudget) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="col-md-4">
|
||||
{{ form_row(form.budgetType) }}
|
||||
</div>
|
||||
|
||||
@@ -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({
|
||||
'team': {'class': 'text-center w-min', 'orderBy': false},
|
||||
'visible': {'class': 'text-center hidden w-min'},
|
||||
'actions': {'class': 'actions alwaysVisible'},
|
||||
@@ -60,14 +68,14 @@
|
||||
</td>
|
||||
{% endfor %}
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'budget') }}">
|
||||
{% if entry.hasBudget() %}
|
||||
{% if entry.hasBudget() and is_granted('budget', entry) %}
|
||||
{{ entry.budget|money(entry.customer.currency) }}
|
||||
{% else %}
|
||||
–
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'timeBudget') }}">
|
||||
{% if entry.hasTimeBudget() %}
|
||||
{% if entry.hasTimeBudget() and is_granted('time', entry) %}
|
||||
{{ entry.timeBudget|duration }}
|
||||
{% else %}
|
||||
–
|
||||
|
||||
@@ -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'},
|
||||
} %}
|
||||
{% 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,7 +85,6 @@
|
||||
{% 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) %}
|
||||
<tr {{ widgets.project_row_attr(project, queryEnd) }}>
|
||||
{% for name, column_config in columns %}
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, name) }}">
|
||||
@@ -85,16 +97,19 @@
|
||||
{% elseif name == 'billable' %}
|
||||
{{ widgets.percent(entry.statistic.rate, entry.statistic.rateBillable) }}
|
||||
{% elseif name == 'timeBudget' %}
|
||||
{% if is_granted('time', project) %}
|
||||
{{ progress.progressbar_timebudget(entry) }}
|
||||
{% endif %}
|
||||
{% elseif name == 'budget' %}
|
||||
{% if is_granted('budget', project) %}
|
||||
{{ progress.progressbar_budget(entry, currency) }}
|
||||
{% endif %}
|
||||
{% elseif name == 'actions' %}
|
||||
{{ projectActions.project(project, 'custom') }}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{{ tables.data_table_footer(entries) }}
|
||||
|
||||
@@ -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 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% 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 %}
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<table class="table table-hover dataTable">
|
||||
{% if project.timeBudget > 0 %}
|
||||
{% if showTimeBudget and project.timeBudget > 0 %}
|
||||
<tr>
|
||||
<th class="w-min">
|
||||
{{ 'label.timeBudget'|trans }}
|
||||
@@ -370,7 +371,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if project.budget > 0 %}
|
||||
{% if showMoneyBudget and project.budget > 0 %}
|
||||
<tr>
|
||||
<th class="w-min">
|
||||
{{ 'label.budget'|trans }}
|
||||
|
||||
@@ -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},
|
||||
} %}
|
||||
{% 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,7 +107,6 @@
|
||||
{% set project = entry.project %}
|
||||
{% set budgetStats = entry.getBudgetStatisticModel() %}
|
||||
{% set currency = project.customer.currency %}
|
||||
{% if is_granted('budget', project) %}
|
||||
<tr {{ widgets.project_row_attr(project, now) }}>
|
||||
{% for name, column_config in columns %}
|
||||
{% if name == 'name' %}
|
||||
@@ -116,13 +129,13 @@
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'durationTotal') }} w-min">{{ entry.durationTotal|duration }}</td>
|
||||
{% elseif name == 'timeBudget' %}
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'timeBudget') }}">
|
||||
{% if budgetStats.hasTimeBudget() %}
|
||||
{% if budgetStats.hasTimeBudget() and is_granted('time', project) %}
|
||||
{{ progress.progressbar_timebudget(budgetStats) }}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% elseif name == 'budget' %}
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'budget') }}">
|
||||
{% if project.hasBudget() %}
|
||||
{% if project.hasBudget() and is_granted('budget', project) %}
|
||||
{{ progress.progressbar_budget(budgetStats, project.customer.currency) }}
|
||||
{% endif %}
|
||||
</td>
|
||||
@@ -159,7 +172,6 @@
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{{ tables.data_table_footer(entries) }}
|
||||
|
||||
@@ -25,10 +25,10 @@
|
||||
<small>{{ widgets.label_customer(project.customer) }}</small>
|
||||
</td>
|
||||
<td style="width:50%">
|
||||
{% 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 %}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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'));
|
||||
|
||||
73
tests/Form/CustomerEditFormTest.php
Normal file
73
tests/Form/CustomerEditFormTest.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Form;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Form\CustomerEditForm;
|
||||
use Symfony\Component\Form\Test\TypeTestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Form\CustomerEditForm
|
||||
*/
|
||||
class CustomerEditFormTest extends TypeTestCase
|
||||
{
|
||||
public function testWithNewProject()
|
||||
{
|
||||
$model = new Customer();
|
||||
$form = $this->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'));
|
||||
}
|
||||
}
|
||||
74
tests/Form/ProjectEditFormTest.php
Normal file
74
tests/Form/ProjectEditFormTest.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Form;
|
||||
|
||||
use App\Entity\Project;
|
||||
use App\Form\ProjectEditForm;
|
||||
use Symfony\Component\Form\Test\TypeTestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Form\ProjectEditForm
|
||||
*/
|
||||
class ProjectEditFormTest extends TypeTestCase
|
||||
{
|
||||
public function testWithNewProject()
|
||||
{
|
||||
$model = new Project();
|
||||
$form = $this->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'));
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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'];
|
||||
|
||||
86
tests/Voter/EntityMultiRoleVoterTest.php
Normal file
86
tests/Voter/EntityMultiRoleVoterTest.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Voter;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\User;
|
||||
use App\Voter\EntityMultiRoleVoter;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
|
||||
|
||||
/**
|
||||
* @covers \App\Voter\EntityMultiRoleVoter
|
||||
*/
|
||||
class EntityMultiRoleVoterTest extends AbstractVoterTest
|
||||
{
|
||||
/**
|
||||
* @dataProvider getTestData
|
||||
*/
|
||||
public function testVote(User $user, $subject, $attribute, $result)
|
||||
{
|
||||
$token = new UsernamePasswordToken($user, 'foo', 'bar', $user->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];
|
||||
}
|
||||
}
|
||||
@@ -66,6 +66,10 @@
|
||||
<source>The budget is completely used.</source>
|
||||
<target>Das Budget ist aufgebraucht. Von den vorhandenen %budget% wurden bisher %used% gebucht, noch nutzbar sind %free%.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="dcPei9G" resname="Sorry, the budget is used up.">
|
||||
<source>Sorry, the budget is used up.</source>
|
||||
<target>Entschuldigung, das Budget ist aufgebraucht.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="6rO8GZ1" resname="Maximum duration of {{ value }} hours exceeded.">
|
||||
<source>Maximum duration of {{ value }} hours exceeded.</source>
|
||||
<target>Erlaubt sind max. {{ value }} Stunden.</target>
|
||||
|
||||
@@ -62,6 +62,10 @@
|
||||
<source>The budget is completely used.</source>
|
||||
<target>Das Budget ist aufgebraucht. Von den vorhandenen %budget% wurden bisher %used% gebucht, noch nutzbar sind %free%.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="dcPei9G" resname="Sorry, the budget is used up.">
|
||||
<source>Sorry, the budget is used up.</source>
|
||||
<target>Entschuldigung, das Budget ist aufgebraucht.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="6rO8GZ1" resname="Maximum duration of {{ value }} hours exceeded.">
|
||||
<source>Maximum duration of {{ value }} hours exceeded.</source>
|
||||
<target>Erlaubt sind max. {{ value }} Stunden.</target>
|
||||
|
||||
@@ -66,6 +66,10 @@
|
||||
<source>The budget is completely used.</source>
|
||||
<target>The budget is used up. Of the available %budget%, %used% has been booked so far, %free% can still be used.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="dcPei9G" resname="Sorry, the budget is used up.">
|
||||
<source>Sorry, the budget is used up.</source>
|
||||
<target>Sorry, the budget is used up.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="6rO8GZ1" resname="Maximum duration of {{ value }} hours exceeded.">
|
||||
<source>Maximum duration of {{ value }} hours exceeded.</source>
|
||||
<target>Maximum {{ value }} hours allowed.</target>
|
||||
|
||||
Reference in New Issue
Block a user