added internal rates (#1591)

This commit is contained in:
Kevin Papst
2020-04-08 14:50:15 +02:00
committed by GitHub
parent 82c713031e
commit 68d703d1e3
128 changed files with 4044 additions and 1571 deletions

1764
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -15,6 +15,8 @@ fos_rest:
# enabled: false
# service: null
routing_loader:
# TODO activate once 2.8 is released
#parse_controller_name: false
default_format: json
include_format: true
# prefix_methods: true

View File

@@ -4,14 +4,20 @@ nelmio_api_doc:
names:
- { alias: CustomerEditForm, type: App\Form\API\CustomerApiEditForm, groups: [Default, Entity, Customer] }
- { alias: CustomerEntity, type: App\Entity\Customer, groups: [Default, Entity, Customer] }
- { alias: CustomerRate, type: App\Entity\CustomerRate, groups: [Default, Entity, CustomerRate] }
- { alias: CustomerRateForm, type: App\Form\API\CustomerRateApiForm, groups: [Default, Entity, CustomerRate] }
- { alias: CustomerMetaField, type: App\Entity\CustomerMeta, groups: [Default, Customer] }
- { alias: CustomerCollection, type: App\Entity\Customer, groups: [Default, Collection, Customer] }
- { alias: ProjectEditForm, type: App\Form\API\ProjectApiEditForm, groups: [Default, Entity, Project] }
- { alias: ProjectEntity, type: App\Entity\Project, groups: [Default, Entity, Project] }
- { alias: ProjectRate, type: App\Entity\ProjectRate, groups: [Default, Entity, ProjectRate] }
- { alias: ProjectRateForm, type: App\Form\API\ProjectRateApiForm, groups: [Default, Entity, ProjectRate] }
- { alias: ProjectMetaField, type: App\Entity\ProjectMeta, groups: [Default, Project] }
- { alias: ProjectCollection, type: App\Entity\Project, groups: [Default, Collection, Project] }
- { alias: ActivityEditForm, type: App\Form\API\ActivityApiEditForm, groups: [Default, Entity, Activity] }
- { alias: ActivityEntity, type: App\Entity\Activity, groups: [Default, Entity, Activity] }
- { alias: ActivityRate, type: App\Entity\ActivityRate, groups: [Default, Entity, ActivityRate] }
- { alias: ActivityRateForm, type: App\Form\API\ActivityRateApiForm, groups: [Default, Entity, ActivityRate] }
- { alias: ActivityMetaField, type: App\Entity\ActivityMeta, groups: [Default, Activity] }
- { alias: ActivityCollection, type: App\Entity\Activity, groups: [Default, Collection, Activity] }
- { alias: TagEditForm, type: App\Form\API\TagApiEditForm, groups: [Default, Entity, Tag] }
@@ -23,6 +29,7 @@ nelmio_api_doc:
- { alias: TimesheetSubCollection, type: App\Entity\Timesheet, groups: [Default, Subresource, Timesheet] }
- { alias: UserCreateForm, type: App\Form\API\UserApiCreateForm, groups: [Default, Entity, User, User_Entity] }
- { alias: UserEditForm, type: App\Form\API\UserApiEditForm, groups: [Default, Entity, User, User_Entity] }
- { alias: User, type: App\Entity\User, groups: [Default, Entity, User] }
- { alias: UserEntity, type: App\Entity\User, groups: [Default, Entity, User, User_Entity] }
- { alias: UserCollection, type: App\Entity\User, groups: [Default, Collection, User] }
- { alias: TeamEditForm, type: App\Form\API\TeamApiEditForm, groups: [Default, Entity, Team] }
@@ -42,8 +49,8 @@ nelmio_api_doc:
title: Kimai 2 - API Docs
description: |
JSON API for the Kimai 2 time-tracking software. Read more about its usage in the [API documentation](https://www.kimai.org/documentation/rest-api.html) and then download a [Swagger file](doc.json) for import e.g. in Postman.
Be aware: it is not yet considered stable and BC breaks might happen, but we try to avoid them.
version: '0.4'
Be aware: it is not yet considered stable and BC breaks might happen.
version: '0.5'
securityDefinitions:
apiUser:
type: apiKey

View File

@@ -0,0 +1,21 @@
App\Entity\ActivityRate:
exclusion_policy: All
custom_accessor_order: [id, rate, internalRate, isFixed, user]
properties:
id:
include: true
groups: [Default]
activity:
exclude: true
user:
include: true
groups: [Default]
rate:
include: true
groups: [Default]
internalRate:
include: true
groups: [Default]
isFixed:
include: true
groups: [Default]

View File

@@ -0,0 +1,21 @@
App\Entity\CustomerRate:
exclusion_policy: All
custom_accessor_order: [id, rate, internalRate, isFixed, user]
properties:
id:
include: true
groups: [Default]
customer:
exclude: true
user:
include: true
groups: [Default]
rate:
include: true
groups: [Default]
internalRate:
include: true
groups: [Default]
isFixed:
include: true
groups: [Default]

View File

@@ -0,0 +1,21 @@
App\Entity\ProjectRate:
exclusion_policy: All
custom_accessor_order: [id, rate, internalRate, isFixed, user]
properties:
id:
include: true
groups: [Default]
project:
exclude: true
user:
include: true
groups: [Default]
rate:
include: true
groups: [Default]
internalRate:
include: true
groups: [Default]
isFixed:
include: true
groups: [Default]

View File

@@ -14,6 +14,8 @@ App\Entity\Timesheet:
include: true
rate:
include: true
internalRate:
include: true
fixedRate:
include: true
groups: [Entity]

View File

@@ -7,8 +7,6 @@ parameters:
tmpDir: %rootDir%/../../../var/cache/phpstan
autoload_directories:
- %rootDir%/../../../src/Migrations
symfony:
container_xml_path: '%rootDir%/../../../var/cache/dev/srcApp_KernelDevDebugContainer.xml'
ignoreErrors:
- '#Call to an undefined method Symfony\\Component\\Config\\Definition\\Builder\\NodeParentInterface::scalarNode\(\).#'
- '#Call to an undefined method Symfony\\Component\\Config\\Definition\\Builder\\NodeParentInterface::integerNode\(\).#'

View File

@@ -12,8 +12,11 @@ declare(strict_types=1);
namespace App\API;
use App\Entity\Activity;
use App\Entity\ActivityRate;
use App\Event\ActivityMetaDefinitionEvent;
use App\Form\API\ActivityApiEditForm;
use App\Form\API\ActivityRateApiForm;
use App\Repository\ActivityRateRepository;
use App\Repository\ActivityRepository;
use App\Repository\Query\ActivityQuery;
use App\Utils\SearchTerm;
@@ -32,6 +35,7 @@ use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
/**
* @RouteResource("Activity")
* @SWG\Tag(name="Activity")
*
* @Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")
*/
@@ -49,12 +53,17 @@ class ActivityController extends BaseApiController
* @var EventDispatcherInterface
*/
private $dispatcher;
/**
* @var ActivityRateRepository
*/
private $activityRateRepository;
public function __construct(ViewHandlerInterface $viewHandler, ActivityRepository $repository, EventDispatcherInterface $dispatcher)
public function __construct(ViewHandlerInterface $viewHandler, ActivityRepository $repository, EventDispatcherInterface $dispatcher, ActivityRateRepository $activityRateRepository)
{
$this->viewHandler = $viewHandler;
$this->repository = $repository;
$this->dispatcher = $dispatcher;
$this->activityRateRepository = $activityRateRepository;
}
/**
@@ -276,7 +285,7 @@ class ActivityController extends BaseApiController
}
/**
* Sets the value of a meta-field for an existing activity.
* Sets the value of a meta-field for an existing activity
*
* @SWG\Response(
* response=200,
@@ -327,4 +336,166 @@ class ActivityController extends BaseApiController
return $this->viewHandler->handle($view);
}
/**
* Returns a collection of all rates for one activity
*
* @SWG\Response(
* response=200,
* description="Returns a collection of activity rate entities",
* @SWG\Schema(
* type="array",
* @SWG\Items(ref="#/definitions/ActivityRate")
* )
* )
* @SWG\Parameter(
* name="id",
* in="path",
* type="integer",
* description="The activity whose rates will be returned",
* required=true,
* )
*
* @ApiSecurity(name="apiUser")
* @ApiSecurity(name="apiToken")
*/
public function getRatesAction(int $id): Response
{
/** @var Activity|null $activity */
$activity = $this->repository->find($id);
if (null === $activity) {
throw new NotFoundException();
}
if (!$this->isGranted('edit', $activity)) {
throw new AccessDeniedHttpException('Access denied.');
}
$rates = $this->activityRateRepository->getRatesForActivity($activity);
$view = new View($rates, 200);
$view->getContext()->setGroups(['Default', 'Entity', 'ActivityRate']);
return $this->viewHandler->handle($view);
}
/**
* Deletes one rate for an activity
*
* @SWG\Delete(
* @SWG\Response(
* response=204,
* description="Returns no content: 204 on successful delete"
* )
* )
* @SWG\Parameter(
* name="id",
* in="path",
* type="integer",
* description="The activity whose rate will be removed",
* required=true,
* )
* @SWG\Parameter(
* name="rateId",
* in="path",
* type="integer",
* description="The rate to remove",
* required=true,
* )
*
* @ApiSecurity(name="apiUser")
* @ApiSecurity(name="apiToken")
*/
public function deleteRateAction(string $id, string $rateId): Response
{
/** @var Activity|null $activity */
$activity = $this->repository->find($id);
if (null === $activity) {
throw new NotFoundException();
}
if (!$this->isGranted('edit', $activity)) {
throw new AccessDeniedHttpException('Access denied.');
}
/** @var ActivityRate|null $rate */
$rate = $this->activityRateRepository->find($rateId);
if (null === $rate || $rate->getActivity() !== $activity) {
throw new NotFoundException();
}
$this->activityRateRepository->deleteRate($rate);
$view = new View(null, Response::HTTP_NO_CONTENT);
return $this->viewHandler->handle($view);
}
/**
* Adds a new rate to an activity
*
* @SWG\Post(
* @SWG\Response(
* response=200,
* description="Returns the new created rate",
* @SWG\Schema(ref="#/definitions/ActivityRate")
* )
* )
* @SWG\Parameter(
* name="id",
* in="path",
* type="integer",
* description="The activity to add the rate for",
* required=true,
* )
* @SWG\Parameter(
* name="body",
* in="body",
* required=true,
* @SWG\Schema(ref="#/definitions/ActivityRateForm")
* )
*
* @ApiSecurity(name="apiUser")
* @ApiSecurity(name="apiToken")
*/
public function postRateAction(int $id, Request $request): Response
{
/** @var Activity|null $activity */
$activity = $this->repository->find($id);
if (null === $activity) {
throw new NotFoundException();
}
if (!$this->isGranted('edit', $activity)) {
throw new AccessDeniedHttpException('Access denied.');
}
$rate = new ActivityRate();
$rate->setActivity($activity);
$form = $this->createForm(ActivityRateApiForm::class, $rate, [
'method' => 'POST',
]);
$form->setData($rate);
$form->submit($request->request->all(), false);
if (false === $form->isValid()) {
$view = new View($form, Response::HTTP_OK);
$view->getContext()->setGroups(['Default', 'Entity', 'ActivityRate']);
return $this->viewHandler->handle($view);
}
$this->activityRateRepository->saveRate($rate);
$view = new View($rate, Response::HTTP_OK);
$view->getContext()->setGroups(['Default', 'Entity', 'ActivityRate']);
return $this->viewHandler->handle($view);
}
}

View File

@@ -25,6 +25,8 @@ use Swagger\Annotations as SWG;
use Symfony\Component\HttpFoundation\Response;
/**
* @SWG\Tag(name="Default")
*
* @Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")
*/
final class ConfigurationController extends BaseApiController

View File

@@ -12,9 +12,12 @@ declare(strict_types=1);
namespace App\API;
use App\Entity\Customer;
use App\Entity\CustomerRate;
use App\Entity\User;
use App\Event\CustomerMetaDefinitionEvent;
use App\Form\API\CustomerApiEditForm;
use App\Form\API\CustomerRateApiForm;
use App\Repository\CustomerRateRepository;
use App\Repository\CustomerRepository;
use App\Repository\Query\CustomerQuery;
use App\Utils\SearchTerm;
@@ -33,6 +36,7 @@ use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
/**
* @RouteResource("Customer")
* @SWG\Tag(name="Customer")
*
* @Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")
*/
@@ -50,12 +54,17 @@ class CustomerController extends BaseApiController
* @var EventDispatcherInterface
*/
private $dispatcher;
/**
* @var CustomerRateRepository
*/
private $customerRateRepository;
public function __construct(ViewHandlerInterface $viewHandler, CustomerRepository $repository, EventDispatcherInterface $dispatcher)
public function __construct(ViewHandlerInterface $viewHandler, CustomerRepository $repository, EventDispatcherInterface $dispatcher, CustomerRateRepository $customerRateRepository)
{
$this->viewHandler = $viewHandler;
$this->repository = $repository;
$this->dispatcher = $dispatcher;
$this->customerRateRepository = $customerRateRepository;
}
/**
@@ -66,7 +75,7 @@ class CustomerController extends BaseApiController
* description="Returns a collection of customer entities",
* @SWG\Schema(
* type="array",
* @SWG\Items(ref="#/definitions/CustomerCollection")
* @SWG\Items(ref="#/definitions/CustomerEntity")
* )
* )
* @Rest\QueryParam(name="visible", requirements="\d+", strict=true, nullable=true, description="Visibility status to filter activities (1=visible, 2=hidden, 3=both)")
@@ -249,7 +258,7 @@ class CustomerController extends BaseApiController
}
/**
* Sets the value of a meta-field for an existing customer.
* Sets the value of a meta-field for an existing customer
*
* @SWG\Response(
* response=200,
@@ -300,4 +309,166 @@ class CustomerController extends BaseApiController
return $this->viewHandler->handle($view);
}
/**
* Returns a collection of all rates for one customer
*
* @SWG\Response(
* response=200,
* description="Returns a collection of customer rate entities",
* @SWG\Schema(
* type="array",
* @SWG\Items(ref="#/definitions/CustomerRate")
* )
* )
* @SWG\Parameter(
* name="id",
* in="path",
* type="integer",
* description="The customer whose rates will be returned",
* required=true,
* )
*
* @ApiSecurity(name="apiUser")
* @ApiSecurity(name="apiToken")
*/
public function getRatesAction(int $id): Response
{
/** @var Customer|null $customer */
$customer = $this->repository->find($id);
if (null === $customer) {
throw new NotFoundException();
}
if (!$this->isGranted('edit', $customer)) {
throw new AccessDeniedHttpException('Access denied.');
}
$rates = $this->customerRateRepository->getRatesForCustomer($customer);
$view = new View($rates, 200);
$view->getContext()->setGroups(['Default', 'Entity', 'CustomerRate']);
return $this->viewHandler->handle($view);
}
/**
* Deletes one rate for an customer
*
* @SWG\Delete(
* @SWG\Response(
* response=204,
* description="Returns no content: 204 on successful delete"
* )
* )
* @SWG\Parameter(
* name="id",
* in="path",
* type="integer",
* description="The customer whose rate will be removed",
* required=true,
* )
* @SWG\Parameter(
* name="rateId",
* in="path",
* type="integer",
* description="The rate to remove",
* required=true,
* )
*
* @ApiSecurity(name="apiUser")
* @ApiSecurity(name="apiToken")
*/
public function deleteRateAction(string $id, string $rateId): Response
{
/** @var Customer|null $customer */
$customer = $this->repository->find($id);
if (null === $customer) {
throw new NotFoundException();
}
if (!$this->isGranted('edit', $customer)) {
throw new AccessDeniedHttpException('Access denied.');
}
/** @var CustomerRate|null $rate */
$rate = $this->customerRateRepository->find($rateId);
if (null === $rate || $rate->getCustomer() !== $customer) {
throw new NotFoundException();
}
$this->customerRateRepository->deleteRate($rate);
$view = new View(null, Response::HTTP_NO_CONTENT);
return $this->viewHandler->handle($view);
}
/**
* Adds a new rate to a customer
*
* @SWG\Post(
* @SWG\Response(
* response=200,
* description="Returns the new created rate",
* @SWG\Schema(ref="#/definitions/CustomerRate")
* )
* )
* @SWG\Parameter(
* name="id",
* in="path",
* type="integer",
* description="The customer to add the rate for",
* required=true,
* )
* @SWG\Parameter(
* name="body",
* in="body",
* required=true,
* @SWG\Schema(ref="#/definitions/CustomerRateForm")
* )
*
* @ApiSecurity(name="apiUser")
* @ApiSecurity(name="apiToken")
*/
public function postRateAction(int $id, Request $request): Response
{
/** @var Customer|null $customer */
$customer = $this->repository->find($id);
if (null === $customer) {
throw new NotFoundException();
}
if (!$this->isGranted('edit', $customer)) {
throw new AccessDeniedHttpException('Access denied.');
}
$rate = new CustomerRate();
$rate->setCustomer($customer);
$form = $this->createForm(CustomerRateApiForm::class, $rate, [
'method' => 'POST',
]);
$form->setData($rate);
$form->submit($request->request->all(), false);
if (false === $form->isValid()) {
$view = new View($form, Response::HTTP_OK);
$view->getContext()->setGroups(['Default', 'Entity', 'CustomerRate']);
return $this->viewHandler->handle($view);
}
$this->customerRateRepository->saveRate($rate);
$view = new View($rate, Response::HTTP_OK);
$view->getContext()->setGroups(['Default', 'Entity', 'CustomerRate']);
return $this->viewHandler->handle($view);
}
}

View File

@@ -11,10 +11,14 @@ declare(strict_types=1);
namespace App\API;
use App\Entity\Customer;
use App\Entity\Project;
use App\Entity\ProjectRate;
use App\Entity\User;
use App\Event\ProjectMetaDefinitionEvent;
use App\Form\API\ProjectApiEditForm;
use App\Form\API\ProjectRateApiForm;
use App\Repository\ProjectRateRepository;
use App\Repository\ProjectRepository;
use App\Repository\Query\ProjectQuery;
use App\Timesheet\UserDateTimeFactory;
@@ -35,6 +39,7 @@ use Symfony\Component\Validator\Constraints;
/**
* @RouteResource("Project")
* @SWG\Tag(name="Project")
*
* @Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")
*/
@@ -56,13 +61,18 @@ class ProjectController extends BaseApiController
* @var UserDateTimeFactory
*/
private $dateTime;
/**
* @var ProjectRateRepository
*/
private $projectRateRepository;
public function __construct(ViewHandlerInterface $viewHandler, ProjectRepository $repository, EventDispatcherInterface $dispatcher, UserDateTimeFactory $dateTime)
public function __construct(ViewHandlerInterface $viewHandler, ProjectRepository $repository, EventDispatcherInterface $dispatcher, UserDateTimeFactory $dateTime, ProjectRateRepository $projectRateRepository)
{
$this->viewHandler = $viewHandler;
$this->repository = $repository;
$this->dispatcher = $dispatcher;
$this->dateTime = $dateTime;
$this->projectRateRepository = $projectRateRepository;
}
/**
@@ -299,7 +309,7 @@ class ProjectController extends BaseApiController
}
/**
* Sets the value of a meta-field for an existing project.
* Sets the value of a meta-field for an existing project
*
* @SWG\Response(
* response=200,
@@ -350,4 +360,166 @@ class ProjectController extends BaseApiController
return $this->viewHandler->handle($view);
}
/**
* Returns a collection of all rates for one project
*
* @SWG\Response(
* response=200,
* description="Returns a collection of project rate entities",
* @SWG\Schema(
* type="array",
* @SWG\Items(ref="#/definitions/ProjectRate")
* )
* )
* @SWG\Parameter(
* name="id",
* in="path",
* type="integer",
* description="The project whose rates will be returned",
* required=true,
* )
*
* @ApiSecurity(name="apiUser")
* @ApiSecurity(name="apiToken")
*/
public function getRatesAction(int $id): Response
{
/** @var Project|null $project */
$project = $this->repository->find($id);
if (null === $project) {
throw new NotFoundException();
}
if (!$this->isGranted('edit', $project)) {
throw new AccessDeniedHttpException('Access denied.');
}
$rates = $this->projectRateRepository->getRatesForProject($project);
$view = new View($rates, 200);
$view->getContext()->setGroups(['Default', 'Entity', 'ProjectRate']);
return $this->viewHandler->handle($view);
}
/**
* Deletes one rate for an project
*
* @SWG\Delete(
* @SWG\Response(
* response=204,
* description="Returns no content: 204 on successful delete"
* )
* )
* @SWG\Parameter(
* name="id",
* in="path",
* type="integer",
* description="The project whose rate will be removed",
* required=true,
* )
* @SWG\Parameter(
* name="rateId",
* in="path",
* type="integer",
* description="The rate to remove",
* required=true,
* )
*
* @ApiSecurity(name="apiUser")
* @ApiSecurity(name="apiToken")
*/
public function deleteRateAction(string $id, string $rateId): Response
{
/** @var Project|null $project */
$project = $this->repository->find($id);
if (null === $project) {
throw new NotFoundException();
}
if (!$this->isGranted('edit', $project)) {
throw new AccessDeniedHttpException('Access denied.');
}
/** @var ProjectRate|null $rate */
$rate = $this->projectRateRepository->find($rateId);
if (null === $rate || $rate->getProject() !== $project) {
throw new NotFoundException();
}
$this->projectRateRepository->deleteRate($rate);
$view = new View(null, Response::HTTP_NO_CONTENT);
return $this->viewHandler->handle($view);
}
/**
* Adds a new rate to an project
*
* @SWG\Post(
* @SWG\Response(
* response=200,
* description="Returns the new created rate",
* @SWG\Schema(ref="#/definitions/ProjectRate")
* )
* )
* @SWG\Parameter(
* name="id",
* in="path",
* type="integer",
* description="The project to add the rate for",
* required=true,
* )
* @SWG\Parameter(
* name="body",
* in="body",
* required=true,
* @SWG\Schema(ref="#/definitions/ProjectRateForm")
* )
*
* @ApiSecurity(name="apiUser")
* @ApiSecurity(name="apiToken")
*/
public function postRateAction(int $id, Request $request): Response
{
/** @var Project|null $project */
$project = $this->repository->find($id);
if (null === $project) {
throw new NotFoundException();
}
if (!$this->isGranted('edit', $project)) {
throw new AccessDeniedHttpException('Access denied.');
}
$rate = new ProjectRate();
$rate->setProject($project);
$form = $this->createForm(ProjectRateApiForm::class, $rate, [
'method' => 'POST',
]);
$form->setData($rate);
$form->submit($request->request->all(), false);
if (false === $form->isValid()) {
$view = new View($form, Response::HTTP_OK);
$view->getContext()->setGroups(['Default', 'Entity', 'ProjectRate']);
return $this->viewHandler->handle($view);
}
$this->projectRateRepository->saveRate($rate);
$view = new View($rate, Response::HTTP_OK);
$view->getContext()->setGroups(['Default', 'Entity', 'ProjectRate']);
return $this->viewHandler->handle($view);
}
}

View File

@@ -20,6 +20,9 @@ use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
use Swagger\Annotations as SWG;
use Symfony\Component\HttpFoundation\Response;
/**
* @SWG\Tag(name="Default")
*/
class StatusController extends BaseApiController
{
/**

View File

@@ -28,6 +28,7 @@ use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
/**
* @RouteResource("Tag")
* @SWG\Tag(name="Tag")
*
* @Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")
*/

View File

@@ -33,6 +33,7 @@ use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
/**
* @RouteResource("Team")
* @SWG\Tag(name="Team")
*
* @Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")
*/

View File

@@ -45,6 +45,7 @@ use Symfony\Component\Validator\Validator\ValidatorInterface;
/**
* @RouteResource("Timesheet")
* @SWG\Tag(name="Timesheet")
*
* @Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")
*/
@@ -169,8 +170,9 @@ class TimesheetController extends BaseApiController
if (!empty($customers)) {
$query->setCustomers($customers);
}
} elseif (!empty($customer = $paramFetcher->get('customer'))) {
@trigger_error('Timesheet API parameter "customer" is deprecated and will be removed with 2.0, use "customers" instead', E_USER_DEPRECATED);
}
if (!empty($customer = $paramFetcher->get('customer'))) {
$query->addCustomer($customer);
}
@@ -181,8 +183,9 @@ class TimesheetController extends BaseApiController
if (!empty($projects)) {
$query->setProjects($projects);
}
} elseif (!empty($project = $paramFetcher->get('project'))) {
@trigger_error('Timesheet API parameter "project" is deprecated and will be removed with 2.0, use "projects" instead', E_USER_DEPRECATED);
}
if (!empty($project = $paramFetcher->get('project'))) {
$query->addProject($project);
}
@@ -193,8 +196,9 @@ class TimesheetController extends BaseApiController
if (!empty($activities)) {
$query->setActivities($activities);
}
} elseif (!empty($activity = $paramFetcher->get('activity'))) {
@trigger_error('Timesheet API parameter "activity" is deprecated and will be removed with 2.0, use "activities" instead', E_USER_DEPRECATED);
}
if (!empty($activity = $paramFetcher->get('activity'))) {
$query->addActivity($activity);
}

View File

@@ -33,6 +33,7 @@ use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
/**
* @RouteResource("User")
* @SWG\Tag(name="User")
*
* @Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")
*/

View File

@@ -125,7 +125,8 @@ class CreateReleaseCommand extends Command
'Dockerfile',
'phpunit.xml.dist',
'webpack.config.js',
'assets/',
// this seems to be required, see https://github.com/kevinpapst/kimai2/issues/1586
//'assets/',
'tests/',
'var/cache/*',
'var/data/kimai_test.sqlite',

View File

@@ -18,6 +18,7 @@ use Symfony\Contracts\Translation\TranslatorInterface;
/**
* The abstract base controller.
* @method null|User getUser()
*/
abstract class AbstractController extends BaseAbstractController implements ServiceSubscriberInterface
{
@@ -42,14 +43,6 @@ abstract class AbstractController extends BaseAbstractController implements Serv
return $this->container->get('logger');
}
/**
* @return User|null
*/
protected function getUser()
{
return parent::getUser();
}
/**
* Adds a "successful" flash message to the stack.
*

View File

@@ -134,25 +134,6 @@ final class ActivityController extends AbstractController
]);
}
/**
* @Route(path="/{id}/rate_delete/{rate}", name="admin_activity_rate_delete", methods={"GET"})
* @Security("is_granted('edit', activity)")
*/
public function deleteRateAction(Activity $activity, ActivityRate $rate, ActivityRateRepository $repository)
{
if ($rate->getActivity() !== $activity) {
$this->flashError('action.delete.error', ['%reason%' => 'Invalid activity']);
} else {
try {
$repository->deleteRate($rate);
} catch (\Exception $ex) {
$this->flashError('action.delete.error', ['%reason%' => $ex->getMessage()]);
}
}
return $this->redirectToRoute('activity_details', ['id' => $activity->getId()]);
}
/**
* @Route(path="/{id}/rate", name="admin_activity_rate_add", methods={"GET", "POST"})
* @Security("is_granted('edit', activity)")

View File

@@ -315,25 +315,6 @@ final class CustomerController extends AbstractController
]);
}
/**
* @Route(path="/{id}/rate_delete/{rate}", name="admin_customer_rate_delete", methods={"GET"})
* @Security("is_granted('edit', customer)")
*/
public function deleteRateAction(Customer $customer, CustomerRate $rate, CustomerRateRepository $repository)
{
if ($rate->getCustomer() !== $customer) {
$this->flashError('action.delete.error', ['%reason%' => 'Invalid customer']);
} else {
try {
$repository->deleteRate($rate);
} catch (\Exception $ex) {
$this->flashError('action.delete.error', ['%reason%' => $ex->getMessage()]);
}
}
return $this->redirectToRoute('customer_details', ['id' => $customer->getId()]);
}
/**
* @Route(path="/{id}/rate", name="admin_customer_rate_add", methods={"GET", "POST"})
* @Security("is_granted('edit', customer)")

View File

@@ -58,7 +58,6 @@ class ProfileController extends AbstractController
}
/**
* @Route(path="/", name="fos_user_profile_show", methods={"GET"})
* @Route(path="/", name="my_profile", methods={"GET"})
*/
public function profileAction()

View File

@@ -314,25 +314,6 @@ final class ProjectController extends AbstractController
]);
}
/**
* @Route(path="/{id}/rate_delete/{rate}", name="admin_project_rate_delete", methods={"GET"})
* @Security("is_granted('edit', project)")
*/
public function deleteRateAction(Project $project, ProjectRate $rate, ProjectRateRepository $repository)
{
if ($rate->getProject() !== $project) {
$this->flashError('action.delete.error', ['%reason%' => 'Invalid project']);
} else {
try {
$repository->deleteRate($rate);
} catch (\Exception $ex) {
$this->flashError('action.delete.error', ['%reason%' => $ex->getMessage()]);
}
}
return $this->redirectToRoute('project_details', ['id' => $project->getId()]);
}
/**
* @Route(path="/{id}/rate", name="admin_project_rate_add", methods={"GET", "POST"})
* @Security("is_granted('edit', project)")

View File

@@ -38,7 +38,6 @@ class Activity implements EntityWithMetaFields
* @ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* @var Project|null
*
@@ -46,25 +45,20 @@ class Activity implements EntityWithMetaFields
* @ORM\JoinColumn(onDelete="CASCADE")
*/
private $project;
/**
* @var string
*
* Do not increase length to more than 190 chars, otherwise "Index column size too large." will be triggered.
*
* @ORM\Column(name="name", type="string", length=150, nullable=false)
* @Assert\NotBlank()
* @Assert\Length(min=2, max=150)
* @Assert\Length(min=2, max=150, allowEmptyString=false)
*/
private $name;
/**
* @var string
*
* @ORM\Column(name="comment", type="text", nullable=true)
*/
private $comment;
/**
* @var bool
*

View File

@@ -37,7 +37,8 @@ class Configuration
* @var string
*
* @ORM\Column(name="name", type="string", length=100, nullable=false)
* @Assert\Length(min=2, max=100)
* @Assert\NotNull()
* @Assert\Length(min=2, max=100, allowEmptyString=false)
*/
private $name;

View File

@@ -36,18 +36,14 @@ class Customer implements EntityWithMetaFields
* @ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* @var string
*
* Do not increase length to more than 190 chars, otherwise "Index column size too large." will be triggered.
*
* @ORM\Column(name="name", type="string", length=150, nullable=false)
* @Assert\NotBlank()
* @Assert\Length(min=2, max=150)
* @Assert\Length(min=2, max=150, allowEmptyString=false)
*/
private $name;
/**
* @var string
*
@@ -55,14 +51,12 @@ class Customer implements EntityWithMetaFields
* @Assert\Length(max=50)
*/
private $number;
/**
* @var string
*
* @ORM\Column(name="comment", type="text", nullable=true)
*/
private $comment;
/**
* @var bool
*
@@ -70,7 +64,6 @@ class Customer implements EntityWithMetaFields
* @Assert\NotNull()
*/
private $visible = true;
/**
* @var string
*
@@ -78,7 +71,6 @@ class Customer implements EntityWithMetaFields
* @Assert\Length(max=255)
*/
private $company;
/**
* @var string
*
@@ -86,7 +78,6 @@ class Customer implements EntityWithMetaFields
* @Assert\Length(max=50)
*/
private $vatId;
/**
* @var string
*
@@ -94,14 +85,12 @@ class Customer implements EntityWithMetaFields
* @Assert\Length(max=255)
*/
private $contact;
/**
* @var string
*
* @ORM\Column(name="address", type="text", nullable=true)
*/
private $address;
/**
* @var string
*
@@ -110,7 +99,6 @@ class Customer implements EntityWithMetaFields
* @Assert\Length(max=2)
*/
private $country;
/**
* @var string
*
@@ -119,7 +107,6 @@ class Customer implements EntityWithMetaFields
* @Assert\Length(max=3)
*/
private $currency = self::DEFAULT_CURRENCY;
/**
* @var string
*
@@ -127,7 +114,6 @@ class Customer implements EntityWithMetaFields
* @Assert\Length(max=255)
*/
private $phone;
/**
* @var string
*
@@ -135,7 +121,6 @@ class Customer implements EntityWithMetaFields
* @Assert\Length(max=255)
*/
private $fax;
/**
* @var string
*
@@ -143,7 +128,6 @@ class Customer implements EntityWithMetaFields
* @Assert\Length(max=255)
*/
private $mobile;
/**
* @var string
*
@@ -153,7 +137,6 @@ class Customer implements EntityWithMetaFields
* @Assert\Length(max=254)
*/
private $email;
/**
* @var string
*
@@ -161,7 +144,6 @@ class Customer implements EntityWithMetaFields
* @Assert\Length(max=255)
*/
private $homepage;
/**
* @var string
*
@@ -183,7 +165,6 @@ class Customer implements EntityWithMetaFields
* @ORM\OneToMany(targetEntity="App\Entity\CustomerMeta", mappedBy="customer", cascade={"persist"})
*/
private $meta;
/**
* @var Team[]|ArrayCollection
*

View File

@@ -36,7 +36,7 @@ class InvoiceTemplate
*
* @ORM\Column(name="name", type="string", length=60, nullable=false)
* @Assert\NotBlank()
* @Assert\Length(min=1, max=60)
* @Assert\Length(min=1, max=60, allowEmptyString=false)
*/
private $name;

View File

@@ -31,7 +31,8 @@ trait MetaTableTypeTrait
* @var string
*
* @ORM\Column(name="name", type="string", length=50, nullable=false)
* @Assert\Length(min=2, max=50)
* @Assert\NotNull()
* @Assert\Length(min=2, max=50, allowEmptyString=false)
*/
private $name;

View File

@@ -48,11 +48,9 @@ class Project implements EntityWithMetaFields
/**
* @var string
*
* Do not increase length to more than 190 chars, otherwise "Index column size too large." will be triggered.
*
* @ORM\Column(name="name", type="string", length=150, nullable=false)
* @Assert\NotNull()
* @Assert\Length(min=2, max=150)
* @Assert\Length(min=2, max=150, allowEmptyString=false)
*/
private $name;
/**

View File

@@ -10,6 +10,7 @@
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Swagger\Annotations as SWG;
use Symfony\Component\Validator\Constraints as Assert;
trait Rate
@@ -27,6 +28,7 @@ trait Rate
*
* @ORM\ManyToOne(targetEntity="App\Entity\User")
* @ORM\JoinColumn(onDelete="CASCADE", nullable=true)
* @SWG\Property(ref="#/definitions/User")
*/
private $user;
/**
@@ -36,6 +38,12 @@ trait Rate
* @Assert\GreaterThanOrEqual(0)
*/
private $rate = 0.00;
/**
* @var float|null
*
* @ORM\Column(name="internal_rate", type="float", nullable=true)
*/
private $internalRate;
/**
* @var bool
*
@@ -78,6 +86,18 @@ trait Rate
return $this->rate;
}
public function setInternalRate(?float $rate): self
{
$this->internalRate = $rate;
return $this;
}
public function getInternalRate(): ?float
{
return $this->internalRate;
}
public function isFixed(): bool
{
return $this->isFixed;

View File

@@ -9,12 +9,17 @@
namespace App\Entity;
/**
* @internal
*/
interface RateInterface
{
public function getUser(): ?User;
public function getRate(): float;
public function getInternalRate(): ?float;
public function isFixed(): bool;
public function getScore(): int;

View File

@@ -39,7 +39,7 @@ class Tag
*
* @ORM\Column(name="name", type="string", length=100, nullable=false)
* @Assert\NotBlank()
* @Assert\Length(min=2, max=100)
* @Assert\Length(min=2, max=100, allowEmptyString=false)
* @Assert\Regex(pattern="/,/",match=false,message="Tag name cannot contain comma")
*/
private $name;

View File

@@ -39,7 +39,7 @@ class Team
*
* @ORM\Column(name="name", type="string", length=100, nullable=false)
* @Assert\NotBlank()
* @Assert\Length(min=2, max=100)
* @Assert\Length(min=2, max=100, allowEmptyString=false)
*/
private $name;
/**

View File

@@ -131,7 +131,14 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface
private $rate = 0.00;
/**
* @var float
* @var float|null
*
* @ORM\Column(name="internal_rate", type="float", nullable=true)
*/
private $internalRate;
/**
* @var float|null
*
* @ORM\Column(name="fixed_rate", type="float", nullable=true)
* @Assert\GreaterThanOrEqual(0)
@@ -363,6 +370,18 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface
return $this->rate;
}
public function setInternalRate(?float $rate): Timesheet
{
$this->internalRate = $rate;
return $this;
}
public function getInternalRate(): ?float
{
return $this->internalRate;
}
/**
* @param Tag $tag
* @return Timesheet

View File

@@ -27,6 +27,7 @@ use Symfony\Component\Validator\Constraints as Assert;
class UserPreference
{
public const HOURLY_RATE = 'hourly_rate';
public const INTERNAL_RATE = 'internal_rate';
public const SKIN = 'skin';
public const LOCALE = 'language';
public const TIMEZONE = 'timezone';
@@ -51,7 +52,8 @@ class UserPreference
* @var string
*
* @ORM\Column(name="name", type="string", length=50, nullable=false)
* @Assert\Length(min=2, max=50)
* @Assert\NotNull()
* @Assert\Length(min=2, max=50, allowEmptyString=false)
*/
private $name;
/**

View File

@@ -67,7 +67,7 @@ final class UserPreferenceEvent extends Event
foreach ($this->preferences as $pref) {
if (strtolower($pref->getName()) === strtolower($preference->getName())) {
throw new \InvalidArgumentException(
'Cannot add preference, one with the name "' . $preference->getName() . '" is already existing'
'Cannot add user preference, one with the name "' . $preference->getName() . '" is already existing'
);
}
}

View File

@@ -14,6 +14,8 @@ use FOS\UserBundle\Event\FormEvent;
use FOS\UserBundle\FOSUserEvents;
use FOS\UserBundle\Model\UserManagerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
/**
* This class intercepts the registration to make sure:
@@ -21,19 +23,21 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
* - the first-ever registered user will get the SUPER_ADMIN role
* - the user uses the current request locale as initial language setting
*/
class RegistrationSubscriber implements EventSubscriberInterface
final class RegistrationSubscriber implements EventSubscriberInterface
{
/**
* @var UserManagerInterface
*/
protected $userManager;
private $userManager;
/**
* @param UserManagerInterface $userManager
* @var UrlGeneratorInterface
*/
public function __construct(UserManagerInterface $userManager)
private $router;
public function __construct(UserManagerInterface $userManager, UrlGeneratorInterface $router)
{
$this->userManager = $userManager;
$this->router = $router;
}
/**
@@ -42,7 +46,8 @@ class RegistrationSubscriber implements EventSubscriberInterface
public static function getSubscribedEvents(): array
{
return [
FOSUserEvents::REGISTRATION_SUCCESS => ['onRegistrationSuccess', 200]
FOSUserEvents::REGISTRATION_SUCCESS => ['onRegistrationSuccess', 200],
FOSUserEvents::RESETTING_RESET_SUCCESS => ['onResettingSuccess', 200],
];
}
@@ -62,4 +67,12 @@ class RegistrationSubscriber implements EventSubscriberInterface
$user->setLanguage($event->getRequest()->getLocale());
$user->setRoles($roles);
}
/**
* @param FormEvent $event
*/
public function onResettingSuccess(FormEvent $event)
{
$event->setResponse(new RedirectResponse($this->router->generate('my_profile')));
}
}

View File

@@ -111,6 +111,15 @@ class UserPreferenceSubscriber implements EventSubscriberInterface
->setOptions($hourlyRateOptions)
->addConstraint(new Range(['min' => 0])),
(new UserPreference())
->setName(UserPreference::INTERNAL_RATE)
->setValue(null)
->setOrder(101)
->setType(MoneyType::class)
->setEnabled($enableHourlyRate)
->setOptions(array_merge($hourlyRateOptions, ['label' => 'label.rate_internal', 'required' => false]))
->addConstraint(new Range(['min' => 0])),
(new UserPreference())
->setName(UserPreference::TIMEZONE)
->setValue($this->getDefaultTimezone())

View File

@@ -70,6 +70,7 @@ abstract class AbstractSpreadsheetRenderer
'end' => [],
'duration' => [],
'rate' => [],
'rate_internal' => [],
'user' => [],
'customer' => [],
'project' => [],
@@ -226,6 +227,20 @@ abstract class AbstractSpreadsheetRenderer
};
}
if ($showRates && isset($columns['rate_internal']) && !isset($columns['rate_internal']['render'])) {
$columns['rate_internal']['render'] = function (Worksheet $sheet, int $row, int $column, ExportItemInterface $entity) {
$currency = '';
if (null !== $entity->getProject()) {
$currency = $entity->getProject()->getCustomer()->getCurrency();
}
$rate = $entity->getRate();
if (method_exists($entity, 'getInternalRate')) {
$rate = $entity->getInternalRate();
}
$this->setRate($sheet, $column, $row, $rate, $currency);
};
}
if (isset($columns['user']) && !isset($columns['user']['render'])) {
$columns['user']['render'] = function (Worksheet $sheet, int $row, int $column, ExportItemInterface $entity) {
$user = '';
@@ -468,7 +483,7 @@ abstract class AbstractSpreadsheetRenderer
}
if (!$showRates) {
$removes = ['rate', 'fixedRate', 'hourlyRate'];
$removes = ['rate', 'fixedRate', 'hourlyRate', 'rate_internal'];
foreach ($removes as $removeMe) {
if (array_key_exists($removeMe, $columns)) {
unset($columns[$removeMe]);
@@ -514,6 +529,7 @@ abstract class AbstractSpreadsheetRenderer
$durationColumn = null;
$rateColumn = null;
$internalRateColumn = null;
foreach ($exportItems as $exportItem) {
$entryHeaderColumn = 1;
@@ -523,6 +539,8 @@ abstract class AbstractSpreadsheetRenderer
$durationColumn = $entryHeaderColumn;
} elseif ($label === 'rate') {
$rateColumn = $entryHeaderColumn;
} elseif ($label === 'rate_internal') {
$internalRateColumn = $entryHeaderColumn;
}
if (!array_key_exists('render', $settings) || !is_callable($settings['render'])) {
@@ -554,6 +572,15 @@ abstract class AbstractSpreadsheetRenderer
$style->getFont()->setBold(true);
}
if (null !== $internalRateColumn) {
$startCoordinate = $sheet->getCellByColumnAndRow($internalRateColumn, 2)->getCoordinate();
$endCoordinate = $sheet->getCellByColumnAndRow($internalRateColumn, $entryHeaderRow - 1)->getCoordinate();
$this->setRateTotal($sheet, $internalRateColumn, $entryHeaderRow, $startCoordinate, $endCoordinate);
$style = $sheet->getStyleByColumnAndRow($internalRateColumn, $entryHeaderRow);
$style->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
$style->getFont()->setBold(true);
}
return $spreadsheet;
}

View File

@@ -54,6 +54,7 @@ trait RendererTrait
'activities' => [],
'currency' => $currency,
'rate' => 0,
'rate_internal' => 0,
'duration' => 0,
];
}
@@ -63,6 +64,7 @@ trait RendererTrait
'activity' => $activityName,
'currency' => $currency,
'rate' => 0,
'rate_internal' => 0,
'duration' => 0,
];
}
@@ -73,6 +75,11 @@ trait RendererTrait
}
$summary[$id]['rate'] += $exportItem->getRate();
if (method_exists($exportItem, 'getInternalRate')) {
$summary[$id]['rate_internal'] += $exportItem->getInternalRate();
} else {
$summary[$id]['rate_internal'] += $exportItem->getRate();
}
$summary[$id]['duration'] += $duration;
$summary[$id]['activities'][$activityId]['rate'] += $exportItem->getRate();
$summary[$id]['activities'][$activityId]['duration'] += $duration;

View File

@@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Form\API;
use App\Form\ActivityRateForm;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ActivityRateApiForm extends ActivityRateForm
{
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'csrf_protection' => false,
]);
}
}

View File

@@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Form\API;
use App\Form\CustomerRateForm;
use Symfony\Component\OptionsResolver\OptionsResolver;
class CustomerRateApiForm extends CustomerRateForm
{
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'csrf_protection' => false,
]);
}
}

View File

@@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Form\API;
use App\Form\ProjectRateForm;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ProjectRateApiForm extends ProjectRateForm
{
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'csrf_protection' => false,
]);
}
}

View File

@@ -0,0 +1,56 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Form;
use App\Form\Type\UserType;
use App\Form\Type\YesNoType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
use Symfony\Component\Form\FormBuilderInterface;
abstract class AbstractRateForm extends AbstractType
{
protected function addFields(FormBuilderInterface $builder, ?string $currency)
{
$builder
->add('user', UserType::class, [
'required' => false,
])
->add('rate', MoneyType::class, [
// documentation is for NelmioApiDocBundle
'documentation' => [
'type' => 'number',
'description' => 'Rate',
],
'label' => 'label.rate',
'attr' => [
'autofocus' => 'autofocus'
],
'currency' => $currency,
'help' => 'help.rate',
])
->add('internalRate', MoneyType::class, [
// documentation is for NelmioApiDocBundle
'documentation' => [
'type' => 'number',
'description' => 'Internal rate',
],
'label' => 'label.rate_internal',
'currency' => $currency,
'required' => false,
'help' => 'help.rate_internal',
])
->add('isFixed', YesNoType::class, [
'label' => 'label.fixedRate',
'help' => 'help.fixedRate',
])
;
}
}

View File

@@ -11,14 +11,10 @@ namespace App\Form;
use App\Entity\ActivityRate;
use App\Entity\Rate;
use App\Form\Type\UserType;
use App\Form\Type\YesNoType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ActivityRateForm extends AbstractType
class ActivityRateForm extends AbstractRateForm
{
/**
* {@inheritdoc}
@@ -27,7 +23,7 @@ class ActivityRateForm extends AbstractType
{
$currency = null;
if ($options['data']) {
if (!empty($options['data'])) {
/** @var ActivityRate $rate */
$rate = $options['data'];
@@ -36,21 +32,7 @@ class ActivityRateForm extends AbstractType
}
}
$builder
->add('user', UserType::class, [
'required' => false,
])
->add('rate', MoneyType::class, [
'label' => 'label.rate',
'attr' => [
'autofocus' => 'autofocus'
],
'currency' => $currency,
])
->add('isFixed', YesNoType::class, [
'label' => 'label.fixedRate'
])
;
$this->addFields($builder, $currency);
}
/**

View File

@@ -11,14 +11,10 @@ namespace App\Form;
use App\Entity\CustomerRate;
use App\Entity\Rate;
use App\Form\Type\UserType;
use App\Form\Type\YesNoType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class CustomerRateForm extends AbstractType
class CustomerRateForm extends AbstractRateForm
{
/**
* {@inheritdoc}
@@ -26,7 +22,8 @@ class CustomerRateForm extends AbstractType
public function buildForm(FormBuilderInterface $builder, array $options)
{
$currency = null;
if ($options['data']) {
if (!empty($options['data'])) {
/** @var CustomerRate $rate */
$rate = $options['data'];
@@ -35,21 +32,7 @@ class CustomerRateForm extends AbstractType
}
}
$builder
->add('user', UserType::class, [
'required' => false,
])
->add('rate', MoneyType::class, [
'label' => 'label.rate',
'attr' => [
'autofocus' => 'autofocus'
],
'currency' => $currency,
])
->add('isFixed', YesNoType::class, [
'label' => 'label.fixedRate'
])
;
$this->addFields($builder, $currency);
}
/**

View File

@@ -11,14 +11,10 @@ namespace App\Form;
use App\Entity\ProjectRate;
use App\Entity\Rate;
use App\Form\Type\UserType;
use App\Form\Type\YesNoType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ProjectRateForm extends AbstractType
class ProjectRateForm extends AbstractRateForm
{
/**
* {@inheritdoc}
@@ -26,7 +22,8 @@ class ProjectRateForm extends AbstractType
public function buildForm(FormBuilderInterface $builder, array $options)
{
$currency = null;
if ($options['data']) {
if (!empty($options['data'])) {
/** @var ProjectRate $rate */
$rate = $options['data'];
@@ -35,21 +32,7 @@ class ProjectRateForm extends AbstractType
}
}
$builder
->add('user', UserType::class, [
'required' => false,
])
->add('rate', MoneyType::class, [
'label' => 'label.rate',
'attr' => [
'autofocus' => 'autofocus'
],
'currency' => $currency,
])
->add('isFixed', YesNoType::class, [
'label' => 'label.fixedRate'
])
;
$this->addFields($builder, $currency);
}
/**

View File

@@ -57,6 +57,11 @@ abstract class AbstractMergedCalculator extends AbstractCalculator
$invoiceItem->setAmount($invoiceItem->getAmount() + $amount);
$invoiceItem->setUser($entry->getUser());
$invoiceItem->setRate($invoiceItem->getRate() + $entry->getRate());
if (method_exists($entry, 'getInternalRate')) {
$invoiceItem->setInternalRate($invoiceItem->getInternalRate() + $entry->getInternalRate());
} else {
$invoiceItem->setInternalRate($invoiceItem->getInternalRate() + $entry->getRate());
}
$invoiceItem->setDuration($duration);
if (null !== $entry->getFixedRate()) {

View File

@@ -53,17 +53,11 @@ abstract class AbstractSumInvoiceCalculator extends AbstractMergedCalculator imp
return array_values($invoiceItems);
}
/**
* @deprecated since 1.9 - use mergeSumInvoiceItem() instead
*/
protected function mergeSumTimesheet(InvoiceItem $invoiceItem, InvoiceItemInterface $entry)
{
}
protected function mergeSumInvoiceItem(InvoiceItem $invoiceItem, InvoiceItemInterface $entry)
{
@trigger_error('mergeSumTimesheet() is deprecated and will be removed with 2.0', E_USER_DEPRECATED);
$this->mergeSumTimesheet($invoiceItem, $entry);
if (method_exists($this, 'mergeSumTimesheet')) {
@trigger_error('mergeSumTimesheet() is deprecated and will be removed with 2.0 - use mergeSumInvoiceItem() instead', E_USER_DEPRECATED);
$this->mergeSumTimesheet($invoiceItem, $entry);
}
}
}

View File

@@ -30,6 +30,7 @@ class InvoiceItemDefaultHydrator implements InvoiceItemHydrator
$formatter = $this->model->getFormatter();
$rate = $item->getRate();
$internalRate = $item->getInternalRate();
$appliedRate = $item->getHourlyRate();
$amount = $formatter->getFormattedDuration($item->getDuration());
$description = $item->getDescription();
@@ -65,6 +66,9 @@ class InvoiceItemDefaultHydrator implements InvoiceItemHydrator
'entry.rate' => $formatter->getFormattedMoney($appliedRate, $currency),
'entry.rate_nc' => $formatter->getFormattedMoney($appliedRate, null),
'entry.rate_plain' => $appliedRate,
'entry.rate_internal' => $formatter->getFormattedMoney($internalRate, $currency),
'entry.rate_internal_nc' => $formatter->getFormattedMoney($internalRate, null),
'entry.rate_internal_plain' => $internalRate,
'entry.total' => $formatter->getFormattedMoney($rate, $currency),
'entry.total_nc' => $formatter->getFormattedMoney($rate, null),
'entry.total_plain' => $rate,

View File

@@ -30,6 +30,10 @@ final class InvoiceItem
* @var float
*/
private $rate = 0.00;
/**
* @var float
*/
private $rateInternal = 0.00;
/**
* @var float
*/
@@ -152,6 +156,18 @@ final class InvoiceItem
return $this;
}
public function getInternalRate(): float
{
return $this->rateInternal;
}
public function setInternalRate(float $rateInternal): InvoiceItem
{
$this->rateInternal = $rateInternal;
return $this;
}
public function getAmount(): float
{
return $this->amount;

View File

@@ -14,6 +14,9 @@ use App\Entity\MetaTableTypeInterface;
use App\Entity\Project;
use App\Entity\User;
/**
* @method float|null getInternalRate()
*/
interface InvoiceItemInterface
{
public function getActivity(): ?Activity;
@@ -26,6 +29,11 @@ interface InvoiceItemInterface
public function getRate(): float;
// will be activated with 2.0
/*
public function getInternalRate(): ?float;
*/
public function getUser(): ?User;
public function getBegin(): ?\DateTime;

View File

@@ -25,14 +25,14 @@ final class ConfigurableNumberGenerator implements NumberGeneratorInterface
*/
private $repository;
/**
* @var string
* @var SystemConfiguration
*/
private $format;
private $configuration;
public function __construct(InvoiceRepository $repository, SystemConfiguration $configuration)
{
$this->repository = $repository;
$this->format = $configuration->find('invoice.number_format');
$this->configuration = $configuration;
}
/**
@@ -56,9 +56,8 @@ final class ConfigurableNumberGenerator implements NumberGeneratorInterface
*/
public function getInvoiceNumber(): string
{
$format = $this->format;
$format = $this->configuration->find('invoice.number_format');
$invoiceDate = $this->model->getInvoiceDate();
$timestamp = $invoiceDate->getTimestamp();
$result = $format;
preg_match_all('/{[^}]*?}/', $format, $matches);

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace DoctrineMigrations;
use App\Doctrine\AbstractMigration;
use Doctrine\DBAL\Schema\Schema;
/**
* @version 1.9
*/
final class Version20200323163038 extends AbstractMigration
{
public function getDescription(): string
{
return 'Adds the internal_rate column to all rate tables';
}
public function up(Schema $schema): void
{
$schema->getTable('kimai2_activities_rates')->addColumn('internal_rate', 'float', ['notnull' => false]);
$schema->getTable('kimai2_projects_rates')->addColumn('internal_rate', 'float', ['notnull' => false]);
$schema->getTable('kimai2_customers_rates')->addColumn('internal_rate', 'float', ['notnull' => false]);
$schema->getTable('kimai2_timesheet')->addColumn('internal_rate', 'float', ['notnull' => false]);
}
public function down(Schema $schema): void
{
$schema->getTable('kimai2_timesheet')->dropColumn('internal_rate');
$schema->getTable('kimai2_activities_rates')->dropColumn('internal_rate');
$schema->getTable('kimai2_projects_rates')->dropColumn('internal_rate');
$schema->getTable('kimai2_customers_rates')->dropColumn('internal_rate');
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace DoctrineMigrations;
use App\Doctrine\AbstractMigration;
use Doctrine\DBAL\Schema\Schema;
/**
* @version 1.9
*/
final class Version20200323163039 extends AbstractMigration
{
public function getDescription(): string
{
return 'Set internal-rate from rate for existing timesheet entries';
}
public function up(Schema $schema): void
{
$this->addSql('UPDATE kimai2_timesheet SET internal_rate = rate');
}
public function down(Schema $schema): void
{
}
}

View File

@@ -23,6 +23,10 @@ class TimesheetCountedStatistic
* @var float
*/
protected $recordRate = 0.0;
/**
* @var float
*/
protected $recordInternalRate = 0.0;
/**
* Returns the total amount of included timesheet records.
@@ -86,4 +90,25 @@ class TimesheetCountedStatistic
return $this;
}
/**
* Returns the total internal rate of all included timesheet records.
*
* @return float
*/
public function getRecordInternalRate()
{
return $this->recordInternalRate;
}
/**
* @param float $recordInternalRate
* @return $this
*/
public function setRecordInternalRate($recordInternalRate)
{
$this->recordInternalRate = (float) $recordInternalRate;
return $this;
}
}

View File

@@ -97,6 +97,7 @@ class ActivityRepository extends EntityRepository
->addSelect('COUNT(t.id) as recordAmount')
->addSelect('SUM(t.duration) as recordDuration')
->addSelect('SUM(t.rate) as recordRate')
->addSelect('SUM(t.internalRate) as recordInternalRate')
->from(Timesheet::class, 't')
->where('t.activity = :activity')
;
@@ -107,6 +108,7 @@ class ActivityRepository extends EntityRepository
$stats->setRecordAmount($timesheetResult[0]['recordAmount']);
$stats->setRecordDuration($timesheetResult[0]['recordDuration']);
$stats->setRecordRate($timesheetResult[0]['recordRate']);
$stats->setRecordInternalRate($timesheetResult[0]['recordInternalRate']);
}
return $stats;

View File

@@ -89,6 +89,7 @@ class CustomerRepository extends EntityRepository
->addSelect('COUNT(t.id) as recordAmount')
->addSelect('SUM(t.duration) as recordDuration')
->addSelect('SUM(t.rate) as recordRate')
->addSelect('SUM(t.internalRate) as recordInternalRate')
->from(Timesheet::class, 't')
->join(Project::class, 'p', Query\Expr\Join::WITH, 't.project = p.id')
->andWhere('p.customer = :customer')
@@ -99,6 +100,7 @@ class CustomerRepository extends EntityRepository
$stats->setRecordAmount($timesheetResult[0]['recordAmount']);
$stats->setRecordDuration($timesheetResult[0]['recordDuration']);
$stats->setRecordRate($timesheetResult[0]['recordRate']);
$stats->setRecordInternalRate($timesheetResult[0]['recordInternalRate']);
}
$qb = $this->getEntityManager()->createQueryBuilder();

View File

@@ -84,6 +84,7 @@ class ProjectRepository extends EntityRepository
->addSelect('COUNT(t.id) as recordAmount')
->addSelect('SUM(t.duration) as recordDuration')
->addSelect('SUM(t.rate) as recordRate')
->addSelect('SUM(t.internalRate) as recordInternalRate')
->andWhere('t.project = :project')
->setParameter('project', $project)
;
@@ -103,6 +104,7 @@ class ProjectRepository extends EntityRepository
$stats->setRecordAmount($timesheetResult[0]['recordAmount']);
$stats->setRecordDuration($timesheetResult[0]['recordDuration']);
$stats->setRecordRate($timesheetResult[0]['recordRate']);
$stats->setRecordInternalRate($timesheetResult[0]['recordInternalRate']);
}
$qb = $this->getEntityManager()->createQueryBuilder();

View File

@@ -44,45 +44,70 @@ class RateCalculator implements CalculatorInterface
{
if (null === $record->getEnd()) {
$record->setRate(0);
$record->setInternalRate(0);
return;
}
$fixedRate = $record->getFixedRate();
$hourlyRate = $record->getHourlyRate();
$fixedInternalRate = null;
$internalRate = null;
if (null === $fixedRate && null === $hourlyRate) {
$rate = $this->getBestFittingRate($record);
$rate = $this->getBestFittingRate($record);
if (null !== $rate) {
if ($rate->isFixed()) {
$fixedRate = $rate->getRate();
} else {
$hourlyRate = $rate->getRate();
if (null !== $rate) {
if ($rate->isFixed()) {
$fixedRate = $fixedRate ?? $rate->getRate();
$fixedInternalRate = $rate->getRate();
if (null !== $rate->getInternalRate()) {
$fixedInternalRate = $rate->getInternalRate();
}
} else {
$hourlyRate = $hourlyRate ?? $rate->getRate();
$internalRate = $rate->getRate();
if (null !== $rate->getInternalRate()) {
$internalRate = $rate->getInternalRate();
}
}
}
if (null !== $fixedRate) {
$record->setRate($fixedRate);
$record->setFixedRate($fixedRate);
$record->setRate($fixedRate);
if (null === $fixedInternalRate) {
$fixedInternalRate = (float) $record->getUser()->getPreferenceValue(UserPreference::INTERNAL_RATE, $fixedRate);
}
$record->setInternalRate($fixedInternalRate);
return;
}
// user preferences => fallback if nothing else was configured
if (null === $hourlyRate) {
$hourlyRate = (float) $record->getUser()->getPreferenceValue(UserPreference::HOURLY_RATE, 0.00);
}
if (null === $internalRate) {
$internalRate = $record->getUser()->getPreferenceValue(UserPreference::INTERNAL_RATE, 0.00);
if (null === $internalRate) {
$internalRate = $hourlyRate;
} else {
$internalRate = (float) $internalRate;
}
}
$factor = $this->getRateFactor($record);
$hourlyRate = (float) ($hourlyRate * $factor);
$factoredHourlyRate = (float) ($hourlyRate * $factor);
$factoredInternalRate = (float) ($internalRate * $factor);
$totalRate = 0;
$totalInternalRate = 0;
if (null !== $record->getDuration()) {
$totalRate = Util::calculateRate($hourlyRate, $record->getDuration());
$totalRate = Util::calculateRate($factoredHourlyRate, $record->getDuration());
$totalInternalRate = Util::calculateRate($factoredInternalRate, $record->getDuration());
}
$record->setHourlyRate($hourlyRate);
$record->setHourlyRate($factoredHourlyRate);
$record->setInternalRate($totalInternalRate);
$record->setRate($totalRate);
}

View File

@@ -77,64 +77,7 @@
{% endif %}
{% if can_edit %}
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
{% import "macros/widgets.html.twig" as widgets %}
{% block box_attributes %}id="activity_rates_box"{% endblock %}
{% block box_title %}
{{ 'rates.title'|trans }}
{% endblock %}
{% block box_tools %}
<a class="modal-ajax-form open-edit btn btn-box-tool" data-href="{{ path('admin_activity_rate_add', {'id': activity.id}) }}" data-toggle="tooltip" data-placement="top" title="{{ 'create'|trans }}"><i class="{{ 'create'|icon }}"></i></a>
{% endblock %}
{% block box_body %}
{% if rates is empty %}
{{ 'rates.empty'|trans }}
{% else %}
<table class="table dataTable" >
<thead>
<tr>
<th>
{{ 'label.user'|trans }}
</th>
<th>
{{ 'label.hourlyRate'|trans }}
</th>
<th>
{{ 'label.fixedRate'|trans }}
</th>
<th class="actions"></th>
</tr>
</thead>
<tbody>
{% for rate in rates %}
<tr>
<td>
{% if rate.user is not null %}
{{ widgets.user_avatar(rate.user) }}
{% else %}
&ndash;
{% endif %}
</td>
<td>
{% if not rate.fixed %}
{{ rate.rate|money(currency) }}
{% endif %}
</td>
<td>
{% if rate.fixed %}
{{ rate.rate|money(currency) }}
{% endif %}
</td>
<td class="actions">
<a href="{{ path('admin_activity_rate_delete', {'id': activity.id, 'rate': rate.id}) }}" class="confirmation-link btn btn-default btn-xs" data-question="confirm.delete"><i class="{{ 'delete'|icon }}"></i></a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% endblock %}
{% endembed %}
{{ include('embeds/rates-table.html.twig', {'id': 'activity_rates_box', 'entity': activity, 'create_url': path('admin_activity_rate_add', {'id': activity.id}), 'delete_route': 'delete_activity_rate', 'currency': currency}) }}
{% endif %}
{% endblock %}
@@ -143,7 +86,7 @@
{{ parent() }}
<script type="text/javascript">
document.addEventListener('kimai.initialized', function() {
KimaiReloadPageWidget.create('kimai.activityUpdate kimai.teamUpdate kimai.projectTeamUpdate kimai.projectUpdate');
KimaiReloadPageWidget.create('kimai.activityUpdate kimai.teamUpdate kimai.projectTeamUpdate kimai.projectUpdate kimai.rateUpdate');
});
</script>
{% endblock %}

View File

@@ -11,6 +11,11 @@
'%duration%': '<strong>' ~ stats.recordDuration|duration ~ '</strong>'
} %}
{% set currency = null %}
{% if activity.project is not null %}
{% set currency = activity.project.customer.currency %}
{% endif %}
{% if activity.project is not null %}
{% set params = params|merge({
'%project%': '<strong>' ~ activity.project.name ~ '</strong>',
@@ -20,12 +25,9 @@
<p>
{{ 'admin_activity.short_stats'|trans(params)|raw }}
{{ 'label.rate_internal'|trans }}: {{ stats.recordInternalRate|money(currency) }}.
</p>
{% set currency = null %}
{% if activity.project is not null %}
{% set currency = activity.project.customer.currency %}
{% endif %}
{{ progress.progressbar(activity.budget, stats.recordRate, 'label.budget'|trans, stats.recordRate|money(currency) ~ ' / ' ~ activity.budget|money(currency) ) }}
{{ progress.progressbar(activity.timeBudget, stats.recordDuration, 'label.timeBudget'|trans, stats.recordDuration|duration ~ ' / ' ~ activity.timeBudget|duration ) }}

View File

@@ -125,64 +125,7 @@
{% endif %}
{% if can_edit %}
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
{% import "macros/widgets.html.twig" as widgets %}
{% block box_attributes %}id="customer_rates_box"{% endblock %}
{% block box_title %}
{{ 'rates.title'|trans }}
{% endblock %}
{% block box_tools %}
<a class="modal-ajax-form open-edit btn btn-box-tool" data-href="{{ path('admin_customer_rate_add', {'id': customer.id}) }}" data-toggle="tooltip" data-placement="top" title="{{ 'create'|trans }}"><i class="{{ 'create'|icon }}"></i></a>
{% endblock %}
{% block box_body %}
{% if rates is empty %}
{{ 'rates.empty'|trans }}
{% else %}
<table class="table dataTable" >
<thead>
<tr>
<th>
{{ 'label.user'|trans }}
</th>
<th>
{{ 'label.hourlyRate'|trans }}
</th>
<th>
{{ 'label.fixedRate'|trans }}
</th>
<th class="actions"></th>
</tr>
</thead>
<tbody>
{% for rate in rates %}
<tr>
<td>
{% if rate.user is not null %}
{{ widgets.user_avatar(rate.user) }}
{% else %}
&ndash;
{% endif %}
</td>
<td>
{% if not rate.fixed %}
{{ rate.rate|money(customer.currency) }}
{% endif %}
</td>
<td>
{% if rate.fixed %}
{{ rate.rate|money(customer.currency) }}
{% endif %}
</td>
<td class="actions">
<a href="{{ path('admin_customer_rate_delete', {'id': customer.id, 'rate': rate.id}) }}" class="confirmation-link btn btn-default btn-xs" data-question="confirm.delete"><i class="{{ 'delete'|icon }}"></i></a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% endblock %}
{% endembed %}
{{ include('embeds/rates-table.html.twig', {'id': 'customer_rates_box', 'entity': customer, 'create_url': path('admin_customer_rate_add', {'id': customer.id}), 'delete_route': 'delete_customer_rate', 'currency': customer.currency}) }}
{% endif %}
{% if teams is not null %}
@@ -207,7 +150,7 @@
{{ parent() }}
<script type="text/javascript">
document.addEventListener('kimai.initialized', function() {
KimaiReloadPageWidget.create('kimai.customerTeamUpdate kimai.customerUpdate kimai.teamUpdate kimai.projectTeamUpdate kimai.projectUpdate');
KimaiReloadPageWidget.create('kimai.customerTeamUpdate kimai.customerUpdate kimai.teamUpdate kimai.projectTeamUpdate kimai.projectUpdate kimai.rateUpdate');
});
</script>
{% endblock %}

View File

@@ -12,11 +12,14 @@
'%rate%': '<strong>' ~ stats.recordRate|money ~ '</strong>'
} %}
{% set currency = customer.currency %}
<p>
{{ 'admin_customer.short_stats'|trans(params)|raw }}
{{ 'label.rate_internal'|trans }}: {{ stats.recordInternalRate|money(currency) }}.
</p>
{{ progress.progressbar(customer.budget, stats.recordRate, 'label.budget'|trans, stats.recordRate|money(customer.currency) ~ ' / ' ~ customer.budget|money(customer.currency) ) }}
{{ progress.progressbar(customer.budget, stats.recordRate, 'label.budget'|trans, stats.recordRate|money(currency) ~ ' / ' ~ customer.budget|money(currency) ) }}
{{ progress.progressbar(customer.timeBudget, stats.recordDuration, 'label.timeBudget'|trans, stats.recordDuration|duration ~ ' / ' ~ customer.timeBudget|duration ) }}
{% endblock %}
{% endembed %}

View File

@@ -0,0 +1,60 @@
{% embed '@AdminLTE/Widgets/box-widget.html.twig' with {'id': id, 'entity': entity, 'create_url': create_url, 'delete_route': delete_route, 'currency': currency} %}
{% import "macros/widgets.html.twig" as widgets %}
{% block box_attributes %}id="{{ id }}"{% endblock %}
{% block box_title %}
{{ 'rates.title'|trans }}
{% endblock %}
{% block box_tools %}
<a class="modal-ajax-form open-edit btn btn-box-tool" data-href="{{ create_url }}" data-toggle="tooltip" data-placement="top" title="{{ 'create'|trans }}"><i class="{{ 'create'|icon }}"></i></a>
{% endblock %}
{% block box_body %}
{% if rates is empty %}
{{ 'rates.empty'|trans }}
{% else %}
<table class="table dataTable" >
<thead>
<tr>
<th>
{{ 'label.user'|trans }}
</th>
<th>
{{ 'label.hourlyRate'|trans }}
</th>
<th>
{{ 'label.rate_internal'|trans }}
</th>
<th>
{{ 'label.fixedRate'|trans }}
</th>
<th class="actions"></th>
</tr>
</thead>
<tbody>
{% for rate in rates %}
<tr>
<td>
{% if rate.user is not null %}
{{ widgets.user_avatar(rate.user) }}
{% else %}
&ndash;
{% endif %}
</td>
<td>
{{ rate.rate|money(currency) }}
</td>
<td>
{{ rate.internalRate|money(currency) }}
</td>
<td>
{{ widgets.label_boolean(rate.fixed) }}
</td>
<td class="actions">
<a href="{{ path(delete_route, {'id': entity.id, 'rateId': rate.id}) }}" class="btn btn-default btn-xs api-link" data-question="confirm.delete" data-event="kimai.rateUpdate kimai.rateDelete" data-method="DELETE" data-msg-error="action.delete.error" data-msg-success="action.delete.success"><i class="{{ 'delete'|icon }}"></i></a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% endblock %}
{% endembed %}

View File

@@ -2,9 +2,10 @@
{% macro export(view) %}
{% import "macros/widgets.html.twig" as widgets %}
{% set actions = {} %}
{% set actions = {'visibility': '#modal_export'} %}
{% if view == 'preview' %}
{% set actions = {'off': {'id':'export-toggle-button'}} %}
{% set actions = actions|merge({'off': {'id':'export-toggle-button'}}) %}
{% endif %}
{% set actions = actions|merge({'help': {'url': 'export.html'|docu_link, 'target': '_blank'}}) %}

View File

@@ -5,15 +5,16 @@
{% import "export/actions.html.twig" as actions %}
{% set columns = {
'date': 'alwaysVisible',
'user': 'hidden-xs hidden-sm',
'project': 'hidden-xs hidden-sm',
'activity': 'hidden-xs hidden-sm',
'description': 'hidden-xs hidden-sm',
'unit_price': 'hidden-xs',
'duration': '',
'total_rate': '',
'exported': 'alwaysVisible',
'date': {'class': 'alwaysVisible text-nowrap', 'orderBy': false},
'user': {'class': 'hidden-xs hidden-sm', 'orderBy': false},
'project': {'class': 'hidden-xs hidden-sm', 'orderBy': false},
'activity': {'class': 'hidden-xs hidden-sm hidden-md', 'orderBy': false},
'description': {'class': 'hidden-xs hidden-sm hidden-md timesheet-description', 'orderBy': false},
'unit_price': {'class': 'hidden hidden-xs text-nowrap', 'orderBy': false},
'duration': {'class': 'text-nowrap', 'orderBy': false},
'rate_internal': {'class': 'hidden-xs text-nowrap', 'orderBy': false},
'total_rate': {'class': 'text-nowrap', 'orderBy': false},
'exported': {'class': 'alwaysVisible', 'orderBy': false},
} %}
{% set tableName = 'export' %}
@@ -22,6 +23,10 @@
{% block page_subtitle %}{{ 'export.subtitle'|trans }}{% endblock %}
{% block page_actions %}{{ actions.export((preview_show ? 'preview' : 'index')) }}{% endblock %}
{% block main_before %}
{{ tables.data_table_column_modal(tableName, columns) }}
{% endblock %}
{% block main %}
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
@@ -84,24 +89,25 @@
{% else %}
{{ tables.datatable_header(tableName, columns, query, {}) }}
{% set totalAmount = {} %}
{% set totalInternalAmount = {} %}
{% set totalDuration = 0 %}
{% for entry in entries %}
{% set currency = entry.project.customer.currency %}
{% set duration = entry.duration|duration %}
{% if totalAmount[currency] is not defined %}
{% set totalAmount = totalAmount|merge({(currency): 0}) %}
{% set totalInternalAmount = totalInternalAmount|merge({(currency): 0}) %}
{% endif %}
{% if entry.fixedRate is not null %}
{% set rate = entry.fixedRate %}
{% set duration = 1 %}
{% set totalDuration = totalDuration + 1 %}
{% else %}
{% set rate = entry.hourlyRate %}
{% set totalDuration = totalDuration + entry.duration %}
{% endif %}
{% set totalDuration = totalDuration + entry.duration %}
{% set totalAmount = totalAmount|merge({(currency): totalAmount[currency] + entry.rate}) %}
{% set totalInternalAmount = totalInternalAmount|merge({(currency): totalInternalAmount[currency] + entry.internalRate}) %}
<tr>
<td class="text-nowrap">{{ entry.begin|date_short }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'date') }}">{{ entry.begin|date_short }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'user') }}">{{ widgets.label_user(entry.user) }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'project') }}">
{{ widgets.label_project(entry.project) }}
@@ -109,19 +115,22 @@
<small>{{ widgets.label_customer(entry.project.customer) }}</small>
</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'activity') }}">{{ widgets.label_activity(entry.activity) }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'description') }} timesheet-description">
<td class="{{ tables.data_table_column_class(tableName, columns, 'description') }}">
{{ entry.description|escape|desc2html }}
</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'unit_price') }} text-nowrap">
<td class="{{ tables.data_table_column_class(tableName, columns, 'unit_price') }}">
{{ rate|money(currency) }}
</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'duration') }} text-nowrap" data-duration="{{ entry.duration }}">
<td class="{{ tables.data_table_column_class(tableName, columns, 'duration') }}" data-duration="{{ entry.duration }}">
{{ duration }}
</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'total_rate') }} text-nowrap">
<td class="{{ tables.data_table_column_class(tableName, columns, 'rate_internal') }}">
{{ entry.internalRate|money(currency) }}
</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'total_rate') }}">
{{ entry.rate|money(currency) }}
</td>
<td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'exported') }}">
{% if is_granted('edit_export', entry) %}
{% if entry.exported %}
<button type="button" class="btn btn-default exportBtn active" data-toggle="button" aria-pressed="true" autocomplete="off"
@@ -145,15 +154,28 @@
</tr>
{% endfor %}
<tr>
<td colspan="6"></td>
<th>{{ totalDuration|duration }}</th>
<th>
<th class="{{ tables.data_table_column_class(tableName, columns, 'date') }}"></th>
<th class="{{ tables.data_table_column_class(tableName, columns, 'user') }}"></th>
<th class="{{ tables.data_table_column_class(tableName, columns, 'project') }}"></th>
<th class="{{ tables.data_table_column_class(tableName, columns, 'activity') }}"></th>
<th class="{{ tables.data_table_column_class(tableName, columns, 'description') }}"></th>
<th class="{{ tables.data_table_column_class(tableName, columns, 'unit_price') }}"></th>
<th class="{{ tables.data_table_column_class(tableName, columns, 'duration') }}">
{{ totalDuration|duration }}
</th>
<th class="{{ tables.data_table_column_class(tableName, columns, 'rate_internal') }}">
{% for currency, amount in totalInternalAmount %}
{{ amount|money(currency) }}
{% if not loop.last %}<br>{% endif %}
{% endfor %}
</th>
<th class="{{ tables.data_table_column_class(tableName, columns, 'total_rate') }}">
{% for currency, amount in totalAmount %}
{{ amount|money(currency) }}
{% if not loop.last %}<br>{% endif %}
{% endfor %}
</th>
<td></td>
<th class="{{ tables.data_table_column_class(tableName, columns, 'exported') }}"></th>
</tr>
{{ tables.data_table_footer(entries) }}
{% endif %}

View File

@@ -0,0 +1,305 @@
{% set showUserColumn = showUserColumn|default(true) %}
{% set showInternalRate = showInternalRate|default(false) %}
{% set showRateColumn = showRateColumn|default(true) %}
{% set showRateBudget = showRateBudget|default(false) %}
{% set showTimeBudget = showTimeBudget|default(false) %}
{% set decimal = decimal|default(false) %}
{% if query.user %}
{# this is only triggered, if a user exports from his personal timesheet screen #}
{% set showUserColumn = false %}
{# if exporting via the admin screen, users without view_rate_own_timesheet might still see their own rates - maybe merge view_rate_own_timesheet and view_rate_other_timesheet into a new view_rate permission? #}
{% set showRateColumn = is_granted('view_rate_own_timesheet') %}
{% endif %}
<html lang="{{ app.request.locale }}">
<head>
{% block styles %}
<style>
body {
font-family: sans-serif;
font-size: 10pt;
margin: 0;
padding: 0;
}
p {
margin: 0;
}
table.items {
border: 0.1mm solid #000000;
width: 100%;
font-size: 9pt;
border-collapse: collapse;
}
td, th {
padding: 7px;
}
td {
vertical-align: top;
}
.items td {
border-left: 0.1mm solid #000000;
border-right: 0.1mm solid #000000;
}
.items tr.even {
background-color: #f5f5f5;
}
.items tr.summary {
background-color: #efefef;
}
.items tr.summary td {
font-weight: bold;
border-top: 0.1mm solid #000000;
border-bottom: 0.1mm solid #000000;
}
table thead th {
background-color: #ececec;
border: 0.1mm solid #000000;
font-weight: bold;
font-size: 10pt;
text-align: left;
}
.items td.totals {
font-weight: bold;
border: 0.1mm solid #000000;
}
.items .center,
.items td.duration,
.items td.cost {
text-align: center;
}
.text-nowrap {
white-space: nowrap;
}
</style>
{% endblock %}
</head>
<body>
{% block pdf_footer %}
<!--mpdf
<htmlpagefooter name="myfooter">
<table style="border-top: 1px solid #000000; font-size: 9pt; padding-top: 3mm; width: 100%">
<tr>
<td align="left">
{{ 'export.page_of'|trans({'%page%': '{PAGENO}', '%pages%': '{nb}'}) }}
{% if not showUserColumn %}
&ndash;
{{ 'label.user'|trans }}: {{ query.user.displayName }}
{% endif %}
</td>
<td align="right">
{% if kimai_context.branding.company is not empty %}
{{ kimai_context.branding.company|raw }} &ndash; {{ now|date_full }}
{% else %}
{{ 'export.date_copyright'|trans({'%date%': now|date_full, '%kimai%': '<a href="' ~ constant('App\\Constants::HOMEPAGE') ~ '">' ~ constant('App\\Constants::SOFTWARE') ~ '</a>'})|raw }}
{% endif %}
</td>
</tr>
</table>
</htmlpagefooter>
<sethtmlpagefooter name="myfooter" value="on" />
mpdf-->
{% endblock %}
{% block summary %}
<h2 style="margin-bottom: 0; padding-bottom: 0">{{ 'export.document_title'|trans }}</h2>
<p>
{{ 'export.period'|trans }}:
{{ query.begin|date_short }} - {{ query.end|date_short }}
</p>
<h3>{{ 'export.summary'|trans }}</h3>
<table class="items">
<thead>
<tr>
<th>{{ 'label.customer'|trans }}</th>
<th>{{ 'label.project'|trans }}</th>
{% if showTimeBudget %}
<th class="center">{{ 'label.timeBudget'|trans }}</th>
{% endif %}
{% if showRateBudget %}
<th class="center">{{ 'label.budget'|trans }}</th>
{% endif %}
<th class="center">{{ 'label.duration'|trans }}</th>
{% if showRateColumn %}
{% if showInternalRate %}
<th class="center">{{ 'label.rate_internal'|trans }}</th>
{% endif %}
<th class="center">{{ 'label.rate'|trans }}</th>
{% endif %}
</tr>
</thead>
<tbody>
{% set customer = null %}
{% set customerDuration = 0 %}
{% set customerRate = 0 %}
{% set customerInternalRate = 0 %}
{% set customerCurrency = null %}
{% set customerCount = 0 %}
{% for id, summary in summaries %}
{% if customer is same as(null) %}
{% set customer = summary.customer %}
{% set customerCurrency = summary.currency %}
{% endif %}
{% if customer is not same as(summary.customer) %}
<tr class="summary">
<td colspan="2">
</td>
{% if showTimeBudget %}
<td></td>
{% endif %}
{% if showRateBudget %}
<td></td>
{% endif %}
<td class="totals duration">{{ customerDuration|duration(decimal) }}</td>
{% if showRateColumn %}
{% if showInternalRate %}
<td class="totals cost">{{ customerInternalRate|money(customerCurrency) }}</td>
{% endif %}
<td class="totals cost">{{ customerRate|money(customerCurrency) }}</td>
{% endif %}
</tr>
{% set customerCurrency = summary.currency %}
{% set customer = summary.customer %}
{% set customerDuration = 0 %}
{% set customerRate = 0 %}
{% set customerInternalRate = 0 %}
{% set customerCount = 0 %}
{% endif %}
<tr class="{{ cycle(['odd', 'even'], customerCount) }}">
<td>{{ summary.customer }}</td>
<td>{{ summary.project }}</td>
{% if showTimeBudget %}
<td class="center">
{% if budgets[id] is defined and budgets[id].time_left > 0 %}
{{ budgets[id].time_left|duration(decimal) }}
{% endif %}
</td>
{% endif %}
{% if showRateBudget %}
<td class="center">
{% if budgets[id] is defined and budgets[id].money_left > 0 %}
{{ budgets[id].money_left|money(summary.currency) }}
{% endif %}
</td>
{% endif %}
<td class="duration">{{ summary.duration|duration(decimal) }}</td>
{% if showRateColumn %}
{% if showInternalRate %}
<td class="cost">{{ summary.rate_internal|money(summary.currency) }}</td>
{% endif %}
<td class="cost">{{ summary.rate|money(summary.currency) }}</td>
{% endif %}
</tr>
{% set customerDuration = customerDuration + summary.duration %}
{% set customerRate = customerRate + summary.rate %}
{% set customerInternalRate = customerInternalRate + summary.rate_internal %}
{% set customerCount = customerCount + 1 %}
{% endfor %}
{% if customer is not same as(null) %}
<tr class="summary">
<td colspan="2"></td>
{% if showTimeBudget %}
<td></td>
{% endif %}
{% if showRateBudget %}
<td></td>
{% endif %}
<td class="totals duration">{{ customerDuration|duration(decimal) }}</td>
{% if showRateColumn %}
{% if showInternalRate %}
<td class="totals cost">{{ customerInternalRate|money(customerCurrency) }}</td>
{% endif %}
<td class="totals cost">{{ customerRate|money(customerCurrency) }}</td>
{% endif %}
</tr>
{% endif %}
</tbody>
</table>
<pagebreak>
{% endblock %}
{% block items %}
<h3>{{ 'export.full_list'|trans }}</h3>
{% set duration = 0 %}
{% set rate = 0 %}
{% set rateInternal = 0 %}
{% set currency = false %}
<table class="items">
<thead>
<tr>
<th>{{ 'label.date'|trans }}</th>
{% if showUserColumn %}
<th>{{ 'label.user'|trans }}</th>
{% endif %}
<th>{{ 'label.description'|trans }}</th>
<th class="center">{{ 'label.duration'|trans }}</th>
{% if showRateColumn %}
{% if showInternalRate %}
<th class="center">{{ 'label.rate_internal'|trans }}</th>
{% endif %}
<th class="center">{{ 'label.rate'|trans }}</th>
{% endif %}
</tr>
</thead>
<tbody>
{% for entry in entries %}
{% set duration = duration + entry.duration %}
{% if currency is same as(false) %}
{% set currency = entry.project.customer.currency %}
{% endif %}
{% if currency is not same as(entry.project.customer.currency) %}
{% set currency = null %}
{% endif %}
<tr class="{{ cycle(['odd', 'even'], loop.index0) }}">
<td class="text-nowrap">
{{ entry.begin|date_time }}
{% if entry.end %}
<br>
{{ entry.end|date_time }}
{% endif %}
</td>
{% if showUserColumn %}
<td>{{ entry.user.displayName }}</td>
{% endif %}
<td>
{{ entry.project.customer.name }} - {{ entry.project.name }}{% if entry.activity is not null %} - {{ entry.activity.name }}{% endif %}
{% if entry.description is not empty %}
<br>
<i>{{ entry.description|escape|desc2html }}</i>
{% endif %}
</td>
<td class="duration">{{ entry.duration|duration(decimal) }}</td>
{% if showRateColumn %}
{% if is_granted('view_rate', entry) %}
{% set rate = rate + entry.rate %}
{% set rateInternal = rateInternal + entry.internalRate %}
{% set entryRate = entry.rate|money(entry.project.customer.currency) %}
{% set entryRateInternal = entry.internalRate|money(entry.project.customer.currency) %}
{% else %}
{% set entryRate = '&ndash;' %}
{% set entryRateInternal = '&ndash;' %}
{% endif %}
{% if showInternalRate %}
<td class="cost">{{ entryRate }}</td>
{% endif %}
<td class="cost">{{ entryRateInternal }}</td>
{% endif %}
</tr>
{% endfor %}
<tr class="summary">
{% if showUserColumn %}
<td colspan="3"></td>
{% else %}
<td colspan="2"></td>
{% endif %}
<td class="totals duration">{{ duration|duration(decimal) }}</td>
{% if showRateColumn %}
{% if showInternalRate %}
<td class="totals cost">{% if currency is not null %}{{ rateInternal|money(currency) }}{% endif %}</td>
{% endif %}
<td class="totals cost">{% if currency is not null %}{{ rate|money(currency) }}{% endif %}</td>
{% endif %}
</tr>
</tbody>
</table>
{% endblock %}
</body>
</html>

View File

@@ -1,12 +1,11 @@
{% extends 'export/renderer/default.pdf.twig' %}
{% block summary %}
{% for budget in budgets %}
{% if budget.money_left > 0 %}
{% set showRateBudget = true %}
{% endif %}
{% if budget.time_left > 0 %}
{% set showTimeBudget = true %}
{% endif %}
{% endfor %}
{{ parent() }}
{% endblock %}
{% extends 'export/pdf-layout.html.twig' %}
{% set showRateBudget = false %}
{% set showTimeBudget = false %}
{% for budget in budgets %}
{% if budget.money_left > 0 %}
{% set showRateBudget = true %}
{% endif %}
{% if budget.time_left > 0 %}
{% set showTimeBudget = true %}
{% endif %}
{% endfor %}

View File

@@ -0,0 +1,2 @@
{% extends 'export/pdf-layout.html.twig' %}
{% set showInternalRate = true %}

View File

@@ -51,6 +51,7 @@
'hourlyRate': false,
'fixedRate': false,
'duration': 'label.duration',
'rate_internal': 'label.rate_internal',
'rate': 'label.rate',
}) %}
@@ -63,38 +64,40 @@
document.getElementById('summary-show').addEventListener('click', function(event) {
document.getElementById('export-summary').style.display = event.target.checked ? 'block' : 'none';
});
{% if showTimeBudget %}
document.getElementById('summary-timeBudget').addEventListener('click', function(event) {
var cells = document.getElementsByClassName('export-timeBudget');
for (var columnCell of cells) {
columnCell.style.display = event.target.checked ? 'table-cell' : 'none';
}
});
{% endif %}
{% if showRateBudget %}
document.getElementById('summary-budget').addEventListener('click', function(event) {
var cells = document.getElementsByClassName('export-budget');
for (var columnCell of cells) {
columnCell.style.display = event.target.checked ? 'table-cell' : 'none';
}
});
{% endif %}
document.getElementById('summary-duration').addEventListener('click', function(event) {
if (!document.getElementById('summary-rate').checked) {
return;
}
document.getElementById('summary-rate').disabled = !event.target.checked;
var cells = document.getElementsByClassName('summary-duration');
for (var columnCell of cells) {
columnCell.style.display = event.target.checked ? 'table-cell' : 'none';
}
});
document.getElementById('summary-rate').addEventListener('click', function(event) {
if (!document.getElementById('summary-duration').checked) {
return;
}
document.getElementById('summary-duration').disabled = !event.target.checked;
var cells = document.getElementsByClassName('summary-rate');
for (var columnCell of cells) {
columnCell.style.display = event.target.checked ? 'table-cell' : 'none';
}
});
document.getElementById('summary-rate-internal').addEventListener('click', function(event) {
var cells = document.getElementsByClassName('summary-rate-internal');
for (var columnCell of cells) {
columnCell.style.display = event.target.checked ? 'table-cell' : 'none';
}
});
document.getElementById('summary-show').addEventListener('click', function(event) {
document.getElementById('export-summary').style.display = event.target.checked ? 'block' : 'none';
});
@@ -177,6 +180,12 @@
{{ 'label.duration'|trans }}
</label>
</div>
<div class="form-group">
<label class="control-label" for="summary-rate-internal">
<input type="checkbox" id="summary-rate-internal" name="summary-rate-internal" checked>
{{ 'label.rate_internal'|trans }}
</label>
</div>
<div class="form-group">
<label class="control-label" for="summary-rate">
<input type="checkbox" id="summary-rate" name="summary-rate" checked>
@@ -232,6 +241,7 @@
<th class="center export-budget">{{ 'label.budget'|trans }}</th>
{% endif %}
<th class="duration summary-duration">{{ 'label.duration'|trans }}</th>
<th class="cost summary-rate-internal">{{ 'label.rate_internal'|trans }}</th>
<th class="cost summary-rate">{{ 'label.rate'|trans }}</th>
</tr>
</thead>
@@ -239,6 +249,7 @@
{% set customer = null %}
{% set customerDuration = 0 %}
{% set customerRate = 0 %}
{% set customerInternalRate = 0 %}
{% set customerCurrency = null %}
{% for id, summary in summaries %}
{% if customer is same as(null) %}
@@ -247,7 +258,7 @@
{% endif %}
{% if customer is not same as(summary.customer) %}
<tr class="summary">
<td colspan="2"></td>
<td colspan="2">&nbsp;</td>
{% if showTimeBudget %}
<td class="export-timeBudget"></td>
{% endif %}
@@ -255,12 +266,14 @@
<td class="export-budget"></td>
{% endif %}
<td class="totals duration summary-duration">{{ customerDuration|duration(decimal) }}</td>
<td class="totals cost summary-rate-internal">{{ customerInternalRate|money(customerCurrency) }}</td>
<td class="totals cost summary-rate">{{ customerRate|money(customerCurrency) }}</td>
</tr>
{% set customerCurrency = summary.currency %}
{% set customer = summary.customer %}
{% set customerDuration = 0 %}
{% set customerRate = 0 %}
{% set customerInternalRate = 0 %}
{% endif %}
<tr>
<td>{{ summary.customer }}</td>
@@ -280,14 +293,16 @@
</td>
{% endif %}
<td class="duration summary-duration">{{ summary.duration|duration(decimal) }}</td>
<td class="cost summary-rate-internal">{{ summary.rate_internal|money(summary.currency) }}</td>
<td class="cost summary-rate">{{ summary.rate|money(summary.currency) }}</td>
</tr>
{% set customerDuration = customerDuration + summary.duration %}
{% set customerRate = customerRate + summary.rate %}
{% set customerInternalRate = customerInternalRate + summary.rate_internal %}
{% endfor %}
{% if customer is not same as(null) %}
<tr class="summary">
<td colspan="2"></td>
<td colspan="2">&nbsp;</td>
{% if showTimeBudget %}
<td class="export-timeBudget"></td>
{% endif %}
@@ -295,12 +310,19 @@
<td class="export-budget"></td>
{% endif %}
<td class="totals duration summary-duration">{{ customerDuration|duration(decimal) }}</td>
<td class="totals cost summary-rate-internal">{{ customerInternalRate|money(customerCurrency) }}</td>
<td class="totals cost summary-rate">{{ customerRate|money(customerCurrency) }}</td>
</tr>
{% endif %}
</tbody>
</table>
{#
============================================
SUMMARY: BY ACTIVITY
============================================
#}
<table class="items table table-condensed table-bordered dataTable" id="summary-activity" style="display:none">
<thead>
<tr>
@@ -308,6 +330,7 @@
<th>{{ 'label.project'|trans }}</th>
<th>{{ 'label.activity'|trans }}</th>
<th class="duration summary-duration">{{ 'label.duration'|trans }}</th>
<th class="cost summary-rate-internal">{{ 'label.rate_internal'|trans }}</th>
<th class="cost summary-rate">{{ 'label.rate'|trans }}</th>
</tr>
</thead>
@@ -315,39 +338,45 @@
{% set customer = null %}
{% set customerDuration = 0 %}
{% set customerRate = 0 %}
{% set customerInternalRate = 0 %}
{% set customerCurrency = null %}
{% for summary in summaries %}
{% if customer is same as(null) %}
{% set customer = summary.customer %}
{% set customerCurrency = summary.currency %}
{% endif %}
{% if customer is not same as(summary.customer) %}
<tr class="summary">
<td colspan="3"></td>
<td class="totals duration summary-duration">{{ customerDuration|duration(decimal) }}</td>
<td class="totals cost summary-rate">{{ customerRate|money(customerCurrency) }}</td>
</tr>
{% set customerCurrency = summary.currency %}
{% set customer = summary.customer %}
{% set customerDuration = 0 %}
{% set customerRate = 0 %}
{% endif %}
{% for activitySummary in summary.activities %}
<tr>
<td>{{ summary.customer }}</td>
<td>{{ summary.project }}</td>
<td>{{ activitySummary.activity }}</td>
<td class="duration summary-duration">{{ activitySummary.duration|duration(decimal) }}</td>
<td class="cost summary-rate">{{ activitySummary.rate|money(activitySummary.currency) }}</td>
</tr>
{% endfor %}
{% set customerDuration = customerDuration + summary.duration %}
{% set customerRate = customerRate + summary.rate %}
{% if customer is same as(null) %}
{% set customer = summary.customer %}
{% set customerCurrency = summary.currency %}
{% endif %}
{% if customer is not same as(summary.customer) %}
<tr class="summary">
<td colspan="3"></td>
<td class="totals duration summary-duration">{{ customerDuration|duration(decimal) }}</td>
<td class="totals cost summary-rate-internal">{{ customerInternalRate|money(customerCurrency) }}</td>
<td class="totals cost summary-rate">{{ customerRate|money(customerCurrency) }}</td>
</tr>
{% set customerCurrency = summary.currency %}
{% set customer = summary.customer %}
{% set customerDuration = 0 %}
{% set customerRate = 0 %}
{% set customerInternalRate = 0 %}
{% endif %}
{% for activitySummary in summary.activities %}
<tr>
<td>{{ summary.customer }}</td>
<td>{{ summary.project }}</td>
<td>{{ activitySummary.activity }}</td>
<td class="duration summary-duration">{{ activitySummary.duration|duration(decimal) }}</td>
<td class="cost summary-rate-internal">{{ activitySummary.rate_internal|money(activitySummary.currency) }}</td>
<td class="cost summary-rate">{{ activitySummary.rate|money(activitySummary.currency) }}</td>
</tr>
{% endfor %}
{% set customerDuration = customerDuration + summary.duration %}
{% set customerRate = customerRate + summary.rate %}
{% set customerInternalRate = customerInternalRate + summary.rate_internal %}
{% endfor %}
{% if customer is not same as(null) %}
<tr class="summary">
<td colspan="3"></td>
<td class="totals duration summary-duration">{{ customerDuration|duration(decimal) }}</td>
<td class="totals cost summary-rate-internal">{{ customerInternalRate|money(customerCurrency) }}</td>
<td class="totals cost summary-rate">{{ customerRate|money(customerCurrency) }}</td>
</tr>
{% endif %}
@@ -372,10 +401,16 @@
<tbody>
{% set timeWorked = 0 %}
{% set rateTotal = 0 %}
{% set rateInternalTotal = 0 %}
{% set currency = false %}
{% for entry in entries %}
{% set timeWorked = timeWorked + entry.duration %}
{% set rateTotal = rateTotal + entry.rate %}
{% if entry.internalRate is defined %}
{% set rateInternalTotal = rateInternalTotal + entry.internalRate %}
{% else %}
{% set rateInternalTotal = rateInternalTotal + entry.rate %}
{% endif %}
{% if currency is same as(false) %}
{% set currency = entry.project.customer.currency %}
{% endif %}
@@ -459,6 +494,13 @@
<td class="column-duration text-nowrap" {% if not columns.duration %}style="display: none"{% endif %}>
{{ entry.duration|duration(decimal) }}
</td>
<td class="column-rate_internal text-nowrap" {% if not columns.rate_internal %}style="display: none"{% endif %}>
{% if entry.internalRate is defined %}
{{ entry.internalRate|money(entry.project.customer.currency) }}
{% else %}
{{ entry.rate|money(entry.project.customer.currency) }}
{% endif %}
</td>
<td class="column-rate text-nowrap" {% if not columns.rate %}style="display: none"{% endif %}>
{{ entry.rate|money(entry.project.customer.currency) }}
</td>
@@ -467,13 +509,20 @@
{# leave in tbody instead of adding it to tfoot, as tfoot will be repeated on each page when printing #}
<tr>
{% for id, visibility in columns %}
{% if id != 'duration' and id != 'rate' %}
{% if id not in ['duration', 'rate', 'rate_internal'] %}
<th class="column-{{ id }}" {% if not visibility %}style="display: none"{% endif %}></th>
{% endif %}
{% endfor %}
<th class="text-nowrap column-duration">
{{- timeWorked|duration(decimal) -}}
</th>
<th class="text-nowrap column-rate_internal">
{%- if currency is not null and currency is not same as(false) %}
{{ rateInternalTotal|money(currency) }}
{% else %}
{{ rateInternalTotal|money }}
{% endif -%}
</th>
<th class="text-nowrap column-rate">
{%- if currency is not null and currency is not same as(false) %}
{{ rateTotal|money(currency) }}

View File

@@ -1,274 +1 @@
{% set showUserColumn = true %}
{% set showRateColumn = true %}
{% set decimal = decimal|default(false) %}
{% set showRateBudget = false %}
{% set showTimeBudget = false %}
{% if query.user %}
{# this is only triggered, if a user exports from his personal timesheet screen#}
{% set showUserColumn = false %}
{% set showRateColumn = is_granted('view_rate_own_timesheet') %}
{# TODO if exporting via the admin screen, users without view_rate_own_timesheet might still see their own rates, maybe merge view_rate_own_timesheet and view_rate_other_timesheet into a new view_rate permission? #}
{% endif %}
<html>
<head>
<style>
body {
font-family: sans-serif;
font-size: 10pt;
}
p {
margin: 0;
}
table.items {
border: 0.1mm solid #000000;
width: 100%;
font-size: 9pt;
border-collapse: collapse;
}
td, th {
padding: 7px;
}
td {
vertical-align: top;
}
.items td {
border-left: 0.1mm solid #000000;
border-right: 0.1mm solid #000000;
}
.items tr.even {
/*background-color: #e0ebff;*/
background-color: #f5f5f5;
}
.items tr.summary {
background-color: #efefef;
}
.items tr.summary td {
font-weight: bold;
border-top: 0.1mm solid #000000;
border-bottom: 0.1mm solid #000000;
}
table thead th {
background-color: #ececec;
border: 0.1mm solid #000000;
font-weight: bold;
font-size: 10pt;
text-align: left;
}
.items td.totals {
font-weight: bold;
border: 0.1mm solid #000000;
}
.items .center,
.items td.duration,
.items td.cost {
text-align: center;
}
.text-nowrap {
white-space: nowrap;
}
</style>
</head>
<body>
<!--mpdf
<htmlpagefooter name="myfooter">
<table style="border-top: 1px solid #000000; font-size: 9pt; padding-top: 3mm; width: 100%">
<tr>
<td align="left">
{{ 'export.page_of'|trans({'%page%': '{PAGENO}', '%pages%': '{nb}'}) }}
{% if not showUserColumn %}
&ndash;
{{ 'label.user'|trans }}: {{ query.user.displayName }}
{% endif %}
</td>
<td align="right">
{% if kimai_context.branding.company is not empty %}
{{ kimai_context.branding.company|raw }} &ndash; {{ now|date_full }}
{% else %}
{{ 'export.date_copyright'|trans({'%date%': now|date_full, '%kimai%': '<a href="' ~ constant('App\\Constants::HOMEPAGE') ~ '">' ~ constant('App\\Constants::SOFTWARE') ~ '</a>'})|raw }}
{% endif %}
</td>
</tr>
</table>
</htmlpagefooter>
<sethtmlpagefooter name="myfooter" value="on" />
mpdf-->
{% block summary %}
<h2 style="margin-bottom: 0; padding-bottom: 0">{{ 'export.document_title'|trans }}</h2>
<p>
{{ 'export.period'|trans }}:
{{ query.begin|date_short }} - {{ query.end|date_short }}
</p>
<h3>{{ 'export.summary'|trans }}</h3>
<table class="items">
<thead>
<tr>
<th>{{ 'label.customer'|trans }}</th>
<th>{{ 'label.project'|trans }}</th>
{% if showTimeBudget %}
<th class="center">{{ 'label.timeBudget'|trans }}</th>
{% endif %}
{% if showRateBudget %}
<th class="center">{{ 'label.budget'|trans }}</th>
{% endif %}
<th class="center">{{ 'label.duration'|trans }}</th>
{% if showRateColumn %}
<th class="center">{{ 'label.rate'|trans }}</th>
{% endif %}
</tr>
</thead>
<tbody>
{% set customer = null %}
{% set customerDuration = 0 %}
{% set customerRate = 0 %}
{% set customerCurrency = null %}
{% set customerCount = 0 %}
{% for id, summary in summaries %}
{% if customer is same as(null) %}
{% set customer = summary.customer %}
{% set customerCurrency = summary.currency %}
{% endif %}
{% if customer is not same as(summary.customer) %}
<tr class="summary">
<td colspan="2">
</td>
{% if showTimeBudget %}
<td></td>
{% endif %}
{% if showRateBudget %}
<td></td>
{% endif %}
<td class="totals duration">{{ customerDuration|duration(decimal) }}</td>
{% if showRateColumn %}
<td class="totals cost">{{ customerRate|money(customerCurrency) }}</td>
{% endif %}
</tr>
{% set customerCurrency = summary.currency %}
{% set customer = summary.customer %}
{% set customerDuration = 0 %}
{% set customerRate = 0 %}
{% set customerCount = 0 %}
{% endif %}
<tr class="{{ cycle(['odd', 'even'], customerCount) }}">
<td>{{ summary.customer }}</td>
<td>{{ summary.project }}</td>
{% if showTimeBudget %}
<td class="center">
{% if budgets[id] is defined and budgets[id].time_left > 0 %}
{{ budgets[id].time_left|duration(decimal) }}
{% endif %}
</td>
{% endif %}
{% if showRateBudget %}
<td class="center">
{% if budgets[id] is defined and budgets[id].money_left > 0 %}
{{ budgets[id].money_left|money(summary.currency) }}
{% endif %}
</td>
{% endif %}
<td class="duration">{{ summary.duration|duration(decimal) }}</td>
{% if showRateColumn %}
<td class="cost">{{ summary.rate|money(summary.currency) }}</td>
{% endif %}
</tr>
{% set customerDuration = customerDuration + summary.duration %}
{% set customerRate = customerRate + summary.rate %}
{% set customerCount = customerCount + 1 %}
{% endfor %}
{% if customer is not same as(null) %}
<tr class="summary">
<td colspan="2"></td>
{% if showTimeBudget %}
<td></td>
{% endif %}
{% if showRateBudget %}
<td></td>
{% endif %}
<td class="totals duration">{{ customerDuration|duration(decimal) }}</td>
{% if showRateColumn %}
<td class="totals cost">{{ customerRate|money(customerCurrency) }}</td>
{% endif %}
</tr>
{% endif %}
</tbody>
</table>
<pagebreak>
{% endblock %}
{% block items %}
<h3>{{ 'export.full_list'|trans }}</h3>
{% set duration = 0 %}
{% set rate = 0 %}
{% set currency = false %}
<table class="items">
<thead>
<tr>
<th>{{ 'label.date'|trans }}</th>
{% if showUserColumn %}
<th>{{ 'label.user'|trans }}</th>
{% endif %}
<th>{{ 'label.description'|trans }}</th>
<th class="center">{{ 'label.duration'|trans }}</th>
{% if showRateColumn %}
<th class="center">{{ 'label.rate'|trans }}</th>
{% endif %}
</tr>
</thead>
<tbody>
{% for entry in entries %}
{% set duration = duration + entry.duration %}
{% if currency is same as(false) %}
{% set currency = entry.project.customer.currency %}
{% endif %}
{% if currency is not same as(entry.project.customer.currency) %}
{% set currency = null %}
{% endif %}
<tr class="{{ cycle(['odd', 'even'], loop.index0) }}">
<td class="text-nowrap">
{{ entry.begin|date_time }}
{% if entry.end %}
<br>
{{ entry.end|date_time }}
{% endif %}
</td>
{% if showUserColumn %}
<td>{{ entry.user.displayName }}</td>
{% endif %}
<td>
{{ entry.project.customer.name }} - {{ entry.project.name }}{% if entry.activity is not null %} - {{ entry.activity.name }}{% endif %}
{% if entry.description is not empty %}
<br>
<i>{{ entry.description|escape|desc2html }}</i>
{% endif %}
</td>
<td class="duration">{{ entry.duration|duration(decimal) }}</td>
{% if showRateColumn %}
<td class="cost">
{% if is_granted('view_rate', entry) %}
{% set rate = rate + entry.rate %}
{{ entry.rate|money(entry.project.customer.currency) }}
{% else %}
&ndash;
{% endif %}
</td>
{% endif %}
</tr>
{% endfor %}
<tr class="summary">
{% if showUserColumn %}
<td colspan="3"></td>
{% else %}
<td colspan="2"></td>
{% endif %}
<td class="totals duration">{{ duration|duration(decimal) }}</td>
{% if showRateColumn %}
<td class="totals cost">{% if currency is not null %}{{ rate|money(currency) }}{% endif %}</td>
{% endif %}
</tr>
</tbody>
</table>
{% endblock %}
</body>
</html>
{% extends 'export/pdf-layout.html.twig' %}

View File

@@ -75,6 +75,7 @@
{% else %}
{% set isDecimal = model.template.decimalDuration|default(false) %}
{% set entries = model.calculator.entries %}
{% set currency = model.currency %}
{{ tables.datatable_header(tableName, columns, query, {}) }}
{% for entry in entries %}
{% set amount = entry.amount %}
@@ -102,10 +103,10 @@
{{ entry.description|escape|desc2html }}
{% endif %}
</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'unit_price') }} text-center">{{ rate|money(model.calculator.currency) }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'unit_price') }} text-center">{{ rate|money(currency) }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'amount') }} text-center text-nowrap">{{ amount }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'duration') }} text-center text-nowrap" data-duration="{{ entry.duration }}">{{ duration }}</td>
<td class="text-right text-nowrap">{{ entry.rate|money(model.calculator.currency) }}</td>
<td class="text-right text-nowrap">{{ entry.rate|money(currency) }}</td>
</tr>
{% endfor %}
<tr>
@@ -116,7 +117,7 @@
<th class="{{ tables.data_table_column_class(tableName, columns, 'unit_price') }}"></th>
<th class="{{ tables.data_table_column_class(tableName, columns, 'amount') }}"></th>
<th class="{{ tables.data_table_column_class(tableName, columns, 'duration') }} text-center text-nowrap">{{ model.calculator.timeWorked|duration(isDecimal) }}</th>
<th class="text-right text-nowrap">{{ model.calculator.total|money(model.calculator.currency) }}</th>
<th class="text-right text-nowrap">{{ model.calculator.total|money(currency) }}</th>
</tr>
{{ tables.data_table_footer(entries) }}
{% endif %}

View File

@@ -2,6 +2,7 @@
{% set fallback = app.request is not null ? app.request.locale : 'en' %}
{% set language = model.template.language|default(fallback) %}
{% set isDecimal = model.template.decimalDuration|default(false) %}
{% set currency = model.currency %}
{% block invoice %}
<div class="row">
@@ -92,9 +93,9 @@
{% if entry.activity is not null %}{{ entry.activity.name }} / {% endif %}{{ entry.project.name }}
{% endif %}
</td>
<td nowrap class="text-nowrap text-right">{{ rate|money(model.calculator.currency) }}</td>
<td nowrap class="text-nowrap text-right">{{ rate|money(currency) }}</td>
<td nowrap class="text-nowrap text-right">{{ duration }}</td>
<td nowrap class="text-nowrap text-right">{{ entry.rate|money(model.calculator.currency) }}</td>
<td nowrap class="text-nowrap text-right">{{ entry.rate|money(currency) }}</td>
</tr>
{% endfor %}
</tbody>
@@ -103,20 +104,20 @@
<td colspan="4" class="text-right">
{{ 'invoice.subtotal'|trans({}, 'messages', language) }}
</td>
<td class="text-right">{{ model.calculator.subtotal|money(model.calculator.currency) }}</td>
<td class="text-right">{{ model.calculator.subtotal|money(currency) }}</td>
</tr>
<tr>
<td colspan="4" class="text-right">
{{ 'invoice.tax'|trans({}, 'messages', language) }} ({{ model.calculator.vat }}%)
</td>
<td class="text-right">{{ model.calculator.tax|money(model.calculator.currency) }}</td>
<td class="text-right">{{ model.calculator.tax|money(currency) }}</td>
</tr>
<tr>
<td colspan="4" class="text-right text-nowrap">
<strong>{{ 'invoice.total'|trans({}, 'messages', language) }}</strong>
</td>
<td class="text-right">
<strong>{{ model.calculator.total|money(model.calculator.currency) }}</strong>
<strong>{{ model.calculator.total|money(currency) }}</strong>
</td>
</tr>
</tfoot>

View File

@@ -3,6 +3,7 @@
{% set fallback = app.request is not null ? app.request.locale : 'en' %}
{% set language = model.template.language|default(fallback) %}
{% set isDecimal = model.template.decimalDuration|default(false) %}
{% set currency = model.currency %}
{% block invoice %}
<div class="row" id="freelancer-invoice">
@@ -84,9 +85,9 @@
{% if entry.activity is not null %}{{ entry.activity.name }} / {% endif %}{{ entry.project.name }}
{% endif %}
</td>
<td class="text-right text-nowrap">{{ rate|money(model.calculator.currency) }}</td>
<td class="text-right text-nowrap">{{ rate|money(currency) }}</td>
<td class="text-right text-nowrap">{{ duration }}</td>
<td class="text-right text-nowrap">{{ entry.rate|money(model.calculator.currency) }}</td>
<td class="text-right text-nowrap">{{ entry.rate|money(currency) }}</td>
</tr>
{% endfor %}
</tbody>
@@ -94,15 +95,15 @@
<table class="balance">
<tr>
<th>{{ 'invoice.subtotal'|trans({}, 'messages', language) }}</th>
<td>{{ model.calculator.subtotal|money(model.calculator.currency) }}</td>
<td>{{ model.calculator.subtotal|money(currency) }}</td>
</tr>
<tr>
<th>{{ 'invoice.tax'|trans({}, 'messages', language) }} ({{ model.calculator.vat }}%)</th>
<td>{{ model.calculator.tax|money(model.calculator.currency) }}</td>
<td>{{ model.calculator.tax|money(currency) }}</td>
</tr>
<tr>
<th class="total">{{ 'invoice.total'|trans({}, 'messages', language) }}</th>
<td class="total">{{ model.calculator.total|money(model.calculator.currency) }}</td>
<td class="total">{{ model.calculator.total|money(currency) }}</td>
</tr>
</table>
</article>

View File

@@ -99,64 +99,7 @@
{% endif %}
{% if can_edit %}
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
{% import "macros/widgets.html.twig" as widgets %}
{% block box_attributes %}id="project_rates_box"{% endblock %}
{% block box_title %}
{{ 'rates.title'|trans }}
{% endblock %}
{% block box_tools %}
<a class="modal-ajax-form open-edit btn btn-box-tool" data-href="{{ path('admin_project_rate_add', {'id': project.id}) }}" data-toggle="tooltip" data-placement="top" title="{{ 'create'|trans }}"><i class="{{ 'create'|icon }}"></i></a>
{% endblock %}
{% block box_body %}
{% if rates is empty %}
{{ 'rates.empty'|trans }}
{% else %}
<table class="table dataTable" >
<thead>
<tr>
<th>
{{ 'label.user'|trans }}
</th>
<th>
{{ 'label.hourlyRate'|trans }}
</th>
<th>
{{ 'label.fixedRate'|trans }}
</th>
<th class="actions"></th>
</tr>
</thead>
<tbody>
{% for rate in rates %}
<tr>
<td>
{% if rate.user is not null %}
{{ widgets.user_avatar(rate.user) }}
{% else %}
&ndash;
{% endif %}
</td>
<td>
{% if not rate.fixed %}
{{ rate.rate|money(project.customer.currency) }}
{% endif %}
</td>
<td>
{% if rate.fixed %}
{{ rate.rate|money(project.customer.currency) }}
{% endif %}
</td>
<td class="actions">
<a href="{{ path('admin_project_rate_delete', {'id': project.id, 'rate': rate.id}) }}" class="confirmation-link btn btn-default btn-xs" data-question="confirm.delete"><i class="{{ 'delete'|icon }}"></i></a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% endblock %}
{% endembed %}
{{ include('embeds/rates-table.html.twig', {'id': 'project_rates_box', 'entity': project, 'create_url': path('admin_project_rate_add', {'id': project.id}), 'delete_route': 'delete_project_rate', 'currency': project.customer.currency}) }}
{% endif %}
{% if teams is not null%}
@@ -184,7 +127,7 @@
{{ parent() }}
<script type="text/javascript">
document.addEventListener('kimai.initialized', function() {
KimaiReloadPageWidget.create('kimai.projectTeamUpdate kimai.projectUpdate kimai.teamUpdate kimai.customerUpdate ');
KimaiReloadPageWidget.create('kimai.projectTeamUpdate kimai.projectUpdate kimai.teamUpdate kimai.customerUpdate kimai.rateUpdate');
});
</script>
{% endblock %}

View File

@@ -10,12 +10,15 @@
'%activities%': '<strong>' ~ stats.activityAmount ~ '</strong>',
'%duration%': '<strong>' ~ stats.recordDuration|duration ~ '</strong>'
} %}
{% set currency = project.customer.currency %}
<p>
{{ 'admin_project.short_stats'|trans(params)|raw }}
{{ 'label.rate_internal'|trans }}: {{ stats.recordInternalRate|money(currency) }}.
</p>
{{ progress.progressbar(project.budget, stats.recordRate, 'label.budget'|trans, stats.recordRate|money(project.customer.currency) ~ ' / ' ~ project.budget|money(project.customer.currency) ) }}
{{ progress.progressbar(project.budget, stats.recordRate, 'label.budget'|trans, stats.recordRate|money(currency) ~ ' / ' ~ project.budget|money(currency) ) }}
{{ progress.progressbar(project.timeBudget, stats.recordDuration, 'label.timeBudget'|trans, stats.recordDuration|duration ~ ' / ' ~ project.timeBudget|duration ) }}
{% endblock %}
{% endembed %}

View File

@@ -149,6 +149,22 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
);
}
protected function assertNotFoundForDelete(HttpKernelBrowser $client, string $url)
{
return $this->assertExceptionForMethod($client, $url, 'DELETE', [], [
'code' => 404,
'message' => 'Not found'
]);
}
protected function assertEntityNotFoundForDelete(string $role, string $url)
{
return $this->assertExceptionForDeleteAction($role, $url, [], [
'code' => 404,
'message' => 'Not found'
]);
}
protected function assertEntityNotFoundForPatch(string $role, string $url, array $data)
{
return $this->assertExceptionForPatchAction($role, $url, $data, [
@@ -157,11 +173,32 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
]);
}
protected function assertEntityNotFoundForPost(string $role, string $url, array $data, ?string $message = null)
{
return $this->assertExceptionForPostAction($role, $url, $data, [
'code' => 404,
'message' => $message ?? 'Not found'
]);
}
protected function assertExceptionForDeleteAction(string $role, string $url, array $data, array $expectedErrors)
{
$this->assertExceptionForRole($role, $url, 'DELETE', $data, $expectedErrors);
}
protected function assertExceptionForPatchAction(string $role, string $url, array $data, array $expectedErrors)
{
$client = $this->getClientForAuthenticatedUser($role);
$this->assertExceptionForRole($role, $url, 'PATCH', $data, $expectedErrors);
}
$this->request($client, $url, 'PATCH', [], json_encode($data));
protected function assertExceptionForPostAction(string $role, string $url, array $data, array $expectedErrors)
{
$this->assertExceptionForRole($role, $url, 'POST', $data, $expectedErrors);
}
protected function assertExceptionForMethod(HttpKernelBrowser $client, string $url, string $method, array $data, array $expectedErrors)
{
$this->request($client, $url, $method, [], json_encode($data));
$response = $client->getResponse();
self::assertFalse($response->isSuccessful());
@@ -173,25 +210,10 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
);
}
protected function assertEntityNotFoundForDelete(string $role, string $url, array $data)
protected function assertExceptionForRole(string $role, string $url, string $method, array $data, array $expectedErrors)
{
$client = $this->getClientForAuthenticatedUser($role);
$this->request($client, $url, 'DELETE', [], json_encode($data));
$response = $client->getResponse();
self::assertFalse($response->isSuccessful());
$expected = [
'code' => 404,
'message' => 'Not found'
];
self::assertEquals(404, $client->getResponse()->getStatusCode());
self::assertEquals(
$expected,
json_decode($client->getResponse()->getContent(), true)
);
$this->assertExceptionForMethod($client, $url, $method, $data, $expectedErrors);
}
protected function assertApiException(Response $response, string $message)

View File

@@ -9,10 +9,14 @@
namespace App\Tests\API;
use App\DataFixtures\UserFixtures;
use App\Entity\Activity;
use App\Entity\ActivityRate;
use App\Entity\Customer;
use App\Entity\Project;
use App\Entity\User;
use App\Repository\ActivityRateRepository;
use App\Repository\ActivityRepository;
use App\Tests\Mocks\ActivityTestMetaFieldSubscriberMock;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelBrowser;
@@ -22,6 +26,51 @@ use Symfony\Component\HttpKernel\HttpKernelBrowser;
*/
class ActivityControllerTest extends APIControllerBaseTest
{
use RateControllerTestTrait;
protected function getRateUrl(string $id = '1', ?string $rateId = null): string
{
if (null !== $rateId) {
return sprintf('/api/activities/%s/rates/%s', $id, $rateId);
}
return sprintf('/api/activities/%s/rates', $id);
}
protected function importTestRates(string $id): array
{
/** @var ActivityRateRepository $rateRepository */
$rateRepository = $this->getEntityManager()->getRepository(ActivityRate::class);
/** @var ActivityRepository $repository */
$repository = $this->getEntityManager()->getRepository(Activity::class);
/** @var Activity|null $activity */
$activity = $repository->find($id);
if (null === $activity) {
$activity = new Activity();
$activity->setName('foooo');
$repository->saveActivity($activity);
}
$rate1 = new ActivityRate();
$rate1->setActivity($activity);
$rate1->setRate(17.45);
$rate1->setIsFixed(false);
$rateRepository->saveRate($rate1);
$rate2 = new ActivityRate();
$rate2->setActivity($activity);
$rate2->setRate(99);
$rate2->setInternalRate(9);
$rate2->setIsFixed(true);
$rate2->setUser($this->getUserByName(UserFixtures::USERNAME_USER));
$rateRepository->saveRate($rate2);
return [$rate1, $rate2];
}
public function testIsSecure()
{
$this->assertUrlIsSecured('/api/activities');
@@ -29,9 +78,11 @@ class ActivityControllerTest extends APIControllerBaseTest
protected function loadActivityTestData(HttpKernelBrowser $client)
{
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
/** @var Project $project */
$project = $em->getRepository(Project::class)->find(1);
/** @var Customer $customer */
$customer = $em->getRepository(Customer::class)->find(1);
$project2 = new Project();
@@ -104,7 +155,6 @@ class ActivityControllerTest extends APIControllerBaseTest
$this->loadActivityTestData($client);
$query = ['order' => 'ASC', 'orderBy' => 'project'];
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->assertAccessIsGranted($client, '/api/activities', 'GET', $query);
$result = json_decode($client->getResponse()->getContent(), true);
@@ -277,7 +327,7 @@ class ActivityControllerTest extends APIControllerBaseTest
$this->assertTrue($client->getResponse()->isSuccessful());
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
/** @var Activity $activity */
$activity = $em->getRepository(Activity::class)->find(1);
$this->assertEquals('another,testing,bar', $activity->getMetaField('metatestmock')->getValue());

View File

@@ -9,8 +9,12 @@
namespace App\Tests\API;
use App\DataFixtures\UserFixtures;
use App\Entity\Customer;
use App\Entity\CustomerRate;
use App\Entity\User;
use App\Repository\CustomerRateRepository;
use App\Repository\CustomerRepository;
use App\Tests\Mocks\CustomerTestMetaFieldSubscriberMock;
use Symfony\Component\HttpFoundation\Response;
@@ -19,6 +23,53 @@ use Symfony\Component\HttpFoundation\Response;
*/
class CustomerControllerTest extends APIControllerBaseTest
{
use RateControllerTestTrait;
protected function getRateUrl(string $id = '1', ?string $rateId = null): string
{
if (null !== $rateId) {
return sprintf('/api/customers/%s/rates/%s', $id, $rateId);
}
return sprintf('/api/customers/%s/rates', $id);
}
protected function importTestRates(string $id): array
{
/** @var CustomerRateRepository $rateRepository */
$rateRepository = $this->getEntityManager()->getRepository(CustomerRate::class);
/** @var CustomerRepository $repository */
$repository = $this->getEntityManager()->getRepository(Customer::class);
/** @var Customer|null $customer */
$customer = $repository->find($id);
if (null === $customer) {
$customer = new Customer();
$customer->setCountry('DE');
$customer->setTimezone('Europre/Paris');
$customer->setName('foooo');
$repository->saveCustomer($customer);
}
$rate1 = new CustomerRate();
$rate1->setCustomer($customer);
$rate1->setRate(17.45);
$rate1->setIsFixed(false);
$rateRepository->saveRate($rate1);
$rate2 = new CustomerRate();
$rate2->setCustomer($customer);
$rate2->setRate(99);
$rate2->setInternalRate(9);
$rate2->setIsFixed(true);
$rate2->setUser($this->getUserByName(UserFixtures::USERNAME_USER));
$rateRepository->saveRate($rate2);
return [$rate1, $rate2];
}
public function testIsSecure()
{
$this->assertUrlIsSecured('/api/customers');
@@ -221,7 +272,7 @@ class CustomerControllerTest extends APIControllerBaseTest
$this->assertTrue($client->getResponse()->isSuccessful());
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
/** @var Customer $customer */
$customer = $em->getRepository(Customer::class)->find(1);
$this->assertEquals('another,testing,bar', $customer->getMetaField('metatestmock')->getValue());

View File

@@ -9,9 +9,13 @@
namespace App\Tests\API;
use App\DataFixtures\UserFixtures;
use App\Entity\Customer;
use App\Entity\Project;
use App\Entity\ProjectRate;
use App\Entity\User;
use App\Repository\ProjectRateRepository;
use App\Repository\ProjectRepository;
use App\Repository\Query\VisibilityInterface;
use App\Tests\Mocks\ProjectTestMetaFieldSubscriberMock;
use Symfony\Component\HttpFoundation\Response;
@@ -22,6 +26,52 @@ use Symfony\Component\HttpKernel\HttpKernelBrowser;
*/
class ProjectControllerTest extends APIControllerBaseTest
{
use RateControllerTestTrait;
protected function getRateUrl(string $id = '1', ?string $rateId = null): string
{
if (null !== $rateId) {
return sprintf('/api/projects/%s/rates/%s', $id, $rateId);
}
return sprintf('/api/projects/%s/rates', $id);
}
protected function importTestRates(string $id): array
{
/** @var ProjectRateRepository $rateRepository */
$rateRepository = $this->getEntityManager()->getRepository(ProjectRate::class);
/** @var ProjectRepository $repository */
$repository = $this->getEntityManager()->getRepository(Project::class);
/** @var Project|null $project */
$project = $repository->find($id);
if (null === $project) {
$project = new Project();
$project->setName('foooo');
$project->setCustomer($this->getEntityManager()->getRepository(Customer::class)->find(1));
$repository->saveProject($project);
}
$rate1 = new ProjectRate();
$rate1->setProject($project);
$rate1->setRate(17.45);
$rate1->setIsFixed(false);
$rateRepository->saveRate($rate1);
$rate2 = new ProjectRate();
$rate2->setProject($project);
$rate2->setRate(99);
$rate2->setInternalRate(9);
$rate2->setIsFixed(true);
$rate2->setUser($this->getUserByName(UserFixtures::USERNAME_USER));
$rateRepository->saveRate($rate2);
return [$rate1, $rate2];
}
public function testIsSecure()
{
$this->assertUrlIsSecured('/api/projects');
@@ -41,7 +91,7 @@ class ProjectControllerTest extends APIControllerBaseTest
protected function loadProjectTestData(HttpKernelBrowser $client)
{
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
$customer = $em->getRepository(Customer::class)->find(1);
@@ -278,7 +328,7 @@ class ProjectControllerTest extends APIControllerBaseTest
$this->assertTrue($client->getResponse()->isSuccessful());
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
/** @var Project $project */
$project = $em->getRepository(Project::class)->find(1);
$this->assertEquals('another,testing,bar', $project->getMetaField('metatestmock')->getValue());

View File

@@ -0,0 +1,205 @@
<?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\API;
use App\Entity\User;
use Symfony\Component\HttpFoundation\Response;
/**
* @group integration
*/
trait RateControllerTestTrait
{
abstract protected function getRateUrl(string $id = '1', ?string $rateId = null): string;
abstract protected function importTestRates(string $id): array;
public function testAddRateMissingEntityAction()
{
$data = [
'user' => 1,
'rate' => 12.34,
'internal_rate' => 6.66,
'is_fixed' => false
];
$this->assertEntityNotFoundForPost(User::ROLE_ADMIN, $this->getRateUrl(99), $data, 'Not found');
}
public function testAddRateMissingUserAction()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$data = [
'user' => 33,
'rate' => 12.34,
'internal_rate' => 6.66,
'is_fixed' => false
];
$this->request($client, $this->getRateUrl(), 'POST', [], json_encode($data));
$response = $client->getResponse();
$this->assertEquals(400, $response->getStatusCode());
$this->assertApiCallValidationError($response, ['user']);
}
public function testAddRateActionWithInvalidUser()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$data = [
'user' => null,
'rate' => 12.34,
'internal_rate' => 6.66,
'is_fixed' => false
];
$this->request($client, $this->getRateUrl(), 'POST', [], json_encode($data));
$response = $client->getResponse();
$this->assertFalse($response->isSuccessful());
$this->assertEquals(Response::HTTP_FORBIDDEN, $response->getStatusCode());
$json = json_decode($response->getContent(), true);
$this->assertEquals('Access denied.', $json['message']);
}
public function testAddRateAction()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$data = [
'user' => null,
'rate' => 12.34,
'internal_rate' => 6.66,
'is_fixed' => false
];
$this->request($client, $this->getRateUrl(), 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertRateStructure($result, null);
$this->assertNotEmpty($result['id']);
$this->assertEquals(12.34, $result['rate']);
$this->assertEquals(6.66, $result['internalRate']);
$this->assertFalse($result['isFixed']);
}
public function testAddFixedRateAction()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$data = [
'user' => 1,
'rate' => 12.34,
'internal_rate' => 6.66,
'is_fixed' => true
];
$this->request($client, $this->getRateUrl(), 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertRateStructure($result, 1);
$this->assertNotEmpty($result['id']);
$this->assertEquals(12.34, $result['rate']);
$this->assertEquals(6.66, $result['internalRate']);
$this->assertTrue($result['isFixed']);
}
public function testGetRatesEmptyResult()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->request($client, $this->getRateUrl(1));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetRates()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$expectedRates = $this->importTestRates(1);
$this->request($client, $this->getRateUrl(1));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertNotEmpty($result);
$this->assertEquals(count($expectedRates), count($result));
foreach ($result as $rate) {
$this->assertRateStructure($rate, ($rate['user'] === null ? null : $rate['user']['id']));
}
}
public function testGetRatesEntityNotFound()
{
$this->assertEntityNotFound(User::ROLE_ADMIN, $this->getRateUrl(99));
}
public function testGetRatesIsSecured()
{
$this->assertUrlIsSecuredForRole(User::ROLE_USER, $this->getRateUrl(1));
}
public function testDeleteRate()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$expectedRates = $this->importTestRates(1);
$this->request($client, $this->getRateUrl(1, 1), 'DELETE');
$this->assertTrue($client->getResponse()->isSuccessful());
$this->assertEmpty($client->getResponse()->getContent());
// fetch rates to validate that one was removed
$this->request($client, $this->getRateUrl(1));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertEquals(count($expectedRates) - 1, count($result));
}
public function testDeleteRateEntityNotFound()
{
$this->assertEntityNotFoundForDelete(User::ROLE_ADMIN, $this->getRateUrl(99, 1));
}
public function testDeleteRateRateNotFound()
{
$this->assertEntityNotFoundForDelete(User::ROLE_ADMIN, $this->getRateUrl(1, 99));
}
public function testDeleteRateWithInvalidAssignment()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->importTestRates(1);
$this->importTestRates(2);
$this->assertNotFoundForDelete($client, $this->getRateUrl(2, 1));
}
protected function assertRateStructure(array $result, $user = null)
{
$expectedKeys = [
'id', 'rate', 'internalRate', 'isFixed', 'user'
];
$actual = array_keys($result);
sort($actual);
sort($expectedKeys);
$this->assertEquals($expectedKeys, $actual, 'Rate structure does not match');
if (null !== $user) {
self::assertIsArray($result['user'], 'Rate user is not an array');
self::assertEquals($user, $result['user']['id'], 'Rate user does not match');
} else {
self::assertNull($result['user']);
}
}
}

View File

@@ -17,9 +17,13 @@ use App\Entity\User;
*/
class StatusControllerTest extends APIControllerBaseTest
{
public function testIsSecure()
public function testIsSecurePing()
{
$this->assertUrlIsSecured('/api/ping');
}
public function testIsSecureVersion()
{
$this->assertUrlIsSecured('/api/version');
}

View File

@@ -12,16 +12,15 @@ namespace App\Tests\API;
use App\Entity\User;
use App\Tests\DataFixtures\TagFixtures;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelBrowser;
/**
* @group integration
*/
class TagControllerTest extends APIControllerBaseTest
{
protected function setUp(): void
protected function importTagFixtures(HttpKernelBrowser $client): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$tagList = ['Test', 'Administration', 'Support', '#2018-001', '#2018-002', '#2018-003', 'Development',
'Marketing', 'First Level Support', 'Bug Fixing'];
@@ -38,6 +37,7 @@ class TagControllerTest extends APIControllerBaseTest
public function testGetCollection()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->importTagFixtures($client);
$this->assertAccessIsGranted($client, '/api/tags');
$result = json_decode($client->getResponse()->getContent(), true);
@@ -50,6 +50,7 @@ class TagControllerTest extends APIControllerBaseTest
public function testEmptyCollection()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->importTagFixtures($client);
$query = ['name' => 'nothing'];
$this->assertAccessIsGranted($client, '/api/tags', 'GET', $query);
$result = json_decode($client->getResponse()->getContent(), true);
@@ -62,6 +63,7 @@ class TagControllerTest extends APIControllerBaseTest
public function testPostAction()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->importTagFixtures($client);
$data = [
'name' => 'foo',
];
@@ -77,6 +79,7 @@ class TagControllerTest extends APIControllerBaseTest
public function testPostActionWithInvalidUser()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->importTagFixtures($client);
$data = [
'name' => 'foo',
];
@@ -91,6 +94,7 @@ class TagControllerTest extends APIControllerBaseTest
public function testPartOfEntries()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->importTagFixtures($client);
$query = ['name' => 'in'];
$this->assertAccessIsGranted($client, '/api/tags', 'GET', $query);
$result = json_decode($client->getResponse()->getContent(), true);
@@ -107,6 +111,7 @@ class TagControllerTest extends APIControllerBaseTest
public function testDeleteAction()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->importTagFixtures($client);
$this->request($client, '/api/tags/1', 'DELETE');
$this->assertTrue($client->getResponse()->isSuccessful());
@@ -121,7 +126,7 @@ class TagControllerTest extends APIControllerBaseTest
public function testDeleteActionWithUnknownTimesheet()
{
$this->assertEntityNotFoundForDelete(User::ROLE_ADMIN, '/api/tags/255', []);
$this->assertEntityNotFoundForDelete(User::ROLE_ADMIN, '/api/tags/255');
}
protected function assertStructure(array $result, $full = true)

View File

@@ -31,8 +31,22 @@ class TeamControllerTest extends APIControllerBaseTest
public function testIsSecure()
{
$this->assertUrlIsSecured('/api/teams');
$this->assertUrlIsSecuredForRole(User::ROLE_USER, '/api/teams');
$this->assertUrlIsSecuredForRole(User::ROLE_TEAMLEAD, '/api/teams');
}
public function getRoleTestData()
{
return [
[User::ROLE_USER],
[User::ROLE_TEAMLEAD],
];
}
/**
* @dataProvider getRoleTestData
*/
public function testIsSecureForRole(string $role)
{
$this->assertUrlIsSecuredForRole($role, '/api/teams');
}
public function testGetCollection()
@@ -66,7 +80,7 @@ class TeamControllerTest extends APIControllerBaseTest
public function testDeleteActionWithUnknownTeam()
{
$this->assertEntityNotFoundForDelete(User::ROLE_ADMIN, '/api/teams/255', []);
$this->assertEntityNotFoundForDelete(User::ROLE_ADMIN, '/api/teams/255');
}
public function testPostAction()
@@ -142,8 +156,6 @@ class TeamControllerTest extends APIControllerBaseTest
$this->assertTrue($client->getResponse()->isSuccessful());
self::assertEquals(Response::HTTP_NO_CONTENT, $client->getResponse()->getStatusCode());
$this->assertEmpty($client->getResponse()->getContent());
$this->assertEntityNotFound(User::ROLE_ADMIN, '/api/teams/' . $id);
}
public function testPostMemberAction()
@@ -334,7 +346,7 @@ class TeamControllerTest extends APIControllerBaseTest
$customer->setVisible(false);
$customer->setCountry('DE');
$customer->setTimezone('Europe/Berlin');
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
$em->persist($customer);
$em->flush();
@@ -472,7 +484,7 @@ class TeamControllerTest extends APIControllerBaseTest
$project->setName('foooo');
$project->setVisible(false);
$project->setCustomer($customer);
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
$em->persist($customer);
$em->persist($project);
$em->flush();

View File

@@ -19,7 +19,6 @@ use App\Entity\User;
use App\Tests\DataFixtures\TimesheetFixtures;
use App\Tests\Mocks\Security\UserDateTimeFactoryFactory;
use App\Tests\Mocks\TimesheetTestMetaFieldSubscriberMock;
use App\Timesheet\UserDateTimeFactory;
use Symfony\Component\HttpFoundation\Response;
/**
@@ -31,21 +30,9 @@ class TimesheetControllerTest extends APIControllerBaseTest
public const DATE_FORMAT_HTML5 = 'Y-m-d\TH:i:s';
public const TEST_TIMEZONE = 'Europe/London';
/**
* @var UserDateTimeFactory
*/
protected $dateTime;
protected function setUp(): void
{
$this->importFixtureForUser(User::ROLE_USER);
$this->dateTime = (new UserDateTimeFactoryFactory($this))->create(self::TEST_TIMEZONE);
}
protected function importFixtureForUser(string $role)
{
$client = $this->getClientForAuthenticatedUser($role);
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
$fixture = new TimesheetFixtures();
$fixture
@@ -56,7 +43,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
->setStartDate((new \DateTime('first day of this month'))->setTime(0, 0, 1))
->setAllowEmptyDescriptions(false)
;
$this->importFixture($client, $fixture);
$this->importFixture($this, $fixture);
}
public function testIsSecure()
@@ -67,6 +54,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
public function testGetCollection()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->importFixtureForUser(User::ROLE_USER);
$this->assertAccessIsGranted($client, '/api/timesheets');
$result = json_decode($client->getResponse()->getContent(), true);
@@ -79,6 +67,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
public function testGetCollectionFull()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->importFixtureForUser(User::ROLE_USER);
$this->assertAccessIsGranted($client, '/api/timesheets', 'GET', ['full' => 'true']);
$result = json_decode($client->getResponse()->getContent(), true);
@@ -92,7 +81,8 @@ class TimesheetControllerTest extends APIControllerBaseTest
public function testGetCollectionForOtherUser()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$this->importFixtureForUser(User::ROLE_USER);
$em = $this->getEntityManager();
$fixture = new TimesheetFixtures();
$fixture
@@ -117,7 +107,8 @@ class TimesheetControllerTest extends APIControllerBaseTest
public function testGetCollectionForAllUser()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$this->importFixtureForUser(User::ROLE_USER);
$em = $this->getEntityManager();
$fixture = new TimesheetFixtures();
$fixture
@@ -172,6 +163,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
];
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->importFixtureForUser(User::ROLE_USER);
$this->assertAccessIsGranted($client, '/api/timesheets', 'GET', $query);
$result = json_decode($client->getResponse()->getContent(), true);
@@ -181,7 +173,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->assertDefaultStructure($result[0], false);
}
public function testGetCollectionWithDeprecatedQuery()
public function testGetCollectionWithSingleParamsQuery()
{
$begin = new \DateTime('first day of this month');
$begin->setTime(0, 0, 0);
@@ -203,6 +195,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
];
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->importFixtureForUser(User::ROLE_USER);
$this->assertAccessIsGranted($client, '/api/timesheets', 'GET', $query);
$result = json_decode($client->getResponse()->getContent(), true);
@@ -215,7 +208,8 @@ class TimesheetControllerTest extends APIControllerBaseTest
public function testExportedFilter()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$this->importFixtureForUser(User::ROLE_USER);
$em = $this->getEntityManager();
$fixture = new TimesheetFixtures();
$fixture
@@ -240,7 +234,6 @@ class TimesheetControllerTest extends APIControllerBaseTest
'exported' => 1,
];
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->assertAccessIsGranted($client, '/api/timesheets', 'GET', $query);
$result = json_decode($client->getResponse()->getContent(), true);
@@ -257,7 +250,6 @@ class TimesheetControllerTest extends APIControllerBaseTest
'exported' => 0,
];
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->assertAccessIsGranted($client, '/api/timesheets', 'GET', $query);
$result = json_decode($client->getResponse()->getContent(), true);
@@ -272,7 +264,6 @@ class TimesheetControllerTest extends APIControllerBaseTest
'begin' => $begin->format(self::DATE_FORMAT_HTML5),
'end' => $end->format(self::DATE_FORMAT_HTML5),
];
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->assertAccessIsGranted($client, '/api/timesheets', 'GET', $query);
$result = json_decode($client->getResponse()->getContent(), true);
@@ -285,6 +276,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
public function testGetEntity()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->importFixtureForUser(User::ROLE_USER);
$this->assertAccessIsGranted($client, '/api/timesheets/1');
$result = json_decode($client->getResponse()->getContent(), true);
@@ -294,15 +286,17 @@ class TimesheetControllerTest extends APIControllerBaseTest
public function testGetEntityAccessDenied()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->importFixtureForUser(User::ROLE_USER);
$this->importFixtureForUser(User::ROLE_ADMIN);
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->assertApiAccessDenied($client, '/api/timesheets/15', 'You are not allowed to view this timesheet');
}
public function testGetEntityAccessAllowedForAdmin()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->importFixtureForUser(User::ROLE_USER);
$this->assertAccessIsGranted($client, '/api/timesheets/1');
$result = json_decode($client->getResponse()->getContent(), true);
@@ -317,12 +311,13 @@ class TimesheetControllerTest extends APIControllerBaseTest
public function testPostAction()
{
$dateTime = (new UserDateTimeFactoryFactory($this))->create(self::TEST_TIMEZONE);
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$data = [
'activity' => 1,
'project' => 1,
'begin' => ($this->dateTime->createDateTime('- 16 hours'))->format('Y-m-d H:m:0'),
'end' => ($this->dateTime->createDateTime())->format('Y-m-d H:m:0'),
'begin' => ($dateTime->createDateTime('- 16 hours'))->format('Y-m-d H:m:0'),
'end' => ($dateTime->createDateTime())->format('Y-m-d H:m:0'),
'description' => 'foo',
'fixedRate' => 2016,
'hourlyRate' => 127
@@ -344,7 +339,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
$customer = (new Customer())->setName('foo-bar-1')->setVisible(false)->setCountry('DE')->setTimezone('Europe/Berlin');
$em->persist($customer);
$project = (new Project())->setName('foo-bar-2')->setVisible(true)->setCustomer($customer);
@@ -372,7 +367,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
$customer = (new Customer())->setName('foo-bar-1')->setVisible(true)->setCountry('DE')->setTimezone('Europe/Berlin');
$em->persist($customer);
$project = (new Project())->setName('foo-bar-2')->setVisible(true)->setCustomer($customer);
@@ -396,12 +391,14 @@ class TimesheetControllerTest extends APIControllerBaseTest
public function testPatchAction()
{
$dateTime = (new UserDateTimeFactoryFactory($this))->create(self::TEST_TIMEZONE);
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
$this->importFixtureForUser(User::ROLE_USER);
$data = [
'activity' => 1,
'project' => 1,
'begin' => ($this->dateTime->createDateTime('- 7 hours'))->format('Y-m-d\TH:m:0'),
'end' => ($this->dateTime->createDateTime())->format('Y-m-d\TH:m:0'),
'begin' => ($dateTime->createDateTime('- 7 hours'))->format('Y-m-d\TH:m:0'),
'end' => ($dateTime->createDateTime())->format('Y-m-d\TH:m:0'),
'description' => 'foo',
'exported' => true,
];
@@ -419,7 +416,8 @@ class TimesheetControllerTest extends APIControllerBaseTest
public function testPatchActionWithInvalidUser()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$this->importFixtureForUser(User::ROLE_USER);
$em = $this->getEntityManager();
$fixture = new TimesheetFixtures();
$fixture
@@ -456,6 +454,8 @@ class TimesheetControllerTest extends APIControllerBaseTest
public function testInvalidPatchAction()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->importFixtureForUser(User::ROLE_USER);
$data = [
'activity' => 10,
'project' => 1,
@@ -475,6 +475,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
public function testDeleteAction()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->importFixtureForUser(User::ROLE_USER);
$this->assertAccessIsGranted($client, '/api/timesheets/1');
$result = json_decode($client->getResponse()->getContent(), true);
@@ -487,32 +488,31 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->assertTrue($client->getResponse()->isSuccessful());
$this->assertEquals(Response::HTTP_NO_CONTENT, $client->getResponse()->getStatusCode());
$this->assertEmpty($client->getResponse()->getContent());
$this->assertEntityNotFound(User::ROLE_USER, '/api/timesheets/' . $id);
}
public function testDeleteActionWithUnknownTimesheet()
{
$this->assertEntityNotFoundForDelete(User::ROLE_ADMIN, '/api/timesheets/255', []);
$this->assertEntityNotFoundForDelete(User::ROLE_ADMIN, '/api/timesheets/255');
}
public function testDeleteActionForDifferentUser()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->importFixtureForUser(User::ROLE_USER);
$id = 1;
$this->request($client, '/api/timesheets/' . $id, 'DELETE');
$this->assertTrue($client->getResponse()->isSuccessful());
$this->assertEquals(Response::HTTP_NO_CONTENT, $client->getResponse()->getStatusCode());
$this->assertEmpty($client->getResponse()->getContent());
$this->assertEntityNotFound(User::ROLE_USER, '/api/timesheets/' . $id);
}
public function testDeleteActionWithoutAuthorization()
{
$this->importFixtureForUser(User::ROLE_ADMIN);
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->importFixtureForUser(User::ROLE_USER);
$this->importFixtureForUser(User::ROLE_ADMIN);
$this->request($client, '/api/timesheets/15', 'DELETE');
@@ -526,8 +526,9 @@ class TimesheetControllerTest extends APIControllerBaseTest
public function testDeleteActionForExportedRecordIsNotAllowed()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->importFixtureForUser(User::ROLE_USER);
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
/** @var Timesheet $timesheet */
$timesheet = $em->getRepository(Timesheet::class)->find(1);
$timesheet->setExported(true);
@@ -541,8 +542,9 @@ class TimesheetControllerTest extends APIControllerBaseTest
public function testDeleteActionForExportedRecordIsAllowedForAdmin()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->importFixtureForUser(User::ROLE_USER);
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
/** @var Timesheet $timesheet */
$timesheet = $em->getRepository(Timesheet::class)->find(1);
$timesheet->setExported(true);
@@ -556,7 +558,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
public function testGetRecentCollectionWithSubresources()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
$start = new \DateTime('-10 days');
@@ -589,7 +591,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
public function testActiveAction()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
$start = new \DateTime('-10 days');
@@ -617,7 +619,8 @@ class TimesheetControllerTest extends APIControllerBaseTest
public function testStopAction()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$this->importFixtureForUser(User::ROLE_USER);
$em = $this->getEntityManager();
$start = new \DateTime('-10 days');
@@ -635,7 +638,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->request($client, '/api/timesheets/11/stop', 'PATCH');
$this->assertTrue($client->getResponse()->isSuccessful());
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
/** @var Timesheet $timesheet */
$timesheet = $em->getRepository(Timesheet::class)->find(1);
$this->assertInstanceOf(\DateTime::class, $timesheet->getEnd());
@@ -644,6 +647,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
public function testStopActionFailsOnStoppedEntry()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->importFixtureForUser(User::ROLE_USER);
$this->request($client, '/api/timesheets/1/stop', 'PATCH');
$this->assertApiException($client->getResponse(), 'Timesheet entry already stopped');
@@ -657,7 +661,8 @@ class TimesheetControllerTest extends APIControllerBaseTest
public function testStopNotAllowedForUser()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$this->importFixtureForUser(User::ROLE_USER);
$em = $this->getEntityManager();
$start = new \DateTime('-10 days');
@@ -679,7 +684,8 @@ class TimesheetControllerTest extends APIControllerBaseTest
public function testGetCollectionWithTags()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$this->importFixtureForUser(User::ROLE_USER);
$em = $this->getEntityManager();
$fixture = new TimesheetFixtures();
$fixture
@@ -724,6 +730,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
public function testRestartAction()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->importFixtureForUser(User::ROLE_USER);
$data = [
'description' => 'foo',
@@ -739,7 +746,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->assertEmpty($result['description']);
$this->assertEmpty($result['tags']);
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
/** @var Timesheet $timesheet */
$timesheet = $em->getRepository(Timesheet::class)->find($result['id']);
$this->assertInstanceOf(\DateTime::class, $timesheet->getBegin());
@@ -753,8 +760,9 @@ class TimesheetControllerTest extends APIControllerBaseTest
public function testRestartActionWithCopyData()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->importFixtureForUser(User::ROLE_USER);
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
/** @var Timesheet $timesheet */
$timesheet = $em->getRepository(Timesheet::class)->find(1);
$timesheet->setDescription('foo');
@@ -779,7 +787,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->assertEquals([['name' => 'sdfsdf', 'value' => 'nnnnn'], ['name' => '1234567890', 'value' => '1234567890']], $result['metaFields']);
$this->assertEquals(['another', 'testing', 'bar'], $result['tags']);
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
/** @var Timesheet $timesheet */
$timesheet = $em->getRepository(Timesheet::class)->find($result['id']);
$this->assertInstanceOf(\DateTime::class, $timesheet->getBegin());
@@ -793,7 +801,8 @@ class TimesheetControllerTest extends APIControllerBaseTest
public function testRestartNotAllowedForUser()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
$start = new \DateTime('-10 days');
@@ -808,7 +817,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
;
$this->importFixture($em, $fixture);
$this->request($client, '/api/timesheets/12/restart', 'PATCH');
$this->request($client, '/api/timesheets/2/restart', 'PATCH');
$this->assertApiResponseAccessDenied($client->getResponse(), 'You are not allowed to re-start this timesheet');
}
@@ -819,12 +828,13 @@ class TimesheetControllerTest extends APIControllerBaseTest
public function testDuplicateAction()
{
$dateTime = (new UserDateTimeFactoryFactory($this))->create(self::TEST_TIMEZONE);
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$data = [
'activity' => 1,
'project' => 1,
'begin' => ($this->dateTime->createDateTime('- 16 hours'))->format('Y-m-d H:m:0'),
'end' => ($this->dateTime->createDateTime())->format('Y-m-d H:m:0'),
'begin' => ($dateTime->createDateTime('- 16 hours'))->format('Y-m-d H:m:0'),
'end' => ($dateTime->createDateTime())->format('Y-m-d H:m:0'),
'description' => 'foo',
'fixedRate' => 2016,
'hourlyRate' => 127
@@ -858,32 +868,34 @@ class TimesheetControllerTest extends APIControllerBaseTest
public function testExportAction()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->importFixtureForUser(User::ROLE_USER);
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
/** @var Timesheet $timesheet */
$timesheet = $em->getRepository(Timesheet::class)->find(1);
$this->assertEquals(false, $timesheet->isExported());
$this->assertFalse($timesheet->isExported());
$this->request($client, '/api/timesheets/1/export', 'PATCH');
$this->assertTrue($client->getResponse()->isSuccessful());
$this->assertDefaultStructure(json_decode($client->getResponse()->getContent(), true), true);
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em->clear();
/** @var Timesheet $timesheet */
$timesheet = $em->getRepository(Timesheet::class)->find(1);
$this->assertEquals(true, $timesheet->isExported());
$this->assertTrue($timesheet->isExported());
$this->request($client, '/api/timesheets/1/export', 'PATCH');
$this->assertTrue($client->getResponse()->isSuccessful());
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em->clear();
$timesheet = $em->getRepository(Timesheet::class)->find(1);
$this->assertEquals(false, $timesheet->isExported());
$this->assertFalse($timesheet->isExported());
}
public function testExportNotAllowedForUser()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->importFixtureForUser(User::ROLE_USER);
$this->request($client, '/api/timesheets/1/export', 'PATCH');
$this->assertApiResponseAccessDenied($client->getResponse(), 'You are not allowed to lock this timesheet');
@@ -901,7 +913,10 @@ class TimesheetControllerTest extends APIControllerBaseTest
public function testMetaActionThrowsExceptionOnMissingName()
{
return $this->assertExceptionForPatchAction(User::ROLE_ADMIN, '/api/timesheets/1/meta', ['value' => 'X'], [
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->importFixtureForUser(User::ROLE_USER);
return $this->assertExceptionForMethod($client, '/api/timesheets/1/meta', 'PATCH', ['value' => 'X'], [
'code' => 400,
'message' => 'Parameter "name" of value "NULL" violated a constraint "This value should not be null."'
]);
@@ -909,7 +924,10 @@ class TimesheetControllerTest extends APIControllerBaseTest
public function testMetaActionThrowsExceptionOnMissingValue()
{
return $this->assertExceptionForPatchAction(User::ROLE_ADMIN, '/api/timesheets/1/meta', ['name' => 'X'], [
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->importFixtureForUser(User::ROLE_USER);
return $this->assertExceptionForMethod($client, '/api/timesheets/1/meta', 'PATCH', ['name' => 'X'], [
'code' => 400,
'message' => 'Parameter "value" of value "NULL" violated a constraint "This value should not be null."'
]);
@@ -917,7 +935,10 @@ class TimesheetControllerTest extends APIControllerBaseTest
public function testMetaActionThrowsExceptionOnMissingMetafield()
{
return $this->assertExceptionForPatchAction(User::ROLE_ADMIN, '/api/timesheets/1/meta', ['name' => 'X', 'value' => 'Y'], [
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->importFixtureForUser(User::ROLE_USER);
return $this->assertExceptionForMethod($client, '/api/timesheets/1/meta', 'PATCH', ['name' => 'X', 'value' => 'Y'], [
'code' => 500,
'message' => 'Unknown meta-field requested'
]);
@@ -926,7 +947,8 @@ class TimesheetControllerTest extends APIControllerBaseTest
public function testMetaAction()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
static::$kernel->getContainer()->get('event_dispatcher')->addSubscriber(new TimesheetTestMetaFieldSubscriberMock());
$this->importFixtureForUser(User::ROLE_USER);
static::$container->get('event_dispatcher')->addSubscriber(new TimesheetTestMetaFieldSubscriberMock());
$data = [
'name' => 'metatestmock',
@@ -936,7 +958,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->assertTrue($client->getResponse()->isSuccessful());
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
/** @var Timesheet $timesheet */
$timesheet = $em->getRepository(Timesheet::class)->find(1);
$this->assertEquals('another,testing,bar', $timesheet->getMetaField('metatestmock')->getValue());
@@ -945,7 +967,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
protected function assertDefaultStructure(array $result, $full = true)
{
$expectedKeys = [
'id', 'begin', 'end', 'duration', 'description', 'rate', 'activity', 'project', 'tags', 'user', 'metaFields'
'id', 'begin', 'end', 'duration', 'description', 'rate', 'activity', 'project', 'tags', 'user', 'metaFields', 'internalRate'
];
if ($full) {

View File

@@ -20,9 +20,23 @@ class UserControllerTest extends APIControllerBaseTest
public function testIsSecure()
{
$this->assertUrlIsSecured('/api/users');
$this->assertUrlIsSecuredForRole(User::ROLE_USER, '/api/users');
$this->assertUrlIsSecuredForRole(User::ROLE_TEAMLEAD, '/api/users');
$this->assertUrlIsSecuredForRole(User::ROLE_ADMIN, '/api/users');
}
public function getRoleTestData()
{
return [
[User::ROLE_USER],
[User::ROLE_TEAMLEAD],
[User::ROLE_ADMIN],
];
}
/**
* @dataProvider getRoleTestData
*/
public function testIsSecureForRole(string $role)
{
$this->assertUrlIsSecuredForRole($role, '/api/users');
}
public function testGetCollection()
@@ -187,7 +201,6 @@ class UserControllerTest extends APIControllerBaseTest
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
$data = [
'avatar' => 'test321',
'title' => 'qwertzui',

View File

@@ -179,7 +179,7 @@ class InvoiceCreateCommandTest extends KernelTestCase
protected function prepareFixtures(\DateTime $start)
{
$em = self::$container->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
$fixture = new CustomerFixtures();
$fixture->setAmount(1);
@@ -197,7 +197,7 @@ class InvoiceCreateCommandTest extends KernelTestCase
$this->importFixture($em, $fixture);
$fixture = new TimesheetFixtures();
$fixture->setUser($this->getUserByName($em, UserFixtures::USERNAME_SUPER_ADMIN));
$fixture->setUser($this->getUserByName(UserFixtures::USERNAME_SUPER_ADMIN));
$fixture->setAmount(20);
$fixture->setStartDate($start);
$fixture->setProjects([$em->getRepository(Project::class)->find(2)]);

View File

@@ -28,6 +28,10 @@ class ActivityControllerTest extends ControllerBaseTest
public function testIsSecure()
{
$this->assertUrlIsSecured('/admin/activity/');
}
public function testIsSecureForRole()
{
$this->assertUrlIsSecuredForRole(User::ROLE_USER, '/admin/activity/');
}
@@ -52,7 +56,6 @@ class ActivityControllerTest extends ControllerBaseTest
});
$this->importFixture($client, $fixture);
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->assertAccessIsGranted($client, '/admin/activity/');
$form = $client->getCrawler()->filter('form.header-search')->form();
@@ -74,7 +77,7 @@ class ActivityControllerTest extends ControllerBaseTest
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
/** @var EntityManager $em */
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
$fixture = new TimesheetFixtures();
$fixture->setAmount(10);
@@ -88,7 +91,6 @@ class ActivityControllerTest extends ControllerBaseTest
$fixture->setProjects([$project]);
$this->importFixture($client, $fixture);
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->assertAccessIsGranted($client, '/admin/activity/1/details');
self::assertHasProgressbar($client);
@@ -120,34 +122,6 @@ class ActivityControllerTest extends ControllerBaseTest
self::assertStringContainsString('123.45', $node->text(null, true));
}
public function testDeleteRateAction()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->assertAccessIsGranted($client, '/admin/activity/1/rate');
$form = $client->getCrawler()->filter('form[name=activity_rate_form]')->form();
$client->submit($form, [
'activity_rate_form' => [
'user' => null,
'rate' => 123.45,
]
]);
$this->assertIsRedirect($client, $this->createUrl('/admin/activity/1/details'));
$client->followRedirect();
$node = $client->getCrawler()->filter('div.box#activity_rates_box');
self::assertEquals(1, $node->count());
$node = $client->getCrawler()->filter('div.box#activity_rates_box table.dataTable tbody tr:not(.summary)');
self::assertEquals(1, $node->count());
self::assertStringContainsString('123.45', $node->text(null, true));
$node = $client->getCrawler()->filter('div.box#activity_rates_box table.dataTable tbody tr td.actions a');
self::assertEquals(1, $node->count());
$url = $node->attr('href');
$client->request('GET', $url);
$this->assertIsRedirect($client, $this->createUrl('/admin/activity/1/details'));
$node = $client->getCrawler()->filter('div.box#activity_rates_box table.dataTable tbody tr:not(.summary)');
self::assertEquals(0, $node->count());
}
public function testCreateAction()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
@@ -285,7 +259,7 @@ class ActivityControllerTest extends ControllerBaseTest
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
/** @var EntityManager $em */
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
$fixture = new TimesheetFixtures();
$fixture->setUser($this->getUserByRole($em, User::ROLE_USER));
@@ -326,7 +300,7 @@ class ActivityControllerTest extends ControllerBaseTest
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
/** @var EntityManager $em */
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
$fixture = new TimesheetFixtures();
$fixture->setUser($this->getUserByRole($em, User::ROLE_USER));

View File

@@ -29,6 +29,10 @@ class CustomerControllerTest extends ControllerBaseTest
public function testIsSecure()
{
$this->assertUrlIsSecured('/admin/customer/');
}
public function testIsSecureForRole()
{
$this->assertUrlIsSecuredForRole(User::ROLE_USER, '/admin/customer/');
}
@@ -53,7 +57,6 @@ class CustomerControllerTest extends ControllerBaseTest
});
$this->importFixture($client, $fixture);
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->assertAccessIsGranted($client, '/admin/customer/');
$form = $client->getCrawler()->filter('form.header-search')->form();
@@ -111,34 +114,6 @@ class CustomerControllerTest extends ControllerBaseTest
self::assertStringContainsString('123.45', $node->text(null, true));
}
public function testDeleteRateAction()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->assertAccessIsGranted($client, '/admin/customer/1/rate');
$form = $client->getCrawler()->filter('form[name=customer_rate_form]')->form();
$client->submit($form, [
'customer_rate_form' => [
'user' => null,
'rate' => 123.45,
]
]);
$this->assertIsRedirect($client, $this->createUrl('/admin/customer/1/details'));
$client->followRedirect();
$node = $client->getCrawler()->filter('div.box#customer_rates_box');
self::assertEquals(1, $node->count());
$node = $client->getCrawler()->filter('div.box#customer_rates_box table.dataTable tbody tr:not(.summary)');
self::assertEquals(1, $node->count());
self::assertStringContainsString('123.45', $node->text(null, true));
$node = $client->getCrawler()->filter('div.box#customer_rates_box table.dataTable tbody tr td.actions a');
self::assertEquals(1, $node->count());
$url = $node->attr('href');
$client->request('GET', $url);
$this->assertIsRedirect($client, $this->createUrl('/admin/customer/1/details'));
$node = $client->getCrawler()->filter('div.box#customer_rates_box table.dataTable tbody tr:not(.summary)');
self::assertEquals(0, $node->count());
}
public function testAddCommentAction()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
@@ -172,7 +147,6 @@ class CustomerControllerTest extends ControllerBaseTest
$node = $client->getCrawler()->filter('div.box#comments_box .box-comment a.confirmation-link');
self::assertEquals($this->createUrl('/admin/customer/1/comment_delete'), $node->attr('href'));
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->request($client, '/admin/customer/1/comment_delete');
$this->assertIsRedirect($client, $this->createUrl('/admin/customer/1/details'));
$client->followRedirect();
@@ -197,7 +171,6 @@ class CustomerControllerTest extends ControllerBaseTest
$node = $client->getCrawler()->filter('div.box#comments_box .box-comment a.btn.active');
self::assertEquals(0, $node->count());
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->request($client, '/admin/customer/1/comment_pin');
$this->assertIsRedirect($client, $this->createUrl('/admin/customer/1/details'));
$client->followRedirect();
@@ -213,7 +186,6 @@ class CustomerControllerTest extends ControllerBaseTest
$node = $client->getCrawler()->filter('div.box#team_listing_box .box-body');
self::assertStringContainsString('Visible to everyone, as no team was assigned yet.', $node->text(null, true));
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->request($client, '/admin/customer/1/create_team');
$this->assertIsRedirect($client, $this->createUrl('/admin/customer/1/details'));
$client->followRedirect();
@@ -231,7 +203,7 @@ class CustomerControllerTest extends ControllerBaseTest
self::assertEquals(1, $node->count());
/** @var EntityManager $em */
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
$customer = $em->getRepository(Customer::class)->find(1);
$fixture = new ProjectFixtures();
@@ -239,7 +211,6 @@ class CustomerControllerTest extends ControllerBaseTest
$fixture->setCustomers([$customer]);
$this->importFixture($client, $fixture);
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->assertAccessIsGranted($client, '/admin/customer/1/projects/1');
$node = $client->getCrawler()->filter('div.box#project_list_box .box-tools ul.pagination li');
@@ -309,7 +280,7 @@ class CustomerControllerTest extends ControllerBaseTest
public function testTeamPermissionAction()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
/** @var Customer $customer */
$customer = $em->getRepository(Customer::class)->find(1);
@@ -368,7 +339,7 @@ class CustomerControllerTest extends ControllerBaseTest
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
$fixture = new TimesheetFixtures();
$fixture->setUser($this->getUserByRole($em, User::ROLE_USER));
$fixture->setAmount(10);
@@ -407,7 +378,7 @@ class CustomerControllerTest extends ControllerBaseTest
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
$fixture = new TimesheetFixtures();
$fixture->setUser($this->getUserByRole($em, User::ROLE_USER));
$fixture->setAmount(10);

View File

@@ -19,6 +19,10 @@ class DoctorControllerTest extends ControllerBaseTest
public function testDoctorIsSecure()
{
$this->assertUrlIsSecured('/doctor');
}
public function testDoctorIsSecureForRole()
{
$this->assertUrlIsSecuredForRole(User::ROLE_ADMIN, '/doctor');
}

View File

@@ -23,6 +23,10 @@ class ExportControllerTest extends ControllerBaseTest
public function testIsSecure()
{
$this->assertUrlIsSecured('/export/');
}
public function testIsSecureForrole()
{
$this->assertUrlIsSecuredForRole(User::ROLE_USER, '/export/');
}
@@ -39,7 +43,7 @@ class ExportControllerTest extends ControllerBaseTest
public function testIndexActionWithEntriesAndTeams()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
$teamlead = $this->getUserByRole($em, User::ROLE_TEAMLEAD);
$user = $this->getUserByRole($em, User::ROLE_USER);
@@ -88,7 +92,7 @@ class ExportControllerTest extends ControllerBaseTest
$this->assertDataTableRowCount($client, 'datatable_export', 23);
// assert export type buttons are available
$expected = ['csv', 'default.html.twig', 'default-budget.pdf.twig', 'default.pdf.twig', 'xlsx'];
$expected = ['csv', 'default.html.twig', 'default-budget.pdf.twig', 'default-internal.pdf.twig', 'default.pdf.twig', 'xlsx'];
$node = $client->getCrawler()->filter('#export-buttons .startExportBtn');
$this->assertEquals(count($expected), $node->count());
/** @var \DOMElement $button */
@@ -101,7 +105,7 @@ class ExportControllerTest extends ControllerBaseTest
public function testIndexActionWithEntriesForTeamleadDoesNotShowUserWithoutTeam()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
$begin = new \DateTime('first day of this month');
$user = $this->getUserByRole($em, User::ROLE_USER);
@@ -140,7 +144,7 @@ class ExportControllerTest extends ControllerBaseTest
$this->assertDataTableRowCount($client, 'datatable_export', 3);
// assert export type buttons are available
$expected = ['csv', 'default.html.twig', 'default-budget.pdf.twig', 'default.pdf.twig', 'xlsx'];
$expected = ['csv', 'default.html.twig', 'default-budget.pdf.twig', 'default-internal.pdf.twig', 'default.pdf.twig', 'xlsx'];
$node = $client->getCrawler()->filter('#export-buttons .startExportBtn');
$this->assertEquals(count($expected), $node->count());
/** @var \DOMElement $button */
@@ -187,7 +191,7 @@ class ExportControllerTest extends ControllerBaseTest
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
/** @var EntityManager $em */
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
$begin = new \DateTime('first day of this month');
$fixture = new TimesheetFixtures();

View File

@@ -35,7 +35,7 @@ class HomepageControllerTest extends ControllerBaseTest
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
$user = $this->getUserByRole($em, User::ROLE_USER);
$pref = (new UserPreference())

View File

@@ -50,6 +50,10 @@ class InvoiceControllerTest extends ControllerBaseTest
public function testIsSecure()
{
$this->assertUrlIsSecured('/invoice/');
}
public function testIsSecureForRole()
{
$this->assertUrlIsSecuredForRole(User::ROLE_USER, '/invoice/');
}
@@ -113,7 +117,7 @@ class InvoiceControllerTest extends ControllerBaseTest
public function testCopyTemplateAction()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
$fixture = new InvoiceFixtures();
$this->importFixture($client, $fixture);
@@ -141,7 +145,7 @@ class InvoiceControllerTest extends ControllerBaseTest
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
/** @var EntityManager $em */
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
$fixture = new InvoiceFixtures();
$this->importFixture($client, $fixture);
@@ -207,7 +211,7 @@ class InvoiceControllerTest extends ControllerBaseTest
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
/** @var EntityManager $em */
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
$fixture = new InvoiceFixtures();
$this->importFixture($client, $fixture);
@@ -266,7 +270,7 @@ class InvoiceControllerTest extends ControllerBaseTest
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
/** @var EntityManager $em */
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
$fixture = new InvoiceFixtures();
$this->importFixture($client, $fixture);
@@ -378,7 +382,7 @@ class InvoiceControllerTest extends ControllerBaseTest
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
$fixture = new InvoiceFixtures();
$this->importFixture($client, $fixture);
@@ -396,7 +400,7 @@ class InvoiceControllerTest extends ControllerBaseTest
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
$fixture = new InvoiceFixtures();
$this->importFixture($client, $fixture);

View File

@@ -21,7 +21,7 @@ class LayoutControllerTest extends ControllerBaseTest
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
$user = $this->getUserByRole($em, User::ROLE_USER);
$this->request($client, '/dashboard/');
@@ -77,7 +77,7 @@ class LayoutControllerTest extends ControllerBaseTest
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
$user = $this->getUserByRole($em, User::ROLE_USER);
$this->request($client, '/layou/active_entries');

View File

@@ -11,16 +11,19 @@ namespace App\Tests\Controller;
use App\Entity\RolePermission;
use App\Entity\User;
use Doctrine\ORM\EntityManager;
/**
* @group integration
*/
class PermissionControllerTest extends ControllerBaseTest
{
public function testPermissionsIsSecure()
public function testIsSecure()
{
$this->assertUrlIsSecured('/admin/permissions');
}
public function testIsSecureForRole()
{
$this->assertUrlIsSecuredForRole(User::ROLE_ADMIN, '/admin/permissions');
}
@@ -47,6 +50,10 @@ class PermissionControllerTest extends ControllerBaseTest
public function testCreateRoleIsSecured()
{
$this->assertUrlIsSecured('/admin/permissions/roles/create');
}
public function testCreateRoleIsSecuredForRole()
{
$this->assertUrlIsSecuredForRole(User::ROLE_ADMIN, '/admin/permissions');
}
@@ -75,6 +82,10 @@ class PermissionControllerTest extends ControllerBaseTest
public function testDeleteRoleIsSecured()
{
$this->assertUrlIsSecured('/admin/permissions/roles/1/delete');
}
public function testDeleteRoleIsSecuredForRole()
{
$this->assertUrlIsSecuredForRole(User::ROLE_ADMIN, '/admin/permissions');
}
@@ -106,6 +117,10 @@ class PermissionControllerTest extends ControllerBaseTest
public function testSavePermissionIsSecured()
{
$this->assertUrlIsSecured('/admin/permissions/roles/1/view_user/1');
}
public function testSavePermissionIsSecuredForRole()
{
$this->assertUrlIsSecuredForRole(User::ROLE_ADMIN, '/admin/permissions');
}
@@ -121,8 +136,7 @@ class PermissionControllerTest extends ControllerBaseTest
]);
$this->assertIsRedirect($client, $this->createUrl('/admin/permissions'));
/** @var EntityManager $em */
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
$rolePermissions = $em->getRepository(RolePermission::class)->findAll();
$this->assertEquals(0, count($rolePermissions));

View File

@@ -21,6 +21,10 @@ class PluginControllerTest extends ControllerBaseTest
public function testIsSecure()
{
$this->assertUrlIsSecured('/admin/plugins/');
}
public function testIsSecureForRole()
{
$this->assertUrlIsSecuredForRole(User::ROLE_ADMIN, '/admin/plugins/');
}

View File

@@ -48,7 +48,7 @@ class ProfileControllerTest extends ControllerBaseTest
new \DateTime('-1 year'),
];
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
foreach ($dates as $start) {
$fixture = new TimesheetFixtures();
@@ -133,7 +133,7 @@ class ProfileControllerTest extends ControllerBaseTest
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->request($client, '/profile/' . UserFixtures::USERNAME_USER . '/edit');
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
/** @var User $user */
$user = $this->getUserByRole($em, User::ROLE_USER);
@@ -160,7 +160,7 @@ class ProfileControllerTest extends ControllerBaseTest
$this->assertHasFlashSuccess($client);
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
$user = $this->getUserByRole($em, User::ROLE_USER);
$this->assertEquals(UserFixtures::USERNAME_USER, $user->getUsername());
@@ -193,7 +193,7 @@ class ProfileControllerTest extends ControllerBaseTest
$this->assertHasFlashSuccess($client);
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
$user = $this->getUserByRole($em, User::ROLE_USER);
$this->assertEquals(UserFixtures::USERNAME_USER, $user->getUsername());
@@ -209,7 +209,7 @@ class ProfileControllerTest extends ControllerBaseTest
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->request($client, '/profile/' . UserFixtures::USERNAME_USER . '/password');
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
/** @var User $user */
$user = $this->getUserByRole($em, User::ROLE_USER);
@@ -236,7 +236,7 @@ class ProfileControllerTest extends ControllerBaseTest
$this->assertHasFlashSuccess($client);
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
$user = $this->getUserByRole($em, User::ROLE_USER);
$this->assertFalse($passwordEncoder->getEncoder($user)->isPasswordValid($user->getPassword(), UserFixtures::DEFAULT_PASSWORD, $user->getSalt()));
@@ -248,7 +248,7 @@ class ProfileControllerTest extends ControllerBaseTest
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->request($client, '/profile/' . UserFixtures::USERNAME_USER . '/api-token');
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
/** @var User $user */
$user = $this->getUserByRole($em, User::ROLE_USER);
/** @var EncoderFactoryInterface $passwordEncoder */
@@ -274,7 +274,7 @@ class ProfileControllerTest extends ControllerBaseTest
$this->assertHasFlashSuccess($client);
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
$user = $this->getUserByRole($em, User::ROLE_USER);
$this->assertFalse($passwordEncoder->getEncoder($user)->isPasswordValid($user->getApiToken(), UserFixtures::DEFAULT_API_TOKEN, $user->getSalt()));
@@ -293,7 +293,7 @@ class ProfileControllerTest extends ControllerBaseTest
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
$this->request($client, '/profile/' . UserFixtures::USERNAME_USER . '/roles');
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
/** @var User $user */
$user = $this->getUserByRole($em, User::ROLE_USER);
@@ -313,7 +313,7 @@ class ProfileControllerTest extends ControllerBaseTest
$this->assertHasFlashSuccess($client);
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
$user = $this->getUserByRole($em, User::ROLE_USER);
$this->assertEquals(['ROLE_TEAMLEAD', 'ROLE_SUPER_ADMIN', 'ROLE_USER'], $user->getRoles());
@@ -322,13 +322,17 @@ class ProfileControllerTest extends ControllerBaseTest
public function testTeamsActionIsSecured()
{
$this->assertUrlIsSecured('/profile/' . UserFixtures::USERNAME_USER . '/teams');
}
public function testTeamsActionIsSecuredForRole()
{
$this->assertUrlIsSecuredForRole(User::ROLE_TEAMLEAD, '/profile/' . UserFixtures::USERNAME_USER . '/teams');
}
public function testTeamsAction()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
/** @var User $user */
$user = $this->getUserByRole($em, User::ROLE_USER);
@@ -359,7 +363,7 @@ class ProfileControllerTest extends ControllerBaseTest
$this->assertHasFlashSuccess($client);
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
$user = $this->getUserByRole($em, User::ROLE_USER);
$this->assertEquals(1, $user->getTeams()->count());
@@ -369,27 +373,27 @@ class ProfileControllerTest extends ControllerBaseTest
{
return [
// assert that the user doesn't have the "hourly-rate_own_profile" permission
[User::ROLE_USER, UserFixtures::USERNAME_USER, 82, 82, 'ar'],
[User::ROLE_USER, UserFixtures::USERNAME_USER, 82, 82, 'ar', null],
// admins are allowed to update their own hourly rate
[User::ROLE_ADMIN, UserFixtures::USERNAME_ADMIN, 81, 37.5, 'ar'],
[User::ROLE_ADMIN, UserFixtures::USERNAME_ADMIN, 81, 37.5, 'ar', 19.54],
// admins are allowed to update other peoples hourly rate
[User::ROLE_SUPER_ADMIN, UserFixtures::USERNAME_USER, 82, 37.5, 'en'],
[User::ROLE_SUPER_ADMIN, UserFixtures::USERNAME_USER, 82, 37.5, 'en', 19.54],
];
}
/**
* @dataProvider getPreferencesTestData
*/
public function testPreferencesAction($role, $username, $hourlyRateOriginal, $hourlyRate, $expectedLocale)
public function testPreferencesAction($role, $username, $hourlyRateOriginal, $hourlyRate, $expectedLocale, $expectedInternalRate)
{
$client = $this->getClientForAuthenticatedUser($role);
$this->request($client, '/profile/' . $username . '/prefs');
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
/** @var User $user */
$user = $this->getUserByName($em, $username);
$user = $this->getUserByName($username);
$this->assertEquals($hourlyRateOriginal, $user->getPreferenceValue(UserPreference::HOURLY_RATE));
$this->assertNull($user->getPreferenceValue(UserPreference::INTERNAL_RATE));
$this->assertNull($user->getPreferenceValue(UserPreference::SKIN));
$this->assertEquals(false, $user->getPreferenceValue('theme.collapsed_sidebar'));
$this->assertEquals('month', $user->getPreferenceValue('calendar.initial_view'));
@@ -399,6 +403,7 @@ class ProfileControllerTest extends ControllerBaseTest
'user_preferences_form' => [
'preferences' => [
['name' => UserPreference::HOURLY_RATE, 'value' => 37.5],
['name' => UserPreference::INTERNAL_RATE, 'value' => 19.54],
['name' => 'timezone', 'value' => 'America/Creston'],
['name' => 'language', 'value' => 'ar'],
['name' => UserPreference::SKIN, 'value' => 'blue'],
@@ -417,10 +422,10 @@ class ProfileControllerTest extends ControllerBaseTest
$this->assertHasFlashSuccess($client);
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$user = $this->getUserByName($em, $username);
$user = $this->getUserByName($username);
$this->assertEquals($hourlyRate, $user->getPreferenceValue(UserPreference::HOURLY_RATE));
$this->assertEquals($expectedInternalRate, $user->getPreferenceValue(UserPreference::INTERNAL_RATE));
$this->assertEquals('', $user->getPreferenceValue('America/Creston'));
$this->assertEquals('ar', $user->getPreferenceValue('language'));
$this->assertEquals('blue', $user->getPreferenceValue(UserPreference::SKIN));

View File

@@ -36,6 +36,10 @@ class ProjectControllerTest extends ControllerBaseTest
public function testIsSecure()
{
$this->assertUrlIsSecured('/admin/project/');
}
public function testIsSecureForRole()
{
$this->assertUrlIsSecuredForRole(User::ROLE_USER, '/admin/project/');
}
@@ -60,7 +64,6 @@ class ProjectControllerTest extends ControllerBaseTest
});
$this->importFixture($client, $fixture);
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->assertAccessIsGranted($client, '/admin/project/');
$form = $client->getCrawler()->filter('form.header-search')->form();
@@ -81,7 +84,7 @@ class ProjectControllerTest extends ControllerBaseTest
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
/** @var EntityManager $em */
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
$project = $em->getRepository(Project::class)->find(1);
@@ -97,7 +100,6 @@ class ProjectControllerTest extends ControllerBaseTest
$fixture->setProjects([$project]);
$this->importFixture($client, $fixture);
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->assertAccessIsGranted($client, '/admin/project/1/details');
self::assertHasProgressbar($client);
@@ -146,7 +148,7 @@ class ProjectControllerTest extends ControllerBaseTest
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
/** @var EntityManager $em */
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
$project = $em->find(Project::class, 1);
$project->setMetaField((new ProjectMeta())->setName('foo')->setValue('bar'));
$project->setEnd(new \DateTime());
@@ -180,34 +182,6 @@ class ProjectControllerTest extends ControllerBaseTest
self::assertStringContainsString('123.45', $node->text(null, true));
}
public function testDeleteRateAction()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->assertAccessIsGranted($client, '/admin/project/1/rate');
$form = $client->getCrawler()->filter('form[name=project_rate_form]')->form();
$client->submit($form, [
'project_rate_form' => [
'user' => null,
'rate' => 123.45,
]
]);
$this->assertIsRedirect($client, $this->createUrl('/admin/project/1/details'));
$client->followRedirect();
$node = $client->getCrawler()->filter('div.box#project_rates_box');
self::assertEquals(1, $node->count());
$node = $client->getCrawler()->filter('div.box#project_rates_box table.dataTable tbody tr:not(.summary)');
self::assertEquals(1, $node->count());
self::assertStringContainsString('123.45', $node->text(null, true));
$node = $client->getCrawler()->filter('div.box#project_rates_box table.dataTable tbody tr td.actions a');
self::assertEquals(1, $node->count());
$url = $node->attr('href');
$client->request('GET', $url);
$this->assertIsRedirect($client, $this->createUrl('/admin/project/1/details'));
$node = $client->getCrawler()->filter('div.box#project_rates_box table.dataTable tbody tr:not(.summary)');
self::assertEquals(0, $node->count());
}
public function testAddCommentAction()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
@@ -241,7 +215,6 @@ class ProjectControllerTest extends ControllerBaseTest
$node = $client->getCrawler()->filter('div.box#comments_box .box-comment a.confirmation-link');
self::assertEquals($this->createUrl('/admin/project/1/comment_delete'), $node->attr('href'));
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->request($client, '/admin/project/1/comment_delete');
$this->assertIsRedirect($client, $this->createUrl('/admin/project/1/details'));
$client->followRedirect();
@@ -266,7 +239,6 @@ class ProjectControllerTest extends ControllerBaseTest
$node = $client->getCrawler()->filter('div.box#comments_box .box-comment a.btn.active');
self::assertEquals(0, $node->count());
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->request($client, '/admin/project/1/comment_pin');
$this->assertIsRedirect($client, $this->createUrl('/admin/project/1/details'));
$client->followRedirect();
@@ -282,7 +254,6 @@ class ProjectControllerTest extends ControllerBaseTest
$node = $client->getCrawler()->filter('div.box#team_listing_box .box-body');
self::assertStringContainsString('Visible to everyone, as no team was assigned yet.', $node->text(null, true));
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->request($client, '/admin/project/1/create_team');
$this->assertIsRedirect($client, $this->createUrl('/admin/project/1/details'));
$client->followRedirect();
@@ -299,14 +270,13 @@ class ProjectControllerTest extends ControllerBaseTest
self::assertEquals('', $client->getResponse()->getContent());
/** @var EntityManager $em */
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
$project = $em->getRepository(Project::class)->find(1);
$fixture = new ActivityFixtures();
$fixture->setAmount(9); // to trigger a second page (every third activity is hidden)
$fixture->setProjects([$project]);
$this->importFixture($client, $fixture);
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->assertAccessIsGranted($client, '/admin/project/1/activities/1');
$node = $client->getCrawler()->filter('div.box#activity_list_box .box-tools ul.pagination li');
@@ -398,7 +368,7 @@ class ProjectControllerTest extends ControllerBaseTest
public function testTeamPermissionAction()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
/** @var Project $project */
$project = $em->getRepository(Project::class)->find(1);
@@ -457,7 +427,7 @@ class ProjectControllerTest extends ControllerBaseTest
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
$fixture = new TimesheetFixtures();
$fixture->setUser($this->getUserByRole($em, User::ROLE_USER));
$fixture->setAmount(10);
@@ -496,7 +466,7 @@ class ProjectControllerTest extends ControllerBaseTest
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$em = $this->getEntityManager();
$fixture = new TimesheetFixtures();
$fixture->setUser($this->getUserByRole($em, User::ROLE_USER));
$fixture->setAmount(10);

View File

@@ -20,6 +20,10 @@ class SystemConfigurationControllerTest extends ControllerBaseTest
public function testIsSecure()
{
$this->assertUrlIsSecured('/admin/system-config/');
}
public function testIsSecureForRole()
{
$this->assertUrlIsSecuredForRole(User::ROLE_ADMIN, '/admin/system-config/');
}

Some files were not shown because too many files have changed in this diff Show More