added user-specific rates (#1455)
This commit is contained in:
4
.github/workflows/coverage.yaml
vendored
4
.github/workflows/coverage.yaml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Code coverage
|
||||
name: CI
|
||||
on:
|
||||
pull_request: null
|
||||
push:
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
matrix:
|
||||
php: ['7.4']
|
||||
|
||||
name: PHP ${{ matrix.php }}
|
||||
name: Coverage - PHP ${{ matrix.php }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: shivammathur/setup-php@v1
|
||||
|
||||
4
.github/workflows/linting.yaml
vendored
4
.github/workflows/linting.yaml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Code Styles
|
||||
name: CI
|
||||
on:
|
||||
pull_request: null
|
||||
push:
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
matrix:
|
||||
php: ['7.4']
|
||||
|
||||
name: PHP ${{ matrix.php }}
|
||||
name: Linting - PHP ${{ matrix.php }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: shivammathur/setup-php@v1
|
||||
|
||||
4
.github/workflows/testing.yaml
vendored
4
.github/workflows/testing.yaml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Tests & Migrations
|
||||
name: CI
|
||||
on:
|
||||
pull_request: null
|
||||
push:
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
matrix:
|
||||
php: ['7.2', '7.3', '7.4']
|
||||
|
||||
name: PHP ${{ matrix.php }}
|
||||
name: Tests - PHP ${{ matrix.php }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: shivammathur/setup-php@v1
|
||||
|
||||
10
README.md
10
README.md
@@ -1,9 +1,9 @@
|
||||
# Kimai 2 - online time-tracker
|
||||
|
||||
[](https://github.com/kevinpapst/kimai2/actions)
|
||||
[](https://codecov.io/gh/kevinpapst/kimai2)
|
||||
[](https://packagist.org/packages/kevinpapst/kimai2)
|
||||
[](https://packagist.org/packages/kevinpapst/kimai2)
|
||||
[](https://travis-ci.org/kevinpapst/kimai2)
|
||||
[](https://codecov.io/gh/kevinpapst/kimai2)
|
||||
[](https://gitter.im/kimai2/support)
|
||||
[](https://www.bountysource.com/teams/kimai2)
|
||||
|
||||
@@ -20,8 +20,8 @@ It is built with modern technologies such as Symfony, Bootstrap, RESTful API, Do
|
||||
|
||||
### Requirements
|
||||
|
||||
- PHP 7.2 or higher
|
||||
- Database (MySQL/MariaDB with timezone data, SQLite for development)
|
||||
- PHP 7.2.9 or higher
|
||||
- Database (MySQL/MariaDB, SQLite for development)
|
||||
- Webserver (nginx, Apache)
|
||||
- A modern browser
|
||||
- [Other libraries](https://www.kimai.org/download/)
|
||||
@@ -32,7 +32,7 @@ This is the new version of the open source timetracker Kimai. It is stable and p
|
||||
with most advanced features from Kimai 1 and many new ones, including but not limited to:
|
||||
|
||||
JSON API, invoicing, data exports, multi-timer and punch-in punch-out mode, tagging, multi-user and multi-timezones,
|
||||
LDAP and built-in authentication, customizable role permissions, responsive and ready for your mobile device,
|
||||
authentication via SAML/LDAP/Database, customizable role permissions, responsive and ready for your mobile device,
|
||||
hourly and fixed rates, advanced filtering, money and time budgets with report, support for plugins and many more.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -59,6 +59,7 @@ export default class KimaiReloadPageWidget {
|
||||
jQuery('section.content').replaceWith(
|
||||
jQuery(response).find('section.content')
|
||||
);
|
||||
document.dispatchEvent(new Event('kimai.reloadPage'));
|
||||
self._hideOverlay();
|
||||
},
|
||||
dataType: 'html',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
App\Entity\Activity:
|
||||
exclusion_policy: All
|
||||
custom_accessor_order: [id, name, comment, visible, project, fixedRate, hourlyRate, color, budget, timeBudget, metaFields, parentTitle]
|
||||
custom_accessor_order: [id, name, comment, visible, project, color, budget, timeBudget, metaFields, parentTitle]
|
||||
properties:
|
||||
id:
|
||||
include: true
|
||||
@@ -20,12 +20,6 @@ App\Entity\Activity:
|
||||
timeBudget:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
fixedRate:
|
||||
include: true
|
||||
groups: [Activity]
|
||||
hourlyRate:
|
||||
include: true
|
||||
groups: [Activity]
|
||||
project:
|
||||
include: false
|
||||
exclude: true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
App\Entity\Customer:
|
||||
exclusion_policy: All
|
||||
custom_accessor_order: [id, name, number, comment, visible, company, contact, address, country, currency, phone, fax, mobile, email, homepage, timezone, fixedRate, hourlyRate, color, budget, timeBudget, metaFields, teams]
|
||||
custom_accessor_order: [id, name, number, comment, visible, company, contact, address, country, currency, phone, fax, mobile, email, homepage, timezone, color, budget, timeBudget, metaFields, teams]
|
||||
properties:
|
||||
id:
|
||||
include: true
|
||||
@@ -53,12 +53,6 @@ App\Entity\Customer:
|
||||
timezone:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
fixedRate:
|
||||
include: true
|
||||
groups: [Customer]
|
||||
hourlyRate:
|
||||
include: true
|
||||
groups: [Customer]
|
||||
color:
|
||||
include: true
|
||||
metaFields:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
App\Entity\Project:
|
||||
exclusion_policy: All
|
||||
custom_accessor_order: [id, name, comment, visible, orderNumber, orderDate, customer, start, end, fixedRate, hourlyRate, color, budget, timeBudget, metaFields, parentTitle, teams]
|
||||
custom_accessor_order: [id, name, comment, visible, orderNumber, orderDate, customer, start, end, color, budget, timeBudget, metaFields, parentTitle, teams]
|
||||
properties:
|
||||
id:
|
||||
include: true
|
||||
@@ -29,12 +29,6 @@ App\Entity\Project:
|
||||
end:
|
||||
include: true
|
||||
groups: [Project]
|
||||
fixedRate:
|
||||
include: true
|
||||
groups: [Project]
|
||||
hourlyRate:
|
||||
include: true
|
||||
groups: [Project]
|
||||
customer:
|
||||
groups: [Subresource]
|
||||
color:
|
||||
|
||||
@@ -211,3 +211,18 @@ services:
|
||||
App\Repository\InvoiceDocumentRepository:
|
||||
class: App\Repository\InvoiceDocumentRepository
|
||||
arguments: ['%kimai.invoice.documents%']
|
||||
|
||||
App\Repository\CustomerRateRepository:
|
||||
class: Doctrine\ORM\EntityRepository
|
||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||
arguments: ['App\Entity\CustomerRate']
|
||||
|
||||
App\Repository\ActivityRateRepository:
|
||||
class: Doctrine\ORM\EntityRepository
|
||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||
arguments: ['App\Entity\ActivityRate']
|
||||
|
||||
App\Repository\ProjectRateRepository:
|
||||
class: Doctrine\ORM\EntityRepository
|
||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||
arguments: ['App\Entity\ProjectRate']
|
||||
|
||||
File diff suppressed because one or more lines are too long
1
public/build/app.9c136e43.js
Normal file
1
public/build/app.9c136e43.js
Normal file
File diff suppressed because one or more lines are too long
@@ -5,7 +5,7 @@
|
||||
"build/runtime.664a9501.js",
|
||||
"build/0.f7fae2b9.js",
|
||||
"build/1.c24a9c6f.js",
|
||||
"build/app.480acc52.js"
|
||||
"build/app.9c136e43.js"
|
||||
],
|
||||
"css": [
|
||||
"build/app.2bde4971.css"
|
||||
@@ -35,7 +35,7 @@
|
||||
"build/runtime.664a9501.js": "sha384-xNNrNinl64G3nCUrIskgSjU0mUXXCB9lj6XCSInBTwxSKXk8uTMafnLHtdWdIGtd",
|
||||
"build/0.f7fae2b9.js": "sha384-DR5A41RIECdWBd6xODR0b1hvGqcEYUyn9vNPreqK9VOCQWTF6bgxB4RVmqlOKilT",
|
||||
"build/1.c24a9c6f.js": "sha384-JPoKdrVtBemSiVBoAnmSxLML7xXM9zYeuwOPYQv/kLzt/P4cmLY5r9gH8oaGRPFG",
|
||||
"build/app.480acc52.js": "sha384-xYyt4WgTB3KsDmkKZW/GYmkDoh/bFXZM/bVGQ9FddGIqUJ7GRp1hfs9RVTEAUqO5",
|
||||
"build/app.9c136e43.js": "sha384-TDHR6NK2lj2HSkxfJJsyuODZQgu0+fX+3eDlCbyE+KP80nAHNCuTqubCHvSU5Eg5",
|
||||
"build/app.2bde4971.css": "sha384-3bVuRNKO8FkMuSAu5Eefo+HJ9WNpiJsrIefh0qSdQsbk+8+MA1zre/NZ/AGrY1M9",
|
||||
"build/2.a47ad919.js": "sha384-1h2P/tsl+bh8qgJd7S9irQHKuns7UATVxeFGLOCH85GPFXfAdRzgzx3nk5MrxzrV",
|
||||
"build/chart.c9943f57.js": "sha384-I57c9DtU3AOG2kzKqIZkIu0hi1aGYHRZ5QG4LKC9+9slzJnAMttPGXoL2cQG3m6y",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"build/1.c24a9c6f.js": "build/1.c24a9c6f.js",
|
||||
"build/2.a47ad919.js": "build/2.a47ad919.js",
|
||||
"build/app.css": "build/app.2bde4971.css",
|
||||
"build/app.js": "build/app.480acc52.js",
|
||||
"build/app.js": "build/app.9c136e43.js",
|
||||
"build/calendar.css": "build/calendar.ade7bcdf.css",
|
||||
"build/calendar.js": "build/calendar.75c80d8f.js",
|
||||
"build/chart.js": "build/chart.c9943f57.js",
|
||||
|
||||
@@ -11,17 +11,19 @@ namespace App\Controller;
|
||||
|
||||
use App\Configuration\FormConfiguration;
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\ActivityRate;
|
||||
use App\Entity\MetaTableTypeInterface;
|
||||
use App\Entity\Project;
|
||||
use App\Event\ActivityMetaDefinitionEvent;
|
||||
use App\Event\ActivityMetaDisplayEvent;
|
||||
use App\Form\ActivityEditForm;
|
||||
use App\Form\ActivityRateForm;
|
||||
use App\Form\Toolbar\ActivityToolbarForm;
|
||||
use App\Form\Type\ActivityType;
|
||||
use App\Repository\ActivityRateRepository;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\Query\ActivityFormTypeQuery;
|
||||
use App\Repository\Query\ActivityQuery;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
@@ -37,7 +39,7 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
* @Route(path="/admin/activity")
|
||||
* @Security("is_granted('view_activity')")
|
||||
*/
|
||||
class ActivityController extends AbstractController
|
||||
final class ActivityController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @var ActivityRepository
|
||||
@@ -50,7 +52,7 @@ class ActivityController extends AbstractController
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
protected $dispatcher;
|
||||
private $dispatcher;
|
||||
|
||||
public function __construct(ActivityRepository $repository, FormConfiguration $configuration, EventDispatcherInterface $dispatcher)
|
||||
{
|
||||
@@ -59,11 +61,6 @@ class ActivityController extends AbstractController
|
||||
$this->dispatcher = $dispatcher;
|
||||
}
|
||||
|
||||
protected function getRepository(): ActivityRepository
|
||||
{
|
||||
return $this->repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/", defaults={"page": 1}, name="admin_activity", methods={"GET"})
|
||||
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_activity_paginated", methods={"GET"})
|
||||
@@ -88,7 +85,7 @@ class ActivityController extends AbstractController
|
||||
}
|
||||
|
||||
/* @var $entries Pagerfanta */
|
||||
$entries = $this->getRepository()->getPagerfantaForQuery($query);
|
||||
$entries = $this->repository->getPagerfantaForQuery($query);
|
||||
|
||||
return $this->render('activity/index.html.twig', [
|
||||
'entries' => $entries,
|
||||
@@ -110,6 +107,85 @@ class ActivityController extends AbstractController
|
||||
return $event->getFields();
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/details", name="activity_details", methods={"GET", "POST"})
|
||||
* @Security("is_granted('view', activity)")
|
||||
*/
|
||||
public function detailsAction(Activity $activity, ActivityRateRepository $rateRepository)
|
||||
{
|
||||
$event = new ActivityMetaDefinitionEvent($activity);
|
||||
$this->dispatcher->dispatch($event);
|
||||
|
||||
$stats = null;
|
||||
$rates = [];
|
||||
|
||||
if ($this->isGranted('edit', $activity)) {
|
||||
$rates = $rateRepository->getRatesForActivity($activity);
|
||||
}
|
||||
|
||||
if ($this->isGranted('budget', $activity)) {
|
||||
$stats = $this->repository->getActivityStatistics($activity);
|
||||
}
|
||||
|
||||
return $this->render('activity/details.html.twig', [
|
||||
'activity' => $activity,
|
||||
'stats' => $stats,
|
||||
'rates' => $rates
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/rate_delete/{rate}", name="admin_activity_rate_delete", methods={"GET"})
|
||||
* @Security("is_granted('edit', activity)")
|
||||
*/
|
||||
public function deleteRateAction(Activity $activity, ActivityRate $rate, ActivityRateRepository $repository)
|
||||
{
|
||||
if ($rate->getActivity() !== $activity) {
|
||||
$this->flashError('action.delete.error', ['%reason%' => 'Invalid activity']);
|
||||
} else {
|
||||
try {
|
||||
$repository->deleteRate($rate);
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.delete.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('activity_details', ['id' => $activity->getId()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/rate", name="admin_activity_rate_add", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', activity)")
|
||||
*/
|
||||
public function addRateAction(Activity $activity, Request $request, ActivityRateRepository $repository)
|
||||
{
|
||||
$rate = new ActivityRate();
|
||||
$rate->setActivity($activity);
|
||||
|
||||
$form = $this->createForm(ActivityRateForm::class, $rate, [
|
||||
'action' => $this->generateUrl('admin_activity_rate_add', ['id' => $activity->getId()]),
|
||||
'method' => 'POST',
|
||||
]);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
try {
|
||||
$repository->saveRate($rate);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('activity_details', ['id' => $activity->getId()]);
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('activity/rates.html.twig', [
|
||||
'activity' => $activity,
|
||||
'form' => $form->createView()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/create", name="admin_activity_create", methods={"GET", "POST"})
|
||||
* @Route(path="/create/{project}", name="admin_activity_create_with_project", methods={"GET", "POST"})
|
||||
@@ -129,25 +205,6 @@ class ActivityController extends AbstractController
|
||||
return $this->renderActivityForm($activity, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/budget", name="admin_activity_budget", methods={"GET"})
|
||||
* @Security("is_granted('budget', activity)")
|
||||
*
|
||||
* @param Activity $activity
|
||||
* @return Response
|
||||
*/
|
||||
public function budgetAction(Activity $activity)
|
||||
{
|
||||
$stats = $this->getRepository()->getActivityStatistics($activity);
|
||||
|
||||
// TODO sent event with stats
|
||||
|
||||
return $this->render('activity/budget.html.twig', [
|
||||
'activity' => $activity,
|
||||
'stats' => $stats,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/edit", name="admin_activity_edit", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', activity)")
|
||||
@@ -171,7 +228,7 @@ class ActivityController extends AbstractController
|
||||
*/
|
||||
public function deleteAction(Activity $activity, Request $request)
|
||||
{
|
||||
$stats = $this->getRepository()->getActivityStatistics($activity);
|
||||
$stats = $this->repository->getActivityStatistics($activity);
|
||||
|
||||
$deleteForm = $this->createFormBuilder(null, [
|
||||
'attr' => [
|
||||
@@ -199,9 +256,9 @@ class ActivityController extends AbstractController
|
||||
|
||||
if ($deleteForm->isSubmitted() && $deleteForm->isValid()) {
|
||||
try {
|
||||
$this->getRepository()->deleteActivity($activity, $deleteForm->get('activity')->getData());
|
||||
$this->repository->deleteActivity($activity, $deleteForm->get('activity')->getData());
|
||||
$this->flashSuccess('action.delete.success');
|
||||
} catch (ORMException $ex) {
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.delete.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
|
||||
@@ -233,7 +290,7 @@ class ActivityController extends AbstractController
|
||||
|
||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||
try {
|
||||
$this->getRepository()->saveActivity($activity);
|
||||
$this->repository->saveActivity($activity);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
if ($editForm->has('create_more') && $editForm->get('create_more')->getData() === true) {
|
||||
@@ -245,7 +302,7 @@ class ActivityController extends AbstractController
|
||||
} else {
|
||||
return $this->redirectToRoute('admin_activity');
|
||||
}
|
||||
} catch (ORMException $ex) {
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,22 +12,25 @@ namespace App\Controller;
|
||||
use App\Configuration\FormConfiguration;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\CustomerComment;
|
||||
use App\Entity\CustomerRate;
|
||||
use App\Entity\MetaTableTypeInterface;
|
||||
use App\Entity\Rate;
|
||||
use App\Entity\Team;
|
||||
use App\Event\CustomerMetaDefinitionEvent;
|
||||
use App\Event\CustomerMetaDisplayEvent;
|
||||
use App\Form\CustomerCommentForm;
|
||||
use App\Form\CustomerEditForm;
|
||||
use App\Form\CustomerRateForm;
|
||||
use App\Form\CustomerTeamPermissionForm;
|
||||
use App\Form\Toolbar\CustomerToolbarForm;
|
||||
use App\Form\Type\CustomerType;
|
||||
use App\Repository\CustomerRateRepository;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\CustomerFormTypeQuery;
|
||||
use App\Repository\Query\CustomerQuery;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
use App\Repository\TeamRepository;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
@@ -257,7 +260,7 @@ final class CustomerController extends AbstractController
|
||||
* @Route(path="/{id}/details", name="customer_details", methods={"GET", "POST"})
|
||||
* @Security("is_granted('view', customer)")
|
||||
*/
|
||||
public function detailsAction(Customer $customer, TeamRepository $teamRepository)
|
||||
public function detailsAction(Customer $customer, TeamRepository $teamRepository, CustomerRateRepository $rateRepository)
|
||||
{
|
||||
$event = new CustomerMetaDefinitionEvent($customer);
|
||||
$this->dispatcher->dispatch($event);
|
||||
@@ -270,10 +273,14 @@ final class CustomerController extends AbstractController
|
||||
$comments = null;
|
||||
$teams = null;
|
||||
$projects = null;
|
||||
$rates = [];
|
||||
|
||||
if ($this->isGranted('edit', $customer) && $this->isGranted('create_team')) {
|
||||
if ($this->isGranted('edit', $customer)) {
|
||||
if ($this->isGranted('create_team')) {
|
||||
$defaultTeam = $teamRepository->findOneBy(['name' => $customer->getName()]);
|
||||
}
|
||||
$rates = $rateRepository->getRatesForCustomer($customer);
|
||||
}
|
||||
|
||||
if (null !== $customer->getTimezone()) {
|
||||
$timezone = new \DateTimeZone($customer->getTimezone());
|
||||
@@ -304,6 +311,59 @@ final class CustomerController extends AbstractController
|
||||
'team' => $defaultTeam,
|
||||
'teams' => $teams,
|
||||
'now' => new \DateTime('now', $timezone),
|
||||
'rates' => $rates
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/rate_delete/{rate}", name="admin_customer_rate_delete", methods={"GET"})
|
||||
* @Security("is_granted('edit', customer)")
|
||||
*/
|
||||
public function deleteRateAction(Customer $customer, CustomerRate $rate, CustomerRateRepository $repository)
|
||||
{
|
||||
if ($rate->getCustomer() !== $customer) {
|
||||
$this->flashError('action.delete.error', ['%reason%' => 'Invalid customer']);
|
||||
} else {
|
||||
try {
|
||||
$repository->deleteRate($rate);
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.delete.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('customer_details', ['id' => $customer->getId()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/rate", name="admin_customer_rate_add", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', customer)")
|
||||
*/
|
||||
public function addRateAction(Customer $customer, Request $request, CustomerRateRepository $repository)
|
||||
{
|
||||
$rate = new CustomerRate();
|
||||
$rate->setCustomer($customer);
|
||||
|
||||
$form = $this->createForm(CustomerRateForm::class, $rate, [
|
||||
'action' => $this->generateUrl('admin_customer_rate_add', ['id' => $customer->getId()]),
|
||||
'method' => 'POST',
|
||||
]);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
try {
|
||||
$repository->saveRate($rate);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('customer_details', ['id' => $customer->getId()]);
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('customer/rates.html.twig', [
|
||||
'customer' => $customer,
|
||||
'form' => $form->createView()
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -352,7 +412,7 @@ final class CustomerController extends AbstractController
|
||||
try {
|
||||
$this->repository->deleteCustomer($customer, $deleteForm->get('customer')->getData());
|
||||
$this->flashSuccess('action.delete.success');
|
||||
} catch (ORMException $ex) {
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.delete.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
|
||||
@@ -383,7 +443,7 @@ final class CustomerController extends AbstractController
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('customer_details', ['id' => $customer->getId()]);
|
||||
} catch (ORMException $ex) {
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,15 +14,19 @@ use App\Entity\Customer;
|
||||
use App\Entity\MetaTableTypeInterface;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\ProjectComment;
|
||||
use App\Entity\ProjectRate;
|
||||
use App\Entity\Rate;
|
||||
use App\Entity\Team;
|
||||
use App\Event\ProjectMetaDefinitionEvent;
|
||||
use App\Event\ProjectMetaDisplayEvent;
|
||||
use App\Form\ProjectCommentForm;
|
||||
use App\Form\ProjectEditForm;
|
||||
use App\Form\ProjectRateForm;
|
||||
use App\Form\ProjectTeamPermissionForm;
|
||||
use App\Form\Toolbar\ProjectToolbarForm;
|
||||
use App\Form\Type\ProjectType;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\ProjectRateRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\ActivityQuery;
|
||||
use App\Repository\Query\ProjectFormTypeQuery;
|
||||
@@ -261,7 +265,7 @@ final class ProjectController extends AbstractController
|
||||
* @Route(path="/{id}/details", name="project_details", methods={"GET", "POST"})
|
||||
* @Security("is_granted('view', project)")
|
||||
*/
|
||||
public function detailsAction(Project $project, TeamRepository $teamRepository)
|
||||
public function detailsAction(Project $project, TeamRepository $teamRepository, ProjectRateRepository $rateRepository)
|
||||
{
|
||||
$event = new ProjectMetaDefinitionEvent($project);
|
||||
$this->dispatcher->dispatch($event);
|
||||
@@ -272,10 +276,14 @@ final class ProjectController extends AbstractController
|
||||
$attachments = [];
|
||||
$comments = null;
|
||||
$teams = null;
|
||||
$rates = [];
|
||||
|
||||
if ($this->isGranted('edit', $project) && $this->isGranted('create_team')) {
|
||||
if ($this->isGranted('edit', $project)) {
|
||||
if ($this->isGranted('create_team')) {
|
||||
$defaultTeam = $teamRepository->findOneBy(['name' => $project->getName()]);
|
||||
}
|
||||
$rates = $rateRepository->getRatesForProject($project);
|
||||
}
|
||||
|
||||
if ($this->isGranted('budget', $project)) {
|
||||
$stats = $this->repository->getProjectStatistics($project);
|
||||
@@ -301,6 +309,59 @@ final class ProjectController extends AbstractController
|
||||
'stats' => $stats,
|
||||
'team' => $defaultTeam,
|
||||
'teams' => $teams,
|
||||
'rates' => $rates
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/rate_delete/{rate}", name="admin_project_rate_delete", methods={"GET"})
|
||||
* @Security("is_granted('edit', project)")
|
||||
*/
|
||||
public function deleteRateAction(Project $project, ProjectRate $rate, ProjectRateRepository $repository)
|
||||
{
|
||||
if ($rate->getProject() !== $project) {
|
||||
$this->flashError('action.delete.error', ['%reason%' => 'Invalid project']);
|
||||
} else {
|
||||
try {
|
||||
$repository->deleteRate($rate);
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.delete.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('project_details', ['id' => $project->getId()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/rate", name="admin_project_rate_add", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', project)")
|
||||
*/
|
||||
public function addRateAction(Project $project, Request $request, ProjectRateRepository $repository)
|
||||
{
|
||||
$rate = new ProjectRate();
|
||||
$rate->setProject($project);
|
||||
|
||||
$form = $this->createForm(ProjectRateForm::class, $rate, [
|
||||
'action' => $this->generateUrl('admin_project_rate_add', ['id' => $project->getId()]),
|
||||
'method' => 'POST',
|
||||
]);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
try {
|
||||
$repository->saveRate($rate);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('project_details', ['id' => $project->getId()]);
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('project/rates.html.twig', [
|
||||
'project' => $project,
|
||||
'form' => $form->createView()
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +74,6 @@ class Activity implements EntityWithMetaFields
|
||||
private $visible = true;
|
||||
|
||||
// keep the trait include exactly here, for placing the column at the correct position
|
||||
use RatesTrait;
|
||||
use ColorTrait;
|
||||
use BudgetTrait;
|
||||
|
||||
@@ -138,6 +137,11 @@ class Activity implements EntityWithMetaFields
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isGlobal(): bool
|
||||
{
|
||||
return $this->project === null;
|
||||
}
|
||||
|
||||
public function isVisible(): bool
|
||||
{
|
||||
return $this->visible;
|
||||
|
||||
54
src/Entity/ActivityRate.php
Normal file
54
src/Entity/ActivityRate.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="kimai2_activities_rates",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(columns={"user_id", "activity_id"}),
|
||||
* }
|
||||
* )
|
||||
* @ORM\Entity(repositoryClass="App\Repository\ActivityRateRepository")
|
||||
* @UniqueEntity({"user", "activity"}, ignoreNull=false)
|
||||
*/
|
||||
class ActivityRate implements RateInterface
|
||||
{
|
||||
use Rate;
|
||||
|
||||
/**
|
||||
* @var Activity
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Activity")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $activity;
|
||||
|
||||
public function setActivity(?Activity $activity): ActivityRate
|
||||
{
|
||||
$this->activity = $activity;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getActivity(): ?Activity
|
||||
{
|
||||
return $this->activity;
|
||||
}
|
||||
|
||||
public function getScore(): int
|
||||
{
|
||||
return 5;
|
||||
}
|
||||
}
|
||||
@@ -174,7 +174,6 @@ class Customer implements EntityWithMetaFields
|
||||
private $timezone;
|
||||
|
||||
// keep the trait include exactly here, for placing the column at the correct position
|
||||
use RatesTrait;
|
||||
use ColorTrait;
|
||||
use BudgetTrait;
|
||||
|
||||
|
||||
54
src/Entity/CustomerRate.php
Normal file
54
src/Entity/CustomerRate.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="kimai2_customers_rates",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(columns={"user_id", "customer_id"}),
|
||||
* }
|
||||
* )
|
||||
* @ORM\Entity(repositoryClass="App\Repository\CustomerRateRepository")
|
||||
* @UniqueEntity({"user", "customer"}, ignoreNull=false)
|
||||
*/
|
||||
class CustomerRate implements RateInterface
|
||||
{
|
||||
use Rate;
|
||||
|
||||
/**
|
||||
* @var Customer
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Customer")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $customer;
|
||||
|
||||
public function setCustomer(?Customer $customer): CustomerRate
|
||||
{
|
||||
$this->customer = $customer;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCustomer(): ?Customer
|
||||
{
|
||||
return $this->customer;
|
||||
}
|
||||
|
||||
public function getScore(): int
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -105,7 +105,6 @@ class Project implements EntityWithMetaFields
|
||||
private $visible = true;
|
||||
|
||||
// keep the trait include exactly here, for placing the column at the correct position
|
||||
use RatesTrait;
|
||||
use ColorTrait;
|
||||
use BudgetTrait;
|
||||
|
||||
|
||||
54
src/Entity/ProjectRate.php
Normal file
54
src/Entity/ProjectRate.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="kimai2_projects_rates",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(columns={"user_id", "project_id"}),
|
||||
* }
|
||||
* )
|
||||
* @ORM\Entity(repositoryClass="App\Repository\ProjectRateRepository")
|
||||
* @UniqueEntity({"user", "project"}, ignoreNull=false)
|
||||
*/
|
||||
class ProjectRate implements RateInterface
|
||||
{
|
||||
use Rate;
|
||||
|
||||
/**
|
||||
* @var Project
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Project")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull
|
||||
*/
|
||||
private $project;
|
||||
|
||||
public function setProject(?Project $project): ProjectRate
|
||||
{
|
||||
$this->project = $project;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getProject(): ?Project
|
||||
{
|
||||
return $this->project;
|
||||
}
|
||||
|
||||
public function getScore(): int
|
||||
{
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
92
src/Entity/Rate.php
Normal file
92
src/Entity/Rate.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
trait Rate
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue(strategy="IDENTITY")
|
||||
*/
|
||||
private $id;
|
||||
/**
|
||||
* @var User
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\User")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=true)
|
||||
*/
|
||||
private $user;
|
||||
/**
|
||||
* @var float
|
||||
*
|
||||
* @ORM\Column(name="rate", type="float", nullable=false)
|
||||
* @Assert\GreaterThanOrEqual(0)
|
||||
*/
|
||||
private $rate = 0.00;
|
||||
/**
|
||||
* @var bool
|
||||
*
|
||||
* @ORM\Column(name="fixed", type="boolean", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $isFixed = false;
|
||||
|
||||
/**
|
||||
* Get entry id, returns null for new entities which were not persisted.
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setUser(?User $user): self
|
||||
{
|
||||
$this->user = $user;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function setRate(float $rate): self
|
||||
{
|
||||
$this->rate = $rate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRate(): float
|
||||
{
|
||||
return $this->rate;
|
||||
}
|
||||
|
||||
public function isFixed(): bool
|
||||
{
|
||||
return $this->isFixed;
|
||||
}
|
||||
|
||||
public function setIsFixed(bool $isFixed): self
|
||||
{
|
||||
$this->isFixed = $isFixed;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
21
src/Entity/RateInterface.php
Normal file
21
src/Entity/RateInterface.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
interface RateInterface
|
||||
{
|
||||
public function getUser(): ?User;
|
||||
|
||||
public function getRate(): float;
|
||||
|
||||
public function isFixed(): bool;
|
||||
|
||||
public function getScore(): int;
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
trait RatesTrait
|
||||
{
|
||||
/**
|
||||
* @var float
|
||||
*
|
||||
* @ORM\Column(name="fixed_rate", type="float", nullable=true)
|
||||
* @Assert\GreaterThanOrEqual(0)
|
||||
*/
|
||||
private $fixedRate = null;
|
||||
|
||||
/**
|
||||
* @var float
|
||||
*
|
||||
* @ORM\Column(name="hourly_rate", type="float", nullable=true)
|
||||
* @Assert\GreaterThanOrEqual(0)
|
||||
*/
|
||||
private $hourlyRate = null;
|
||||
|
||||
/**
|
||||
* @return float
|
||||
*/
|
||||
public function getFixedRate(): ?float
|
||||
{
|
||||
return $this->fixedRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param float $fixedRate
|
||||
* @return self
|
||||
*/
|
||||
public function setFixedRate(?float $fixedRate)
|
||||
{
|
||||
$this->fixedRate = $fixedRate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return float
|
||||
*/
|
||||
public function getHourlyRate(): ?float
|
||||
{
|
||||
return $this->hourlyRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param float $hourlyRate
|
||||
* @return self
|
||||
*/
|
||||
public function setHourlyRate(?float $hourlyRate)
|
||||
{
|
||||
$this->hourlyRate = $hourlyRate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -130,8 +130,21 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface
|
||||
*/
|
||||
private $rate = 0.00;
|
||||
|
||||
// keep the trait include exactly here, for placing the column at the correct position
|
||||
use RatesTrait;
|
||||
/**
|
||||
* @var float
|
||||
*
|
||||
* @ORM\Column(name="fixed_rate", type="float", nullable=true)
|
||||
* @Assert\GreaterThanOrEqual(0)
|
||||
*/
|
||||
private $fixedRate = null;
|
||||
|
||||
/**
|
||||
* @var float
|
||||
*
|
||||
* @ORM\Column(name="hourly_rate", type="float", nullable=true)
|
||||
* @Assert\GreaterThanOrEqual(0)
|
||||
*/
|
||||
private $hourlyRate = null;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
@@ -452,6 +465,30 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface
|
||||
return self::CATEGORY_WORK;
|
||||
}
|
||||
|
||||
public function getFixedRate(): ?float
|
||||
{
|
||||
return $this->fixedRate;
|
||||
}
|
||||
|
||||
public function setFixedRate(?float $fixedRate): Timesheet
|
||||
{
|
||||
$this->fixedRate = $fixedRate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getHourlyRate(): ?float
|
||||
{
|
||||
return $this->hourlyRate;
|
||||
}
|
||||
|
||||
public function setHourlyRate(?float $hourlyRate): Timesheet
|
||||
{
|
||||
$this->hourlyRate = $hourlyRate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal only here for symfony forms
|
||||
* @return Collection|MetaTableTypeInterface[]
|
||||
|
||||
@@ -95,19 +95,19 @@ final class MenuSubscriber implements EventSubscriberInterface
|
||||
|
||||
if ($auth->isGranted('view_customer') || $auth->isGranted('view_teamlead_customer') || $auth->isGranted('view_team_customer')) {
|
||||
$customers = new MenuItemModel('customer_admin', 'menu.admin_customer', 'admin_customer', [], $this->getIcon('customer'));
|
||||
$customers->setChildRoutes(['admin_customer_create', 'admin_customer_permissions', 'admin_customer_budget', 'admin_customer_edit', 'admin_customer_delete']);
|
||||
$customers->setChildRoutes(['admin_customer_create', 'admin_customer_permissions', 'customer_details', 'admin_customer_edit', 'admin_customer_delete']);
|
||||
$menu->addChild($customers);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('view_project') || $auth->isGranted('view_teamlead_project') || $auth->isGranted('view_team_project')) {
|
||||
$projects = new MenuItemModel('project_admin', 'menu.admin_project', 'admin_project', [], $this->getIcon('project'));
|
||||
$projects->setChildRoutes(['admin_project_permissions', 'admin_project_create', 'admin_project_budget', 'admin_project_edit', 'admin_project_delete']);
|
||||
$projects->setChildRoutes(['admin_project_permissions', 'admin_project_create', 'project_details', 'admin_project_edit', 'admin_project_delete']);
|
||||
$menu->addChild($projects);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('view_activity')) {
|
||||
$activities = new MenuItemModel('activity_admin', 'menu.admin_activity', 'admin_activity', [], $this->getIcon('activity'));
|
||||
$activities->setChildRoutes(['admin_activity_create', 'admin_activity_budget', 'admin_activity_edit', 'admin_activity_delete']);
|
||||
$activities->setChildRoutes(['admin_activity_create', 'activity_details', 'admin_activity_edit', 'admin_activity_delete']);
|
||||
$menu->addChild($activities);
|
||||
}
|
||||
|
||||
|
||||
72
src/Form/ActivityRateForm.php
Normal file
72
src/Form/ActivityRateForm.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\ActivityRate;
|
||||
use App\Entity\Rate;
|
||||
use App\Form\Type\UserType;
|
||||
use App\Form\Type\YesNoType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class ActivityRateForm extends AbstractType
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$currency = null;
|
||||
|
||||
if ($options['data']) {
|
||||
/** @var ActivityRate $rate */
|
||||
$rate = $options['data'];
|
||||
|
||||
if (null !== $rate->getActivity() && !$rate->getActivity()->isGlobal()) {
|
||||
$currency = $rate->getActivity()->getProject()->getCustomer()->getCurrency();
|
||||
}
|
||||
}
|
||||
|
||||
$builder
|
||||
->add('user', UserType::class, [
|
||||
'required' => false,
|
||||
])
|
||||
->add('rate', MoneyType::class, [
|
||||
'label' => 'label.rate',
|
||||
'attr' => [
|
||||
'autofocus' => 'autofocus'
|
||||
],
|
||||
'currency' => $currency,
|
||||
])
|
||||
->add('isFixed', YesNoType::class, [
|
||||
'label' => 'label.fixedRate'
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => ActivityRate::class,
|
||||
'csrf_protection' => true,
|
||||
'csrf_field_name' => '_token',
|
||||
'expand_users' => true,
|
||||
'csrf_token_id' => 'admin_customer_rate_edit',
|
||||
'attr' => [
|
||||
'data-form-event' => 'kimai.activityUpdate'
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
71
src/Form/CustomerRateForm.php
Normal file
71
src/Form/CustomerRateForm.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\CustomerRate;
|
||||
use App\Entity\Rate;
|
||||
use App\Form\Type\UserType;
|
||||
use App\Form\Type\YesNoType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class CustomerRateForm extends AbstractType
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$currency = null;
|
||||
if ($options['data']) {
|
||||
/** @var CustomerRate $rate */
|
||||
$rate = $options['data'];
|
||||
|
||||
if (null !== $customer = $rate->getCustomer()) {
|
||||
$currency = $customer->getCurrency();
|
||||
}
|
||||
}
|
||||
|
||||
$builder
|
||||
->add('user', UserType::class, [
|
||||
'required' => false,
|
||||
])
|
||||
->add('rate', MoneyType::class, [
|
||||
'label' => 'label.rate',
|
||||
'attr' => [
|
||||
'autofocus' => 'autofocus'
|
||||
],
|
||||
'currency' => $currency,
|
||||
])
|
||||
->add('isFixed', YesNoType::class, [
|
||||
'label' => 'label.fixedRate'
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => CustomerRate::class,
|
||||
'csrf_protection' => true,
|
||||
'csrf_field_name' => '_token',
|
||||
'expand_users' => true,
|
||||
'csrf_token_id' => 'admin_customer_rate_edit',
|
||||
'attr' => [
|
||||
'data-form-event' => 'kimai.customerUpdate'
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,6 @@ namespace App\Form;
|
||||
|
||||
use App\Form\Type\ColorPickerType;
|
||||
use App\Form\Type\DurationType;
|
||||
use App\Form\Type\FixedRateType;
|
||||
use App\Form\Type\HourlyRateType;
|
||||
use App\Form\Type\MetaFieldsCollectionType;
|
||||
use App\Form\Type\YesNoType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
@@ -26,12 +24,6 @@ trait EntityFormTrait
|
||||
$currency = $options['currency'];
|
||||
$builder
|
||||
->add('color', ColorPickerType::class)
|
||||
->add('fixedRate', FixedRateType::class, [
|
||||
'currency' => $currency,
|
||||
])
|
||||
->add('hourlyRate', HourlyRateType::class, [
|
||||
'currency' => $currency,
|
||||
])
|
||||
;
|
||||
|
||||
if ($options['include_budget']) {
|
||||
|
||||
71
src/Form/ProjectRateForm.php
Normal file
71
src/Form/ProjectRateForm.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\ProjectRate;
|
||||
use App\Entity\Rate;
|
||||
use App\Form\Type\UserType;
|
||||
use App\Form\Type\YesNoType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class ProjectRateForm extends AbstractType
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$currency = null;
|
||||
if ($options['data']) {
|
||||
/** @var ProjectRate $rate */
|
||||
$rate = $options['data'];
|
||||
|
||||
if (null !== $customer = $rate->getProject()->getCustomer()) {
|
||||
$currency = $customer->getCurrency();
|
||||
}
|
||||
}
|
||||
|
||||
$builder
|
||||
->add('user', UserType::class, [
|
||||
'required' => false,
|
||||
])
|
||||
->add('rate', MoneyType::class, [
|
||||
'label' => 'label.rate',
|
||||
'attr' => [
|
||||
'autofocus' => 'autofocus'
|
||||
],
|
||||
'currency' => $currency,
|
||||
])
|
||||
->add('isFixed', YesNoType::class, [
|
||||
'label' => 'label.fixedRate'
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => ProjectRate::class,
|
||||
'csrf_protection' => true,
|
||||
'csrf_field_name' => '_token',
|
||||
'expand_users' => true,
|
||||
'csrf_token_id' => 'admin_project_rate_edit',
|
||||
'attr' => [
|
||||
'data-form-event' => 'kimai.projectUpdate'
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -29,12 +29,6 @@ class InvoiceModelActivityHydrator implements InvoiceModelHydrator
|
||||
'activity.id' => $activity->getId(),
|
||||
'activity.name' => $activity->getName(),
|
||||
'activity.comment' => $activity->getComment(),
|
||||
'activity.fixed_rate' => $formatter->getFormattedMoney($activity->getFixedRate(), $currency),
|
||||
'activity.fixed_rate_nc' => $formatter->getFormattedMoney($activity->getFixedRate(), null),
|
||||
'activity.fixed_rate_plain' => $activity->getFixedRate(),
|
||||
'activity.hourly_rate' => $formatter->getFormattedMoney($activity->getHourlyRate(), $currency),
|
||||
'activity.hourly_rate_nc' => $formatter->getFormattedMoney($activity->getHourlyRate(), null),
|
||||
'activity.hourly_rate_plain' => $activity->getHourlyRate(),
|
||||
];
|
||||
|
||||
foreach ($activity->getVisibleMetaFields() as $metaField) {
|
||||
|
||||
@@ -36,12 +36,6 @@ class InvoiceModelCustomerHydrator implements InvoiceModelHydrator
|
||||
'customer.country' => $customer->getCountry(),
|
||||
'customer.homepage' => $customer->getHomepage(),
|
||||
'customer.comment' => $customer->getComment(),
|
||||
'customer.fixed_rate' => $formatter->getFormattedMoney($customer->getFixedRate(), $currency),
|
||||
'customer.fixed_rate_nc' => $formatter->getFormattedMoney($customer->getFixedRate(), null),
|
||||
'customer.fixed_rate_plain' => $customer->getFixedRate(),
|
||||
'customer.hourly_rate' => $formatter->getFormattedMoney($customer->getHourlyRate(), $currency),
|
||||
'customer.hourly_rate_nc' => $formatter->getFormattedMoney($customer->getHourlyRate(), null),
|
||||
'customer.hourly_rate_plain' => $customer->getHourlyRate(),
|
||||
];
|
||||
|
||||
foreach ($customer->getVisibleMetaFields() as $metaField) {
|
||||
|
||||
@@ -33,12 +33,6 @@ class InvoiceModelProjectHydrator implements InvoiceModelHydrator
|
||||
'project.start_date' => null !== $project->getStart() ? $formatter->getFormattedDateTime($project->getStart()) : '',
|
||||
'project.end_date' => null !== $project->getEnd() ? $formatter->getFormattedDateTime($project->getEnd()) : '',
|
||||
'project.order_date' => null !== $project->getOrderDate() ? $formatter->getFormattedDateTime($project->getOrderDate()) : '',
|
||||
'project.fixed_rate' => $formatter->getFormattedMoney($project->getFixedRate(), $currency),
|
||||
'project.fixed_rate_nc' => $formatter->getFormattedMoney($project->getFixedRate(), null),
|
||||
'project.fixed_rate_plain' => $project->getFixedRate(),
|
||||
'project.hourly_rate' => $formatter->getFormattedMoney($project->getHourlyRate(), $currency),
|
||||
'project.hourly_rate_nc' => $formatter->getFormattedMoney($project->getHourlyRate(), null),
|
||||
'project.hourly_rate_plain' => $project->getHourlyRate(),
|
||||
'project.budget_money' => $formatter->getFormattedMoney($project->getBudget(), $currency),
|
||||
'project.budget_money_nc' => $formatter->getFormattedMoney($project->getBudget(), null),
|
||||
'project.budget_money_plain' => $project->getBudget(),
|
||||
|
||||
@@ -14,6 +14,11 @@ namespace DoctrineMigrations;
|
||||
use App\Doctrine\AbstractMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
|
||||
/**
|
||||
* Adds language and decimal_duration column to invoice template table
|
||||
*
|
||||
* @version 1.8
|
||||
*/
|
||||
final class Version20200204124425 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
|
||||
71
src/Migrations/Version20200205115243.php
Normal file
71
src/Migrations/Version20200205115243.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use App\Doctrine\AbstractMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
|
||||
/**
|
||||
* Adds the rate table, which allows to define user specific rate rules
|
||||
*
|
||||
* @version 1.8
|
||||
*/
|
||||
final class Version20200205115243 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Adds the rate table, which allows to define user specific rate rules';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$customerRates = $schema->createTable('kimai2_customers_rates');
|
||||
$customerRates->addColumn('id', 'integer', ['autoincrement' => true, 'notnull' => true]);
|
||||
$customerRates->addColumn('user_id', 'integer', ['length' => 11, 'notnull' => false]);
|
||||
$customerRates->addColumn('customer_id', 'integer', ['length' => 11, 'notnull' => false]);
|
||||
$customerRates->addColumn('rate', 'float', ['notnull' => true]);
|
||||
$customerRates->addColumn('fixed', 'boolean', ['notnull' => true]);
|
||||
$customerRates->addForeignKeyConstraint('kimai2_users', ['user_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_82AB0AECA76ED395');
|
||||
$customerRates->addForeignKeyConstraint('kimai2_customers', ['customer_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_82AB0AEC9395C3F3');
|
||||
$customerRates->addUniqueIndex(['user_id', 'customer_id'], 'UNIQ_82AB0AECA76ED3959395C3F3');
|
||||
$customerRates->setPrimaryKey(['id']);
|
||||
|
||||
$projectRates = $schema->createTable('kimai2_projects_rates');
|
||||
$projectRates->addColumn('id', 'integer', ['autoincrement' => true, 'notnull' => true]);
|
||||
$projectRates->addColumn('user_id', 'integer', ['length' => 11, 'notnull' => false]);
|
||||
$projectRates->addColumn('project_id', 'integer', ['length' => 11, 'notnull' => false]);
|
||||
$projectRates->addColumn('rate', 'float', ['notnull' => true]);
|
||||
$projectRates->addColumn('fixed', 'boolean', ['notnull' => true]);
|
||||
$projectRates->addForeignKeyConstraint('kimai2_users', ['user_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_41535D55A76ED395');
|
||||
$projectRates->addForeignKeyConstraint('kimai2_projects', ['project_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_41535D55166D1F9C');
|
||||
$projectRates->addUniqueIndex(['user_id', 'project_id'], 'UNIQ_41535D55A76ED395166D1F9C');
|
||||
$projectRates->setPrimaryKey(['id']);
|
||||
|
||||
$activityRates = $schema->createTable('kimai2_activities_rates');
|
||||
$activityRates->addColumn('id', 'integer', ['autoincrement' => true, 'notnull' => true]);
|
||||
$activityRates->addColumn('user_id', 'integer', ['length' => 11, 'notnull' => false]);
|
||||
$activityRates->addColumn('activity_id', 'integer', ['length' => 11, 'notnull' => false]);
|
||||
$activityRates->addColumn('rate', 'float', ['notnull' => true]);
|
||||
$activityRates->addColumn('fixed', 'boolean', ['notnull' => true]);
|
||||
$activityRates->addForeignKeyConstraint('kimai2_users', ['user_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_4A7F11BEA76ED395');
|
||||
$activityRates->addForeignKeyConstraint('kimai2_activities', ['activity_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_4A7F11BE81C06096');
|
||||
$activityRates->addUniqueIndex(['user_id', 'activity_id'], 'UNIQ_4A7F11BEA76ED39581C06096');
|
||||
$activityRates->setPrimaryKey(['id']);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$schema->dropTable('kimai2_activities_rates');
|
||||
$schema->dropTable('kimai2_projects_rates');
|
||||
$schema->dropTable('kimai2_customers_rates');
|
||||
}
|
||||
}
|
||||
72
src/Migrations/Version20200205115244.php
Normal file
72
src/Migrations/Version20200205115244.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use App\Doctrine\AbstractMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
|
||||
/**
|
||||
* Migrates the data from entity tables to user specific rate tables
|
||||
*
|
||||
* @version 1.8
|
||||
*/
|
||||
final class Version20200205115244 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Migrates the data from entity tables to user specific rate tables';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$migrates = [
|
||||
['kimai2_activities', 'activity_id', 'kimai2_activities_rates'],
|
||||
['kimai2_projects', 'project_id', 'kimai2_projects_rates'],
|
||||
['kimai2_customers', 'customer_id', 'kimai2_customers_rates'],
|
||||
];
|
||||
|
||||
foreach ($migrates as $migrateOpts) {
|
||||
$tableName = $migrateOpts[0];
|
||||
$fieldName = $migrateOpts[1];
|
||||
$targetTable = $migrateOpts[2];
|
||||
|
||||
$rules = $this->connection->prepare(
|
||||
'SELECT id, fixed_rate, hourly_rate FROM ' . $tableName . ' WHERE fixed_rate IS NOT NULL OR hourly_rate IS NOT NULL'
|
||||
);
|
||||
$rules->execute();
|
||||
|
||||
foreach ($rules->fetchAll() as $rateRule) {
|
||||
$isFixed = $rateRule['fixed_rate'] !== null;
|
||||
$rate = $rateRule['fixed_rate'] ?? $rateRule['hourly_rate'];
|
||||
$params = ['user_id' => null, $fieldName => $rateRule['id'], 'rate' => $rate, 'fixed' => $isFixed];
|
||||
|
||||
$this->connection->insert($targetTable, $params, ['fixed' => \PDO::PARAM_BOOL]);
|
||||
}
|
||||
}
|
||||
|
||||
$schema->getTable('kimai2_customers')->dropColumn('fixed_rate')->dropColumn('hourly_rate');
|
||||
$schema->getTable('kimai2_projects')->dropColumn('fixed_rate')->dropColumn('hourly_rate');
|
||||
$schema->getTable('kimai2_activities')->dropColumn('fixed_rate')->dropColumn('hourly_rate');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE kimai2_customers ADD COLUMN fixed_rate NUMERIC(10, 2) DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE kimai2_customers ADD COLUMN hourly_rate NUMERIC(10, 2) DEFAULT NULL');
|
||||
|
||||
$this->addSql('ALTER TABLE kimai2_projects ADD COLUMN fixed_rate NUMERIC(10, 2) DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE kimai2_projects ADD COLUMN hourly_rate NUMERIC(10, 2) DEFAULT NULL');
|
||||
|
||||
$this->addSql('ALTER TABLE kimai2_activities ADD COLUMN fixed_rate NUMERIC(10, 2) DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE kimai2_activities ADD COLUMN hourly_rate NUMERIC(10, 2) DEFAULT NULL');
|
||||
}
|
||||
}
|
||||
62
src/Repository/ActivityRateRepository.php
Normal file
62
src/Repository/ActivityRateRepository.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\ActivityRate;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\ORMException;
|
||||
|
||||
class ActivityRateRepository extends EntityRepository
|
||||
{
|
||||
public function saveRate(ActivityRate $rate)
|
||||
{
|
||||
$entityManager = $this->getEntityManager();
|
||||
$entityManager->persist($rate);
|
||||
$entityManager->flush();
|
||||
}
|
||||
|
||||
public function deleteRate(ActivityRate $rate)
|
||||
{
|
||||
$em = $this->getEntityManager();
|
||||
$em->beginTransaction();
|
||||
|
||||
try {
|
||||
$em->remove($rate);
|
||||
$em->flush();
|
||||
$em->commit();
|
||||
} catch (ORMException $ex) {
|
||||
$em->rollback();
|
||||
throw $ex;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Activity $activity
|
||||
* @return ActivityRate[]
|
||||
*/
|
||||
public function getRatesForActivity(Activity $activity): array
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
|
||||
$qb->select('r, u, a')
|
||||
->from(ActivityRate::class, 'r')
|
||||
->leftJoin('r.user', 'u')
|
||||
->leftJoin('r.activity', 'a')
|
||||
->andWhere(
|
||||
$qb->expr()->eq('r.activity', ':activity')
|
||||
)
|
||||
->addOrderBy('u.alias')
|
||||
->setParameter('activity', $activity)
|
||||
;
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
}
|
||||
62
src/Repository/CustomerRateRepository.php
Normal file
62
src/Repository/CustomerRateRepository.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\CustomerRate;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\ORMException;
|
||||
|
||||
class CustomerRateRepository extends EntityRepository
|
||||
{
|
||||
public function saveRate(CustomerRate $rate)
|
||||
{
|
||||
$entityManager = $this->getEntityManager();
|
||||
$entityManager->persist($rate);
|
||||
$entityManager->flush();
|
||||
}
|
||||
|
||||
public function deleteRate(CustomerRate $rate)
|
||||
{
|
||||
$em = $this->getEntityManager();
|
||||
$em->beginTransaction();
|
||||
|
||||
try {
|
||||
$em->remove($rate);
|
||||
$em->flush();
|
||||
$em->commit();
|
||||
} catch (ORMException $ex) {
|
||||
$em->rollback();
|
||||
throw $ex;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Customer $customer
|
||||
* @return CustomerRate[]
|
||||
*/
|
||||
public function getRatesForCustomer(Customer $customer): array
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
|
||||
$qb->select('r, u, c')
|
||||
->from(CustomerRate::class, 'r')
|
||||
->leftJoin('r.user', 'u')
|
||||
->leftJoin('r.customer', 'c')
|
||||
->andWhere(
|
||||
$qb->expr()->eq('r.customer', ':customer')
|
||||
)
|
||||
->addOrderBy('u.alias')
|
||||
->setParameter('customer', $customer)
|
||||
;
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
}
|
||||
62
src/Repository/ProjectRateRepository.php
Normal file
62
src/Repository/ProjectRateRepository.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Project;
|
||||
use App\Entity\ProjectRate;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\ORMException;
|
||||
|
||||
class ProjectRateRepository extends EntityRepository
|
||||
{
|
||||
public function saveRate(ProjectRate $rate)
|
||||
{
|
||||
$entityManager = $this->getEntityManager();
|
||||
$entityManager->persist($rate);
|
||||
$entityManager->flush();
|
||||
}
|
||||
|
||||
public function deleteRate(ProjectRate $rate)
|
||||
{
|
||||
$em = $this->getEntityManager();
|
||||
$em->beginTransaction();
|
||||
|
||||
try {
|
||||
$em->remove($rate);
|
||||
$em->flush();
|
||||
$em->commit();
|
||||
} catch (ORMException $ex) {
|
||||
$em->rollback();
|
||||
throw $ex;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Project $project
|
||||
* @return ProjectRate[]
|
||||
*/
|
||||
public function getRatesForProject(Project $project): array
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
|
||||
$qb->select('r, u, p')
|
||||
->from(ProjectRate::class, 'r')
|
||||
->leftJoin('r.user', 'u')
|
||||
->leftJoin('r.project', 'p')
|
||||
->andWhere(
|
||||
$qb->expr()->eq('r.project', ':project')
|
||||
)
|
||||
->addOrderBy('u.alias')
|
||||
->setParameter('project', $project)
|
||||
;
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,10 @@
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\ActivityRate;
|
||||
use App\Entity\CustomerRate;
|
||||
use App\Entity\ProjectRate;
|
||||
use App\Entity\RateInterface;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Model\Statistic\Day;
|
||||
@@ -834,4 +838,73 @@ class TimesheetRepository extends EntityRepository
|
||||
|
||||
return $field;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $timesheet
|
||||
* @return RateInterface[]
|
||||
*/
|
||||
public function findMatchingRates(Timesheet $timesheet): array
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
$qb->select('r, u, a')
|
||||
->from(ActivityRate::class, 'r')
|
||||
->leftJoin('r.user', 'u')
|
||||
->leftJoin('r.activity', 'a')
|
||||
->andWhere(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->eq('r.user', ':user'),
|
||||
$qb->expr()->isNull('r.user')
|
||||
),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->eq('r.activity', ':activity'),
|
||||
$qb->expr()->isNull('r.activity')
|
||||
)
|
||||
)
|
||||
->setParameter('user', $timesheet->getUser())
|
||||
->setParameter('activity', $timesheet->getActivity())
|
||||
;
|
||||
$results = $qb->getQuery()->getResult();
|
||||
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
$qb->select('r, u, p')
|
||||
->from(ProjectRate::class, 'r')
|
||||
->leftJoin('r.user', 'u')
|
||||
->leftJoin('r.project', 'p')
|
||||
->andWhere(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->eq('r.user', ':user'),
|
||||
$qb->expr()->isNull('r.user')
|
||||
),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->eq('r.project', ':project'),
|
||||
$qb->expr()->isNull('r.project')
|
||||
)
|
||||
)
|
||||
->setParameter('user', $timesheet->getUser())
|
||||
->setParameter('project', $timesheet->getProject())
|
||||
;
|
||||
$results = array_merge($results, $qb->getQuery()->getResult());
|
||||
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
$qb->select('r, u, c')
|
||||
->from(CustomerRate::class, 'r')
|
||||
->leftJoin('r.user', 'u')
|
||||
->leftJoin('r.customer', 'c')
|
||||
->andWhere(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->eq('r.user', ':user'),
|
||||
$qb->expr()->isNull('r.user')
|
||||
),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->eq('r.customer', ':customer'),
|
||||
$qb->expr()->isNull('r.customer')
|
||||
)
|
||||
)
|
||||
->setParameter('user', $timesheet->getUser())
|
||||
->setParameter('customer', $timesheet->getProject()->getCustomer())
|
||||
;
|
||||
$results = array_merge($results, $qb->getQuery()->getResult());
|
||||
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,11 @@
|
||||
|
||||
namespace App\Timesheet\Calculator;
|
||||
|
||||
use App\Entity\Rate;
|
||||
use App\Entity\RateInterface;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\UserPreference;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Timesheet\CalculatorInterface;
|
||||
use App\Timesheet\Util;
|
||||
|
||||
@@ -22,15 +25,16 @@ class RateCalculator implements CalculatorInterface
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $rates;
|
||||
|
||||
private $rates;
|
||||
/**
|
||||
* RateCalculator constructor.
|
||||
* @param array $rates
|
||||
* @var TimesheetRepository
|
||||
*/
|
||||
public function __construct(array $rates)
|
||||
private $repository;
|
||||
|
||||
public function __construct(array $rates, TimesheetRepository $repository)
|
||||
{
|
||||
$this->rates = $rates;
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,7 +48,21 @@ class RateCalculator implements CalculatorInterface
|
||||
return;
|
||||
}
|
||||
|
||||
$fixedRate = $this->findFixedRate($record);
|
||||
$fixedRate = $record->getFixedRate();
|
||||
$hourlyRate = $record->getHourlyRate();
|
||||
|
||||
if (null === $fixedRate && null === $hourlyRate) {
|
||||
$rate = $this->getBestFittingRate($record);
|
||||
|
||||
if (null !== $rate) {
|
||||
if ($rate->isFixed()) {
|
||||
$fixedRate = $rate->getRate();
|
||||
} else {
|
||||
$hourlyRate = $rate->getRate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $fixedRate) {
|
||||
$record->setRate($fixedRate);
|
||||
$record->setFixedRate($fixedRate);
|
||||
@@ -52,74 +70,40 @@ class RateCalculator implements CalculatorInterface
|
||||
return;
|
||||
}
|
||||
|
||||
$hourlyRate = $this->findHourlyRate($record);
|
||||
if (null === $hourlyRate) {
|
||||
$hourlyRate = (float) $record->getUser()->getPreferenceValue(UserPreference::HOURLY_RATE, 0.00);
|
||||
}
|
||||
|
||||
$factor = $this->getRateFactor($record);
|
||||
|
||||
$hourlyRate = (float) ($hourlyRate * $factor);
|
||||
$rate = 0;
|
||||
$totalRate = 0;
|
||||
if (null !== $record->getDuration()) {
|
||||
$rate = Util::calculateRate($hourlyRate, $record->getDuration());
|
||||
$totalRate = Util::calculateRate($hourlyRate, $record->getDuration());
|
||||
}
|
||||
|
||||
$record->setHourlyRate($hourlyRate);
|
||||
$record->setRate($rate);
|
||||
$record->setRate($totalRate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $record
|
||||
* @return float
|
||||
*/
|
||||
protected function findHourlyRate(Timesheet $record)
|
||||
private function getBestFittingRate(Timesheet $timesheet): ?RateInterface
|
||||
{
|
||||
if (null !== $record->getHourlyRate()) {
|
||||
return $record->getHourlyRate();
|
||||
$rates = $this->repository->findMatchingRates($timesheet);
|
||||
/** @var RateInterface[] $sorted */
|
||||
$sorted = [];
|
||||
foreach ($rates as $rate) {
|
||||
$score = $rate->getScore();
|
||||
if (null !== $rate->getUser() && $timesheet->getUser() === $rate->getUser()) {
|
||||
++$score;
|
||||
}
|
||||
|
||||
$activity = $record->getActivity();
|
||||
if (null !== $activity->getHourlyRate()) {
|
||||
return $activity->getHourlyRate();
|
||||
$sorted[$score] = $rate;
|
||||
}
|
||||
|
||||
$project = $record->getProject();
|
||||
if (null !== $project) {
|
||||
if (null !== $project->getHourlyRate()) {
|
||||
return $project->getHourlyRate();
|
||||
}
|
||||
if (!empty($sorted)) {
|
||||
ksort($sorted);
|
||||
|
||||
$customer = $project->getCustomer();
|
||||
if (null !== $customer->getHourlyRate()) {
|
||||
return $customer->getHourlyRate();
|
||||
}
|
||||
}
|
||||
|
||||
return (float) $record->getUser()->getPreferenceValue(UserPreference::HOURLY_RATE, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $record
|
||||
* @return float|null
|
||||
*/
|
||||
protected function findFixedRate(Timesheet $record)
|
||||
{
|
||||
if (null !== $record->getFixedRate()) {
|
||||
return $record->getFixedRate();
|
||||
}
|
||||
|
||||
$activity = $record->getActivity();
|
||||
if (null !== $activity->getFixedRate()) {
|
||||
return $activity->getFixedRate();
|
||||
}
|
||||
|
||||
$project = $record->getProject();
|
||||
if (null !== $project) {
|
||||
if (null !== $project->getFixedRate()) {
|
||||
return $project->getFixedRate();
|
||||
}
|
||||
|
||||
$customer = $project->getCustomer();
|
||||
if (null !== $customer->getFixedRate()) {
|
||||
return $customer->getFixedRate();
|
||||
}
|
||||
return end($sorted);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
{% set actions = {} %}
|
||||
|
||||
{% if activity.id is not empty %}
|
||||
{% if view != 'details' and is_granted('view', activity) %}
|
||||
{% set actions = actions|merge({'details': path('activity_details', {'id': activity.id})}) %}
|
||||
{% endif %}
|
||||
{% if is_granted('edit', activity) %}
|
||||
{% set class = '' %}
|
||||
{% if view != 'edit' %}
|
||||
@@ -25,9 +28,6 @@
|
||||
{% endif %}
|
||||
{% set actions = actions|merge({'edit': {'url': path('admin_activity_edit', {'id': activity.id}), 'class': class}}) %}
|
||||
{% endif %}
|
||||
{% if is_granted('budget', activity) %}
|
||||
{% set actions = actions|merge({'report': {'url': path('admin_activity_budget', {'id': activity.id})}}) %}
|
||||
{% endif %}
|
||||
{% if actions|length > 0 %}
|
||||
{% set actions = actions|merge({'divider': null}) %}
|
||||
{% endif %}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
{% import "activity/actions.html.twig" as actions %}
|
||||
|
||||
{% block page_title %}{{ 'admin_activity.title'|trans }}{% endblock %}
|
||||
{% block page_actions %}{{ actions.activity(activity, 'delete') }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
|
||||
{% set params = {
|
||||
'%activity%': '<strong>' ~ activity.name ~ '</strong>',
|
||||
'%project%': '<strong>-</strong>',
|
||||
'%customer%': '<strong>-</strong>',
|
||||
'%records%': '<strong>' ~ stats.recordAmount ~ '</strong>',
|
||||
'%duration%': '<strong>' ~ stats.recordDuration|duration ~ '</strong>'
|
||||
} %}
|
||||
|
||||
{% if activity.project is not null %}
|
||||
{% set params = params|merge({
|
||||
'%project%': '<strong>' ~ activity.project.name ~ '</strong>',
|
||||
'%customer%': '<strong>' ~ activity.project.customer.name ~ '</strong>',
|
||||
}) %}
|
||||
{% endif %}
|
||||
|
||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
|
||||
{% import "macros/progressbar.html.twig" as progress %}
|
||||
{% block box_title %}{{ activity.name }}{% endblock %}
|
||||
{% block box_body %}
|
||||
<p>
|
||||
{{ 'admin_activity.short_stats'|trans(params)|raw }}
|
||||
</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 ) }}
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
|
||||
{% endblock %}
|
||||
149
templates/activity/details.html.twig
Normal file
149
templates/activity/details.html.twig
Normal file
@@ -0,0 +1,149 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% import "activity/actions.html.twig" as actions %}
|
||||
|
||||
{% block page_title %}{{ 'admin_activity.title'|trans }}{% endblock %}
|
||||
{% block page_actions %}{{ actions.activity(activity, 'details') }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% set can_edit = is_granted('edit', activity) %}
|
||||
{% set currency = null %}
|
||||
{% if activity.project is not null %}
|
||||
{% set currency = activity.project.customer.currency %}
|
||||
{% endif %}
|
||||
|
||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% import "customer/actions.html.twig" as customerActions %}
|
||||
{% import "project/actions.html.twig" as projectActions %}
|
||||
{% block box_attributes %}id="activity_details_box"{% endblock %}
|
||||
{% block box_title %}
|
||||
{{ widgets.label_activity(activity) }}
|
||||
{% endblock %}
|
||||
{% block box_tools %}
|
||||
{% if can_edit %}
|
||||
<a class="modal-ajax-form open-edit btn btn-box-tool" data-href="{{ path('admin_activity_edit', {'id': activity.id}) }}" data-toggle="tooltip" data-placement="top" title="{{ 'action.edit'|trans }}"><i class="{{ 'edit'|icon }}"></i></a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block box_body %}
|
||||
{% if activity.comment is not empty %}
|
||||
{{ activity.comment|comment2html(true) }}
|
||||
{% endif %}
|
||||
<table class="table table-hover">
|
||||
{% if not activity.visible %}
|
||||
<tr>
|
||||
<th>{{ 'label.visible'|trans }}</th>
|
||||
<td>
|
||||
{{ widgets.label_boolean(activity.visible) }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if not activity.global %}
|
||||
<tr>
|
||||
<th>{{ 'label.customer'|trans }}</th>
|
||||
<td>
|
||||
{{ widgets.label_customer(activity.project.customer) }}
|
||||
{% if activity.project.customer.teams|length == 0 %}
|
||||
{{ widgets.icon('unlocked') }}
|
||||
{% endif %}
|
||||
|
||||
{{ customerActions.customer(activity.project.customer, 'custom') }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ 'label.project'|trans }}</th>
|
||||
<td>
|
||||
{{ widgets.label_project(activity.project) }}
|
||||
{% if activity.project.teams|length == 0 %}
|
||||
{{ widgets.icon('unlocked') }}
|
||||
{% endif %}
|
||||
|
||||
{{ projectActions.project(activity.project, 'custom') }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% for metaField in activity.visibleMetaFields %}
|
||||
<tr>
|
||||
<th>{{ metaField.label }}</th>
|
||||
<td>{{ widgets.form_type_value(metaField.type, metaField.value, activity) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
|
||||
{% if stats is not null %}
|
||||
{{ include('activity/embed_budget.html.twig', {'activity': activity, 'stats': stats}) }}
|
||||
{% 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 %}
|
||||
–
|
||||
{% 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 %}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block javascripts %}
|
||||
{{ parent() }}
|
||||
<script type="text/javascript">
|
||||
document.addEventListener('kimai.initialized', function() {
|
||||
KimaiReloadPageWidget.create('kimai.activityUpdate kimai.teamUpdate kimai.projectTeamUpdate kimai.projectUpdate');
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -25,16 +25,6 @@
|
||||
{{ form_row(form.comment) }}
|
||||
{{ form_row(form.customer) }}
|
||||
{{ form_row(form.project) }}
|
||||
<fieldset>
|
||||
{#<legend>Financial data</legend>#}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
{{ form_row(form.fixedRate) }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{{ form_row(form.hourlyRate) }}
|
||||
</div>
|
||||
</div>
|
||||
{% if form.budget is defined %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
@@ -45,13 +35,9 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
{{ form_row(form.visible) }}
|
||||
{% if form.metaFields is defined and form.metaFields is not empty %}
|
||||
<fieldset>
|
||||
{#<legend>Custom fields</legend>#}
|
||||
{{ form_row(form.metaFields) }}
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
{{ form_widget(form) }}
|
||||
{% endblock %}
|
||||
|
||||
33
templates/activity/embed_budget.html.twig
Normal file
33
templates/activity/embed_budget.html.twig
Normal file
@@ -0,0 +1,33 @@
|
||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
|
||||
{% import "macros/progressbar.html.twig" as progress %}
|
||||
{% block box_title %}{{ 'label.budget'|trans }}{% endblock %}
|
||||
{% block box_attributes %}id="budget_box"{% endblock %}
|
||||
{% block box_body %}
|
||||
{% set params = {
|
||||
'%activity%': '<strong>' ~ activity.name ~ '</strong>',
|
||||
'%project%': '<strong>-</strong>',
|
||||
'%customer%': '<strong>-</strong>',
|
||||
'%records%': '<strong>' ~ stats.recordAmount ~ '</strong>',
|
||||
'%duration%': '<strong>' ~ stats.recordDuration|duration ~ '</strong>'
|
||||
} %}
|
||||
|
||||
{% if activity.project is not null %}
|
||||
{% set params = params|merge({
|
||||
'%project%': '<strong>' ~ activity.project.name ~ '</strong>',
|
||||
'%customer%': '<strong>' ~ activity.project.customer.name ~ '</strong>',
|
||||
}) %}
|
||||
{% endif %}
|
||||
|
||||
<p>
|
||||
{{ 'admin_activity.short_stats'|trans(params)|raw }}
|
||||
</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 ) }}
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
@@ -42,7 +42,7 @@
|
||||
{{ tables.datatable_header(tableName, columns, query, {'reload': 'kimai.activityUpdate'}) }}
|
||||
|
||||
{% for entry in entries %}
|
||||
<tr{% if is_granted('edit', entry) %} class="modal-ajax-form open-edit" data-href="{{ path('admin_activity_edit', {'id': entry.id}) }}"{% endif %}>
|
||||
<tr{% if is_granted('view', entry) %} class="alternative-link open-edit" data-href="{{ path('activity_details', {'id': entry.id}) }}"{% endif %}>
|
||||
<td>{{ widgets.label_color_dot('activity', true, entry.name, null, entry.color) }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'project') }}">
|
||||
{# only none-global activities have a project and customer assigned #}
|
||||
|
||||
13
templates/activity/rates.html.twig
Normal file
13
templates/activity/rates.html.twig
Normal file
@@ -0,0 +1,13 @@
|
||||
{% extends app.request.xmlHttpRequest ? 'form.html.twig' : 'base.html.twig' %}
|
||||
{% import "activity/actions.html.twig" as actions %}
|
||||
|
||||
{% block page_title %}{{ 'admin_activity.title'|trans }}{% endblock %}
|
||||
{% block page_actions %}{{ actions.activity(activity, 'rates') }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{{ include(app.request.xmlHttpRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig', {
|
||||
'title': activity.name,
|
||||
'form': form,
|
||||
'back': path('admin_activity')
|
||||
}) }}
|
||||
{% endblock %}
|
||||
@@ -108,28 +108,6 @@
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if can_edit %}
|
||||
<tr>
|
||||
<th>{{ 'label.fixedRate'|trans }}</th>
|
||||
<td>
|
||||
{% if customer.fixedRate is not empty %}
|
||||
{{ customer.fixedRate|money(customer.currency) }}
|
||||
{% else %}
|
||||
–
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ 'label.hourlyRate'|trans }}</th>
|
||||
<td>
|
||||
{% if customer.hourlyRate is not empty %}
|
||||
{{ customer.hourlyRate|money(customer.currency) }}
|
||||
{% else %}
|
||||
–
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% for metaField in customer.visibleMetaFields %}
|
||||
<tr>
|
||||
<th>{{ metaField.label }}</th>
|
||||
@@ -146,6 +124,67 @@
|
||||
{{ include('customer/embed_budget.html.twig', {'customer': customer, 'stats': stats}) }}
|
||||
{% 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 %}
|
||||
–
|
||||
{% 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 %}
|
||||
{% endif %}
|
||||
|
||||
{% if teams is not null %}
|
||||
{% set options = {'teams': teams, 'team': team} %}
|
||||
{% if is_granted('permissions', customer) %}
|
||||
|
||||
@@ -68,14 +68,6 @@
|
||||
{{ form_row(form.fax) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
{{ form_row(form.fixedRate) }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{{ form_row(form.hourlyRate) }}
|
||||
</div>
|
||||
</div>
|
||||
{% if form.budget is defined %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
|
||||
@@ -33,6 +33,9 @@
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
<script type="text/javascript">
|
||||
{% set eventName = app.request.xmlHttpRequest ? 'kimai.reloadPage' : 'kimai.initialized' %}
|
||||
document.addEventListener('{{ eventName }}', function() {
|
||||
KimaiPaginatedBoxWidget.create('#project_list_box');
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
@@ -1,5 +1,4 @@
|
||||
{% extends app.request.xmlHttpRequest ? 'form.html.twig' : 'base.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% import "customer/actions.html.twig" as actions %}
|
||||
|
||||
{% block page_title %}{{ 'admin_customer.title'|trans }}{% endblock %}
|
||||
|
||||
13
templates/customer/rates.html.twig
Normal file
13
templates/customer/rates.html.twig
Normal file
@@ -0,0 +1,13 @@
|
||||
{% extends app.request.xmlHttpRequest ? 'form.html.twig' : 'base.html.twig' %}
|
||||
{% import "customer/actions.html.twig" as actions %}
|
||||
|
||||
{% block page_title %}{{ 'admin_customer.title'|trans }}{% endblock %}
|
||||
{% block page_actions %}{{ actions.customer(customer, 'rates') }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{{ include(app.request.xmlHttpRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig', {
|
||||
'title': customer.name,
|
||||
'form': form,
|
||||
'back': path('admin_customer')
|
||||
}) }}
|
||||
{% endblock %}
|
||||
@@ -1,23 +1,23 @@
|
||||
{% if form_theme is defined %}
|
||||
{% form_theme form form_theme %}
|
||||
{% endif %}
|
||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' with {'boxtype': 'primary'} %}
|
||||
{% block box_before %}
|
||||
<div class="box box-primary">
|
||||
{% block form_before %}{% endblock %}
|
||||
{{ form_start(form) }}
|
||||
{% endblock %}
|
||||
{% block box_title %}
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">
|
||||
{{ title }}
|
||||
{% if form.vars.docu_chapter is defined and form.vars.docu_chapter is not empty %}
|
||||
<a href="{{ form.vars.docu_chapter|docu_link }}" target="_blank"><i class="{{ 'help'|icon }}"></i></a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block box_body %}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
{% block form_body %}
|
||||
{{ form_widget(form) }}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
{% block box_footer %}
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
<input type="submit" value="{{ 'action.save'|trans }}" class="btn btn-primary" />
|
||||
{% if back|default(false) %}
|
||||
<a href="{{ back }}" class="btn btn-link">{{ 'action.back'|trans }}</a>
|
||||
@@ -25,9 +25,7 @@
|
||||
{% if reset|default(true) %}
|
||||
<input type="reset" value="{{ 'action.reset'|trans }}" class="btn btn-link pull-right" />
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block box_after %}
|
||||
</div>
|
||||
{{ form_end(form) }}
|
||||
{% block form_after %}{% endblock %}
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
</div>
|
||||
@@ -26,7 +26,7 @@
|
||||
<p>{{ 'team.visibility_restricted'|trans({}, 'teams') }}</p>
|
||||
{{ widgets.team_list(teams) }}
|
||||
{% else %}
|
||||
<p>{{ empty_message|default('team.visibility_global')|trans({}, 'teams') }}</p>
|
||||
{{ empty_message|default('team.visibility_global')|trans({}, 'teams') }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
|
||||
@@ -61,11 +61,12 @@
|
||||
{% if model.calculator is empty or model.calculator.entries is empty %}
|
||||
{{ widgets.callout('warning', 'error.no_entries_found') }}
|
||||
{% else %}
|
||||
{% set isDecimal = model.template.decimalDuration|default(false) %}
|
||||
{% set entries = model.calculator.entries %}
|
||||
{{ tables.datatable_header(tableName, columns, query, {}) }}
|
||||
{% for entry in entries %}
|
||||
{% set amount = entry.amount %}
|
||||
{% set duration = entry.duration|duration %}
|
||||
{% set duration = entry.duration|duration(isDecimal) %}
|
||||
{% set rate = 0 %}
|
||||
{% if entry.fixedRate is not null %}
|
||||
{% set rate = entry.fixedRate %}
|
||||
|
||||
@@ -82,28 +82,6 @@
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if can_edit %}
|
||||
<tr>
|
||||
<th>{{ 'label.fixedRate'|trans }}</th>
|
||||
<td>
|
||||
{% if project.fixedRate is not empty %}
|
||||
{{ project.fixedRate|money(project.customer.currency) }}
|
||||
{% else %}
|
||||
–
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ 'label.hourlyRate'|trans }}</th>
|
||||
<td>
|
||||
{% if project.hourlyRate is not empty %}
|
||||
{{ project.hourlyRate|money(project.customer.currency) }}
|
||||
{% else %}
|
||||
–
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% for metaField in project.visibleMetaFields %}
|
||||
<tr>
|
||||
<th>{{ metaField.label }}</th>
|
||||
@@ -120,6 +98,67 @@
|
||||
{{ include('project/embed_budget.html.twig', {'project': project, 'stats': stats}) }}
|
||||
{% 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 %}
|
||||
–
|
||||
{% 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 %}
|
||||
{% endif %}
|
||||
|
||||
{% if teams is not null%}
|
||||
{% set options = {'teams': teams, 'team': team} %}
|
||||
{% if is_granted('permissions', project) %}
|
||||
|
||||
@@ -40,16 +40,6 @@
|
||||
{{ form_row(form.end) }}
|
||||
</div>
|
||||
</div>
|
||||
<fieldset>
|
||||
{#<legend>Financial data</legend>#}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
{{ form_row(form.fixedRate) }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{{ form_row(form.hourlyRate) }}
|
||||
</div>
|
||||
</div>
|
||||
{% if form.budget is defined %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
@@ -60,13 +50,9 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
{{ form_row(form.visible) }}
|
||||
{% if form.metaFields is defined and form.metaFields is not empty %}
|
||||
<fieldset>
|
||||
{#<legend>Custom fields</legend>#}
|
||||
{{ form_row(form.metaFields) }}
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
{{ form_widget(form) }}
|
||||
{% endblock %}
|
||||
|
||||
@@ -24,6 +24,9 @@
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
<script type="text/javascript">
|
||||
{% set eventName = app.request.xmlHttpRequest ? 'kimai.reloadPage' : 'kimai.initialized' %}
|
||||
document.addEventListener('{{ eventName }}', function() {
|
||||
KimaiPaginatedBoxWidget.create('#activity_list_box');
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
@@ -1,5 +1,4 @@
|
||||
{% extends app.request.xmlHttpRequest ? 'form.html.twig' : 'base.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% import "project/actions.html.twig" as actions %}
|
||||
|
||||
{% block page_title %}{{ 'admin_project.title'|trans }}{% endblock %}
|
||||
|
||||
13
templates/project/rates.html.twig
Normal file
13
templates/project/rates.html.twig
Normal file
@@ -0,0 +1,13 @@
|
||||
{% extends app.request.xmlHttpRequest ? 'form.html.twig' : 'base.html.twig' %}
|
||||
{% import "project/actions.html.twig" as actions %}
|
||||
|
||||
{% block page_title %}{{ 'admin_project.title'|trans }}{% endblock %}
|
||||
{% block page_actions %}{{ actions.project(project, 'rates') }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{{ include(app.request.xmlHttpRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig', {
|
||||
'title': project.name,
|
||||
'form': form,
|
||||
'back': path('admin_project')
|
||||
}) }}
|
||||
{% endblock %}
|
||||
@@ -285,7 +285,7 @@ class ActivityControllerTest extends APIControllerBaseTest
|
||||
protected function assertStructure(array $result, $full = true)
|
||||
{
|
||||
$expectedKeys = [
|
||||
'id', 'name', 'visible', 'project', 'hourlyRate', 'fixedRate', 'color', 'metaFields', 'parentTitle'
|
||||
'id', 'name', 'visible', 'project', 'color', 'metaFields', 'parentTitle'
|
||||
];
|
||||
|
||||
if ($full) {
|
||||
|
||||
@@ -230,7 +230,7 @@ class CustomerControllerTest extends APIControllerBaseTest
|
||||
protected function assertStructure(array $result, $full = true)
|
||||
{
|
||||
$expectedKeys = [
|
||||
'id', 'name', 'visible', 'hourlyRate', 'fixedRate', 'color', 'metaFields', 'teams'
|
||||
'id', 'name', 'visible', 'color', 'metaFields', 'teams'
|
||||
];
|
||||
|
||||
if ($full) {
|
||||
|
||||
@@ -152,7 +152,7 @@ class ProjectControllerTest extends APIControllerBaseTest
|
||||
$data = [
|
||||
'name' => 'foo',
|
||||
'customer' => 1,
|
||||
'visible' => true
|
||||
'visible' => true,
|
||||
];
|
||||
$this->request($client, '/api/projects', 'POST', [], json_encode($data));
|
||||
$response = $client->getResponse();
|
||||
@@ -283,7 +283,7 @@ class ProjectControllerTest extends APIControllerBaseTest
|
||||
protected function assertStructure(array $result, $full = true)
|
||||
{
|
||||
$expectedKeys = [
|
||||
'id', 'name', 'visible', 'customer', 'hourlyRate', 'fixedRate', 'color', 'metaFields', 'parentTitle', 'start', 'end', 'teams'
|
||||
'id', 'name', 'visible', 'customer', 'color', 'metaFields', 'parentTitle', 'start', 'end', 'teams'
|
||||
];
|
||||
|
||||
if ($full) {
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace App\Tests\Controller;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\ActivityMeta;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Tests\DataFixtures\ActivityFixtures;
|
||||
@@ -67,7 +68,7 @@ class ActivityControllerTest extends ControllerBaseTest
|
||||
$this->assertDataTableRowCount($client, 'datatable_activity_admin', 5);
|
||||
}
|
||||
|
||||
public function testBudgetAction()
|
||||
public function testDetailsAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
/** @var EntityManager $em */
|
||||
@@ -79,9 +80,70 @@ class ActivityControllerTest extends ControllerBaseTest
|
||||
$fixture->setUser($this->getUserByRole($em, User::ROLE_ADMIN));
|
||||
$this->importFixture($client, $fixture);
|
||||
|
||||
$project = $em->getRepository(Project::class)->find(1);
|
||||
$fixture = new ActivityFixtures();
|
||||
$fixture->setAmount(6); // to trigger a second page
|
||||
$fixture->setProjects([$project]);
|
||||
$this->importFixture($client, $fixture);
|
||||
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$this->assertAccessIsGranted($client, '/admin/activity/1/budget');
|
||||
$this->assertAccessIsGranted($client, '/admin/activity/1/details');
|
||||
self::assertHasProgressbar($client);
|
||||
|
||||
$node = $client->getCrawler()->filter('div.box#activity_details_box');
|
||||
self::assertEquals(1, $node->count());
|
||||
$node = $client->getCrawler()->filter('div.box#budget_box');
|
||||
self::assertEquals(1, $node->count());
|
||||
$node = $client->getCrawler()->filter('div.box#activity_rates_box');
|
||||
self::assertEquals(1, $node->count());
|
||||
}
|
||||
|
||||
public function testAddRateAction()
|
||||
{
|
||||
$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));
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
@@ -87,6 +87,56 @@ class CustomerControllerTest extends ControllerBaseTest
|
||||
self::assertEquals(1, $node->count());
|
||||
$node = $client->getCrawler()->filter('div.box#team_listing_box a.btn-box-tool');
|
||||
self::assertEquals(2, $node->count());
|
||||
$node = $client->getCrawler()->filter('div.box#customer_rates_box');
|
||||
self::assertEquals(1, $node->count());
|
||||
}
|
||||
|
||||
public function testAddRateAction()
|
||||
{
|
||||
$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));
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
@@ -106,6 +106,56 @@ class ProjectControllerTest extends ControllerBaseTest
|
||||
self::assertEquals(1, $node->count());
|
||||
$node = $client->getCrawler()->filter('div.box#team_listing_box a.btn-box-tool');
|
||||
self::assertEquals(2, $node->count());
|
||||
$node = $client->getCrawler()->filter('div.box#project_rates_box');
|
||||
self::assertEquals(1, $node->count());
|
||||
}
|
||||
|
||||
public function testAddRateAction()
|
||||
{
|
||||
$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));
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
@@ -33,7 +33,7 @@ class SystemConfigurationControllerTest extends ControllerBaseTest
|
||||
$result = $client->getCrawler()->filter('section.content div.box.box-primary');
|
||||
$this->assertEquals(count($expectedForms), count($result));
|
||||
|
||||
$result = $client->getCrawler()->filter('section.content form div.box.box-primary');
|
||||
$result = $client->getCrawler()->filter('section.content div.box.box-primary form');
|
||||
$this->assertEquals(count($expectedForms), count($result));
|
||||
|
||||
foreach ($expectedForms as $formConfig) {
|
||||
|
||||
55
tests/Entity/ActivityRateTest.php
Normal file
55
tests/Entity/ActivityRateTest.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?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\Entity;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\ActivityRate;
|
||||
use App\Entity\User;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Entity\ActivityRate
|
||||
* @covers \App\Entity\Rate
|
||||
*/
|
||||
class ActivityRateTest extends TestCase
|
||||
{
|
||||
public function testDefaultValues()
|
||||
{
|
||||
$sut = new ActivityRate();
|
||||
self::assertNull($sut->getId());
|
||||
self::assertEquals(0.00, $sut->getRate());
|
||||
self::assertNull($sut->getActivity());
|
||||
self::assertNull($sut->getUser());
|
||||
self::assertEquals(5, $sut->getScore());
|
||||
self::assertFalse($sut->isFixed());
|
||||
}
|
||||
|
||||
public function testSetterAndGetter()
|
||||
{
|
||||
$sut = new ActivityRate();
|
||||
|
||||
self::assertInstanceOf(ActivityRate::class, $sut->setIsFixed(true));
|
||||
self::assertTrue($sut->isFixed());
|
||||
|
||||
self::assertInstanceOf(ActivityRate::class, $sut->setRate(12.34));
|
||||
self::assertEquals(12.34, $sut->getRate());
|
||||
|
||||
$user = new User();
|
||||
$user->setAlias('foo');
|
||||
$user->setUsername('bar');
|
||||
self::assertInstanceOf(ActivityRate::class, $sut->setUser($user));
|
||||
self::assertSame($user, $sut->getUser());
|
||||
|
||||
$entity = new Activity();
|
||||
$entity->setName('foo');
|
||||
self::assertInstanceOf(ActivityRate::class, $sut->setActivity($entity));
|
||||
self::assertSame($entity, $sut->getActivity());
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ namespace App\Tests\Entity;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\ActivityMeta;
|
||||
use App\Entity\Project;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
@@ -27,8 +28,7 @@ class ActivityTest extends TestCase
|
||||
$this->assertNull($sut->getName());
|
||||
$this->assertNull($sut->getComment());
|
||||
$this->assertTrue($sut->getVisible());
|
||||
$this->assertNull($sut->getFixedRate());
|
||||
$this->assertNull($sut->getHourlyRate());
|
||||
$this->assertTrue($sut->isGlobal());
|
||||
$this->assertNull($sut->getColor());
|
||||
$this->assertEquals(0.0, $sut->getBudget());
|
||||
$this->assertEquals(0, $sut->getTimeBudget());
|
||||
@@ -53,17 +53,15 @@ class ActivityTest extends TestCase
|
||||
$this->assertInstanceOf(Activity::class, $sut->setColor('#fffccc'));
|
||||
$this->assertEquals('#fffccc', $sut->getColor());
|
||||
|
||||
$this->assertInstanceOf(Activity::class, $sut->setFixedRate(13.47));
|
||||
$this->assertEquals(13.47, $sut->getFixedRate());
|
||||
|
||||
$this->assertInstanceOf(Activity::class, $sut->setHourlyRate(99));
|
||||
$this->assertEquals(99, $sut->getHourlyRate());
|
||||
|
||||
$this->assertInstanceOf(Activity::class, $sut->setBudget(12345.67));
|
||||
$this->assertEquals(12345.67, $sut->getBudget());
|
||||
|
||||
$this->assertInstanceOf(Activity::class, $sut->setTimeBudget(937321));
|
||||
$this->assertEquals(937321, $sut->getTimeBudget());
|
||||
|
||||
$this->assertTrue($sut->isGlobal());
|
||||
$this->assertInstanceOf(Activity::class, $sut->setProject(new Project()));
|
||||
$this->assertFalse($sut->isGlobal());
|
||||
}
|
||||
|
||||
public function testMetaFields()
|
||||
|
||||
55
tests/Entity/CustomerRateTest.php
Normal file
55
tests/Entity/CustomerRateTest.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?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\Entity;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\CustomerRate;
|
||||
use App\Entity\User;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Entity\CustomerRate
|
||||
* @covers \App\Entity\Rate
|
||||
*/
|
||||
class CustomerRateTest extends TestCase
|
||||
{
|
||||
public function testDefaultValues()
|
||||
{
|
||||
$sut = new CustomerRate();
|
||||
self::assertNull($sut->getId());
|
||||
self::assertEquals(0.00, $sut->getRate());
|
||||
self::assertNull($sut->getCustomer());
|
||||
self::assertNull($sut->getUser());
|
||||
self::assertEquals(1, $sut->getScore());
|
||||
self::assertFalse($sut->isFixed());
|
||||
}
|
||||
|
||||
public function testSetterAndGetter()
|
||||
{
|
||||
$sut = new CustomerRate();
|
||||
|
||||
self::assertInstanceOf(CustomerRate::class, $sut->setIsFixed(true));
|
||||
self::assertTrue($sut->isFixed());
|
||||
|
||||
self::assertInstanceOf(CustomerRate::class, $sut->setRate(12.34));
|
||||
self::assertEquals(12.34, $sut->getRate());
|
||||
|
||||
$user = new User();
|
||||
$user->setAlias('foo');
|
||||
$user->setUsername('bar');
|
||||
self::assertInstanceOf(CustomerRate::class, $sut->setUser($user));
|
||||
self::assertSame($user, $sut->getUser());
|
||||
|
||||
$entity = new Customer();
|
||||
$entity->setName('foo');
|
||||
self::assertInstanceOf(CustomerRate::class, $sut->setCustomer($entity));
|
||||
self::assertSame($entity, $sut->getCustomer());
|
||||
}
|
||||
}
|
||||
@@ -43,8 +43,6 @@ class CustomerTest extends TestCase
|
||||
self::assertNull($sut->getHomepage());
|
||||
self::assertNull($sut->getTimezone());
|
||||
|
||||
self::assertNull($sut->getFixedRate());
|
||||
self::assertNull($sut->getHourlyRate());
|
||||
self::assertNull($sut->getColor());
|
||||
self::assertEquals(0.0, $sut->getBudget());
|
||||
self::assertEquals(0, $sut->getTimeBudget());
|
||||
@@ -92,12 +90,6 @@ class CustomerTest extends TestCase
|
||||
self::assertInstanceOf(Customer::class, $sut->setHomepage('https://www.example.com'));
|
||||
self::assertEquals('https://www.example.com', $sut->getHomepage());
|
||||
|
||||
self::assertInstanceOf(Customer::class, $sut->setFixedRate(13.47));
|
||||
self::assertEquals(13.47, $sut->getFixedRate());
|
||||
|
||||
self::assertInstanceOf(Customer::class, $sut->setHourlyRate(99));
|
||||
self::assertEquals(99, $sut->getHourlyRate());
|
||||
|
||||
self::assertInstanceOf(Customer::class, $sut->setBudget(12345.67));
|
||||
self::assertEquals(12345.67, $sut->getBudget());
|
||||
|
||||
|
||||
55
tests/Entity/ProjectRateTest.php
Normal file
55
tests/Entity/ProjectRateTest.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?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\Entity;
|
||||
|
||||
use App\Entity\Project;
|
||||
use App\Entity\ProjectRate;
|
||||
use App\Entity\User;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Entity\ProjectRate
|
||||
* @covers \App\Entity\Rate
|
||||
*/
|
||||
class ProjectRateTest extends TestCase
|
||||
{
|
||||
public function testDefaultValues()
|
||||
{
|
||||
$sut = new ProjectRate();
|
||||
self::assertNull($sut->getId());
|
||||
self::assertEquals(0.00, $sut->getRate());
|
||||
self::assertNull($sut->getProject());
|
||||
self::assertNull($sut->getUser());
|
||||
self::assertEquals(3, $sut->getScore());
|
||||
self::assertFalse($sut->isFixed());
|
||||
}
|
||||
|
||||
public function testSetterAndGetter()
|
||||
{
|
||||
$sut = new ProjectRate();
|
||||
|
||||
self::assertInstanceOf(ProjectRate::class, $sut->setIsFixed(true));
|
||||
self::assertTrue($sut->isFixed());
|
||||
|
||||
self::assertInstanceOf(ProjectRate::class, $sut->setRate(12.34));
|
||||
self::assertEquals(12.34, $sut->getRate());
|
||||
|
||||
$user = new User();
|
||||
$user->setAlias('foo');
|
||||
$user->setUsername('bar');
|
||||
self::assertInstanceOf(ProjectRate::class, $sut->setUser($user));
|
||||
self::assertSame($user, $sut->getUser());
|
||||
|
||||
$entity = new Project();
|
||||
$entity->setName('foo');
|
||||
self::assertInstanceOf(ProjectRate::class, $sut->setProject($entity));
|
||||
self::assertSame($entity, $sut->getProject());
|
||||
}
|
||||
}
|
||||
@@ -34,8 +34,6 @@ class ProjectTest extends TestCase
|
||||
self::assertNull($sut->getComment());
|
||||
self::assertTrue($sut->getVisible());
|
||||
self::assertTrue($sut->isVisible());
|
||||
self::assertNull($sut->getFixedRate());
|
||||
self::assertNull($sut->getHourlyRate());
|
||||
self::assertNull($sut->getColor());
|
||||
self::assertEquals(0.0, $sut->getBudget());
|
||||
self::assertEquals(0, $sut->getTimeBudget());
|
||||
@@ -85,12 +83,6 @@ class ProjectTest extends TestCase
|
||||
self::assertInstanceOf(Project::class, $sut->setVisible(false));
|
||||
self::assertFalse($sut->getVisible());
|
||||
|
||||
self::assertInstanceOf(Project::class, $sut->setFixedRate(13.47));
|
||||
self::assertEquals(13.47, $sut->getFixedRate());
|
||||
|
||||
self::assertInstanceOf(Project::class, $sut->setHourlyRate(99));
|
||||
self::assertEquals(99, $sut->getHourlyRate());
|
||||
|
||||
self::assertInstanceOf(Project::class, $sut->setBudget(12345.67));
|
||||
self::assertEquals(12345.67, $sut->getBudget());
|
||||
|
||||
|
||||
@@ -40,12 +40,6 @@ class InvoiceModelActivityHydratorTest extends TestCase
|
||||
'activity.id',
|
||||
'activity.name',
|
||||
'activity.comment',
|
||||
'activity.fixed_rate',
|
||||
'activity.fixed_rate_nc',
|
||||
'activity.fixed_rate_plain',
|
||||
'activity.hourly_rate',
|
||||
'activity.hourly_rate_nc',
|
||||
'activity.hourly_rate_plain',
|
||||
'activity.meta.foo-activity',
|
||||
];
|
||||
|
||||
|
||||
@@ -47,12 +47,6 @@ class InvoiceModelCustomerHydratorTest extends TestCase
|
||||
'customer.number',
|
||||
'customer.homepage',
|
||||
'customer.comment',
|
||||
'customer.fixed_rate',
|
||||
'customer.fixed_rate_nc',
|
||||
'customer.fixed_rate_plain',
|
||||
'customer.hourly_rate',
|
||||
'customer.hourly_rate_nc',
|
||||
'customer.hourly_rate_plain',
|
||||
'customer.meta.foo-customer',
|
||||
];
|
||||
|
||||
|
||||
@@ -42,12 +42,6 @@ class InvoiceModelProjectHydratorTest extends TestCase
|
||||
'project.comment',
|
||||
'project.order_date',
|
||||
'project.order_number',
|
||||
'project.fixed_rate',
|
||||
'project.fixed_rate_nc',
|
||||
'project.fixed_rate_plain',
|
||||
'project.hourly_rate',
|
||||
'project.hourly_rate_nc',
|
||||
'project.hourly_rate_plain',
|
||||
'project.meta.foo-project',
|
||||
'project.start_date',
|
||||
'project.end_date',
|
||||
|
||||
@@ -119,22 +119,10 @@ class DebugRendererTest extends TestCase
|
||||
'customer.number',
|
||||
'customer.homepage',
|
||||
'customer.comment',
|
||||
'customer.fixed_rate',
|
||||
'customer.fixed_rate_nc',
|
||||
'customer.fixed_rate_plain',
|
||||
'customer.hourly_rate',
|
||||
'customer.hourly_rate_nc',
|
||||
'customer.hourly_rate_plain',
|
||||
'customer.meta.foo-customer',
|
||||
'activity.id',
|
||||
'activity.name',
|
||||
'activity.comment',
|
||||
'activity.fixed_rate',
|
||||
'activity.fixed_rate_nc',
|
||||
'activity.fixed_rate_plain',
|
||||
'activity.hourly_rate',
|
||||
'activity.hourly_rate_nc',
|
||||
'activity.hourly_rate_plain',
|
||||
'activity.meta.foo-activity',
|
||||
'user.alias',
|
||||
'user.email',
|
||||
@@ -152,12 +140,6 @@ class DebugRendererTest extends TestCase
|
||||
'project.comment',
|
||||
'project.order_date',
|
||||
'project.order_number',
|
||||
'project.fixed_rate',
|
||||
'project.fixed_rate_nc',
|
||||
'project.fixed_rate_plain',
|
||||
'project.hourly_rate',
|
||||
'project.hourly_rate_nc',
|
||||
'project.hourly_rate_plain',
|
||||
'project.meta.foo-project',
|
||||
'project.start_date',
|
||||
'project.end_date',
|
||||
|
||||
@@ -10,11 +10,15 @@
|
||||
namespace App\Tests\Timesheet\Calculator;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\ActivityRate;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\CustomerRate;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\ProjectRate;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Entity\UserPreference;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Timesheet\Calculator\RateCalculator;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
@@ -23,6 +27,16 @@ use PHPUnit\Framework\TestCase;
|
||||
*/
|
||||
class RateCalculatorTest extends TestCase
|
||||
{
|
||||
protected function getRateRepositoryMock(array $rates = [])
|
||||
{
|
||||
$mock = $this->getMockBuilder(TimesheetRepository::class)->disableOriginalConstructor()->getMock();
|
||||
if (!empty($rates)) {
|
||||
$mock->expects($this->any())->method('findMatchingRates')->willReturn($rates);
|
||||
}
|
||||
|
||||
return $mock;
|
||||
}
|
||||
|
||||
public function testCalculateWithTimesheetHourlyRate()
|
||||
{
|
||||
$record = new Timesheet();
|
||||
@@ -31,7 +45,7 @@ class RateCalculatorTest extends TestCase
|
||||
$record->setHourlyRate(100);
|
||||
$record->setActivity(new Activity());
|
||||
|
||||
$sut = new RateCalculator([]);
|
||||
$sut = new RateCalculator([], $this->getRateRepositoryMock());
|
||||
$sut->calculate($record);
|
||||
$this->assertEquals(50, $record->getRate());
|
||||
}
|
||||
@@ -46,7 +60,7 @@ class RateCalculatorTest extends TestCase
|
||||
$record->setHourlyRate(99);
|
||||
$record->setActivity(new Activity());
|
||||
|
||||
$sut = new RateCalculator([]);
|
||||
$sut = new RateCalculator([], $this->getRateRepositoryMock());
|
||||
$sut->calculate($record);
|
||||
$this->assertEquals(10, $record->getRate());
|
||||
}
|
||||
@@ -93,24 +107,12 @@ class RateCalculatorTest extends TestCase
|
||||
$customerFixed
|
||||
) {
|
||||
$customer = new Customer();
|
||||
$customer
|
||||
->setHourlyRate($customerHourly)
|
||||
->setFixedRate($customerFixed)
|
||||
;
|
||||
|
||||
$project = new Project();
|
||||
$project
|
||||
->setHourlyRate($projectHourly)
|
||||
->setFixedRate($projectFixed)
|
||||
->setCustomer($customer)
|
||||
;
|
||||
$project->setCustomer($customer);
|
||||
|
||||
$activity = new Activity();
|
||||
$activity
|
||||
->setHourlyRate($activityHourly)
|
||||
->setFixedRate($activityFixed)
|
||||
->setProject($project)
|
||||
;
|
||||
$activity->setProject($project);
|
||||
|
||||
$timesheet = new Timesheet();
|
||||
$timesheet
|
||||
@@ -123,7 +125,27 @@ class RateCalculatorTest extends TestCase
|
||||
->setUser($this->getTestUser($userRate))
|
||||
;
|
||||
|
||||
$sut = new RateCalculator([]);
|
||||
$rates = [];
|
||||
|
||||
if (null !== $customerFixed) {
|
||||
$rates[] = (new CustomerRate())->setRate($customerFixed)->setIsFixed(true);
|
||||
} elseif (null !== $customerHourly) {
|
||||
$rates[] = (new CustomerRate())->setRate($customerHourly);
|
||||
}
|
||||
|
||||
if (null !== $projectFixed) {
|
||||
$rates[] = (new ProjectRate())->setRate($projectFixed)->setIsFixed(true);
|
||||
} elseif (null !== $projectHourly) {
|
||||
$rates[] = (new ProjectRate())->setRate($projectHourly);
|
||||
}
|
||||
|
||||
if (null !== $activityFixed) {
|
||||
$rates[] = (new ActivityRate())->setRate($activityFixed)->setIsFixed(true);
|
||||
} elseif (null !== $activityHourly) {
|
||||
$rates[] = (new ActivityRate())->setRate($activityHourly);
|
||||
}
|
||||
|
||||
$sut = new RateCalculator([], $this->getRateRepositoryMock($rates));
|
||||
$sut->calculate($timesheet);
|
||||
$this->assertEquals($exptectedRate, $timesheet->getRate());
|
||||
}
|
||||
@@ -151,7 +173,7 @@ class RateCalculatorTest extends TestCase
|
||||
|
||||
$this->assertEquals(0, $record->getRate());
|
||||
|
||||
$sut = new RateCalculator([]);
|
||||
$sut = new RateCalculator([], $this->getRateRepositoryMock());
|
||||
$sut->calculate($record);
|
||||
$this->assertEquals(0, $record->getRate());
|
||||
}
|
||||
@@ -177,7 +199,7 @@ class RateCalculatorTest extends TestCase
|
||||
|
||||
$record->setEnd($end);
|
||||
|
||||
$sut = new RateCalculator($rules);
|
||||
$sut = new RateCalculator($rules, $this->getRateRepositoryMock());
|
||||
$sut->calculate($record);
|
||||
|
||||
$this->assertEquals($expectedRate, $record->getRate());
|
||||
|
||||
@@ -66,7 +66,15 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="attachments">
|
||||
<source>attachments</source>
|
||||
<target>Files</target>
|
||||
<target>Dateien</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="rates.empty">
|
||||
<source>rates.empty</source>
|
||||
<target>Es wurden noch keine Gebühren hinterlegt.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="rates.title">
|
||||
<source>rates.title</source>
|
||||
<target>Gebühren</target>
|
||||
</trans-unit>
|
||||
|
||||
<!--
|
||||
|
||||
@@ -66,7 +66,15 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="attachments">
|
||||
<source>attachments</source>
|
||||
<target>Dateien</target>
|
||||
<target>Files</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="rates.empty">
|
||||
<source>rates.empty</source>
|
||||
<target>No fees have been configured yet.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="rates.title">
|
||||
<source>rates.title</source>
|
||||
<target>Fees</target>
|
||||
</trans-unit>
|
||||
|
||||
<!--
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user