refactored invoice and export screens (#1046)
This commit is contained in:
@@ -10,13 +10,17 @@ Perform EACH version specific task between your version and the new one, otherwi
|
||||
|
||||
## [1.2](https://github.com/kevinpapst/kimai2/releases/tag/1.2)
|
||||
|
||||
### BC breaks
|
||||
### Possible BC breaks
|
||||
|
||||
- Deleted timezone conversion command. If you are still using 0.7 or below, you need to upgrade to 1.1 before upgrading to this version.
|
||||
- Minimum password length raised from 5 to 8 character (applies only for password changes and new users)
|
||||
- Maximum customer name length lowered to 150 character
|
||||
- Maximum project name length lowered to 150 character
|
||||
- Maximum activity name length lowered to 150 character
|
||||
- Added new permission: `manage_invoice_template`
|
||||
- Removed permissions: `view_invoice_template`, `create_invoice_template`, `edit_invoice_template`, `delete_invoice_template`
|
||||
- Removed permission: `view_invoice` (using `create_export` only)
|
||||
- Export renderer need to check usage of `Timesheet::getEnd()` as running entries can now be exported as well
|
||||
|
||||
## [1.1](https://github.com/kevinpapst/kimai2/releases/tag/1.1)
|
||||
|
||||
|
||||
27
composer.lock
generated
27
composer.lock
generated
@@ -3902,16 +3902,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpoffice/phpspreadsheet",
|
||||
"version": "1.8.2",
|
||||
"version": "1.9.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
|
||||
"reference": "0c1346a1956347590b7db09533966307d20cb7cc"
|
||||
"reference": "8dea03eaf60a349b6097e4bcad11f894668280df"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/0c1346a1956347590b7db09533966307d20cb7cc",
|
||||
"reference": "0c1346a1956347590b7db09533966307d20cb7cc",
|
||||
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/8dea03eaf60a349b6097e4bcad11f894668280df",
|
||||
"reference": "8dea03eaf60a349b6097e4bcad11f894668280df",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3930,17 +3930,16 @@
|
||||
"ext-zlib": "*",
|
||||
"markbaker/complex": "^1.4",
|
||||
"markbaker/matrix": "^1.1",
|
||||
"php": "^5.6|^7.0",
|
||||
"php": "^7.1",
|
||||
"psr/simple-cache": "^1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/instantiator": "^1.0.0",
|
||||
"dompdf/dompdf": "^0.8.0",
|
||||
"friendsofphp/php-cs-fixer": "@stable",
|
||||
"jpgraph/jpgraph": "^4.0",
|
||||
"mpdf/mpdf": "^7.0.0",
|
||||
"phpcompatibility/php-compatibility": "^8.0",
|
||||
"phpunit/phpunit": "^5.7",
|
||||
"phpunit/phpunit": "^7.5",
|
||||
"squizlabs/php_codesniffer": "^3.3",
|
||||
"tecnickcom/tcpdf": "^6.2"
|
||||
},
|
||||
@@ -3961,12 +3960,6 @@
|
||||
"LGPL-2.1-or-later"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Erik Tilt"
|
||||
},
|
||||
{
|
||||
"name": "Adrien Crivelli"
|
||||
},
|
||||
{
|
||||
"name": "Maarten Balliauw",
|
||||
"homepage": "https://blog.maartenballiauw.be"
|
||||
@@ -3978,6 +3971,12 @@
|
||||
{
|
||||
"name": "Franck Lefevre",
|
||||
"homepage": "https://rootslabs.net"
|
||||
},
|
||||
{
|
||||
"name": "Erik Tilt"
|
||||
},
|
||||
{
|
||||
"name": "Adrien Crivelli"
|
||||
}
|
||||
],
|
||||
"description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine",
|
||||
@@ -3992,7 +3991,7 @@
|
||||
"xls",
|
||||
"xlsx"
|
||||
],
|
||||
"time": "2019-07-08T21:21:25+00:00"
|
||||
"time": "2019-08-17T22:24:35+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpoffice/phpword",
|
||||
|
||||
@@ -90,7 +90,7 @@ kimai:
|
||||
CUSTOMERS: ['view_customer','create_customer','edit_customer','budget_customer','delete_customer','permissions_customer']
|
||||
CUSTOMERS_TEAM: ['view_customer','edit_teamlead_customer','budget_teamlead_customer']
|
||||
INVOICE: ['view_invoice','create_invoice']
|
||||
INVOICE_TEMPLATE: ['view_invoice_template','create_invoice_template','edit_invoice_template','delete_invoice_template']
|
||||
INVOICE_TEMPLATE: ['manage_invoice_template']
|
||||
TIMESHEET: ['view_own_timesheet','start_own_timesheet','stop_own_timesheet','create_own_timesheet','edit_own_timesheet','export_own_timesheet','delete_own_timesheet']
|
||||
TIMESHEET_OTHER: ['view_other_timesheet','start_other_timesheet','stop_other_timesheet','create_other_timesheet','edit_other_timesheet','export_other_timesheet','delete_other_timesheet']
|
||||
PROFILE: ['view_own_profile','edit_own_profile','password_own_profile','preferences_own_profile','api-token_own_profile']
|
||||
@@ -99,16 +99,16 @@ kimai:
|
||||
USER: ['view_user','create_user','delete_user','role_permissions']
|
||||
RATE: ['view_rate_own_timesheet','edit_rate_own_timesheet']
|
||||
RATE_OTHER: ['view_rate_other_timesheet','edit_rate_other_timesheet']
|
||||
EXPORT: ['view_export','create_export','edit_export_own_timesheet','edit_export_other_timesheet']
|
||||
EXPORT: ['create_export','edit_export_own_timesheet','edit_export_other_timesheet']
|
||||
TEAMS: ['view_team','create_team','edit_team','delete_team']
|
||||
# some single default definitions for roles
|
||||
SINGLE_USER: []
|
||||
SINGLE_TEAMLEAD: ['!delete_invoice_template','view_rate_own_timesheet','view_rate_other_timesheet','hourly-rate_own_profile']
|
||||
SINGLE_TEAMLEAD: ['view_rate_own_timesheet','view_rate_other_timesheet','hourly-rate_own_profile']
|
||||
SINGLE_ADMIN: ['hourly-rate_own_profile','edit_exported_timesheet','teams_own_profile']
|
||||
SINGLE_SUPER_ADMIN: ['hourly-rate_own_profile','hourly-rate_other_profile','delete_own_profile','roles_own_profile','system_information','system_configuration','plugins','edit_exported_timesheet','teams_own_profile']
|
||||
# link above sets to one set for each user role
|
||||
ROLE_USER: ['@TIMESHEET','@PROFILE']
|
||||
ROLE_TEAMLEAD: ['@ACTIVITIES_TEAM','@PROJECTS_TEAM','@CUSTOMERS_TEAM','@TIMESHEET_OTHER','@INVOICE','@TIMESHEET','@PROFILE','@EXPORT','@TAGS','@INVOICE_TEMPLATE','@SINGLE_TEAMLEAD']
|
||||
ROLE_TEAMLEAD: ['@ACTIVITIES_TEAM','@PROJECTS_TEAM','@CUSTOMERS_TEAM','@TIMESHEET_OTHER','@INVOICE','@TIMESHEET','@PROFILE','@EXPORT','@TAGS','@SINGLE_TEAMLEAD']
|
||||
ROLE_ADMIN: ['@ACTIVITIES','@PROJECTS','@CUSTOMERS','@INVOICE','@INVOICE_TEMPLATE','@TIMESHEET','@TIMESHEET_OTHER','@PROFILE','@TEAMS','@RATE','@RATE_OTHER','@EXPORT','@TAGS','@SINGLE_ADMIN']
|
||||
ROLE_SUPER_ADMIN: ['@ACTIVITIES','@PROJECTS','@CUSTOMERS','@INVOICE','@INVOICE_TEMPLATE','@TIMESHEET','@TIMESHEET_OTHER','@PROFILE','@PROFILE_OTHER','@USER','@TEAMS','@RATE','@RATE_OTHER','@EXPORT','@TAGS','@SINGLE_SUPER_ADMIN']
|
||||
# mapping "sets" or permissions to user roles ("role name" = [array of "set names"])
|
||||
|
||||
@@ -3,6 +3,7 @@ includes:
|
||||
- vendor/phpstan/phpstan-doctrine/extension.neon
|
||||
|
||||
parameters:
|
||||
tmpDir: %rootDir%/../../../var/cache/phpstan
|
||||
autoload_directories:
|
||||
- %rootDir%/../../../src/Migrations
|
||||
# symfony:
|
||||
|
||||
@@ -17,6 +17,7 @@ use App\Repository\TimesheetRepository;
|
||||
use App\Timesheet\UserDateTimeFactory;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\SubmitButton;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
@@ -25,7 +26,7 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
* Controller used to export timesheet data.
|
||||
*
|
||||
* @Route(path="/export")
|
||||
* @Security("is_granted('view_export')")
|
||||
* @Security("is_granted('create_export')")
|
||||
*/
|
||||
class ExportController extends AbstractController
|
||||
{
|
||||
@@ -54,61 +55,45 @@ class ExportController extends AbstractController
|
||||
$this->dateFactory = $dateTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ExportQuery
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function getDefaultQuery()
|
||||
{
|
||||
$begin = $this->dateFactory->createDateTime('first day of this month 00:00:00');
|
||||
$end = $this->dateFactory->createDateTime('last day of this month 23:59:59');
|
||||
|
||||
$query = new ExportQuery();
|
||||
$query->setOrder(ExportQuery::ORDER_ASC);
|
||||
$query->setBegin($begin);
|
||||
$query->setEnd($end);
|
||||
$query->setState(ExportQuery::STATE_STOPPED);
|
||||
$query->setExported(ExportQuery::STATE_NOT_EXPORTED);
|
||||
$query->setCurrentUser($this->getUser());
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/", name="export", methods={"GET"})
|
||||
* @Security("is_granted('view_export')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function indexAction(Request $request)
|
||||
public function indexAction(Request $request): Response
|
||||
{
|
||||
$query = $this->getDefaultQuery();
|
||||
|
||||
$showPreview = false;
|
||||
$maxItemsPreview = 500;
|
||||
$entries = [];
|
||||
|
||||
$form = $this->getToolbarForm($query, 'GET');
|
||||
$form->setData($query);
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
$entries = $this->getEntries($query);
|
||||
if ($form->isValid()) {
|
||||
/** @var SubmitButton $previewButton */
|
||||
$previewButton = $form->get('preview');
|
||||
if ($previewButton->isClicked()) {
|
||||
$showPreview = true;
|
||||
$query->setPageSize($maxItemsPreview);
|
||||
$entries = $this->getEntries($query);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('export/index.html.twig', [
|
||||
'query' => $query,
|
||||
'entries' => $entries,
|
||||
'form' => $form->createView(),
|
||||
'renderer' => $this->export->getRenderer(),
|
||||
'preview_max' => $maxItemsPreview,
|
||||
'preview_show' => $showPreview,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/data", name="export_data", methods={"POST"})
|
||||
* @Security("is_granted('create_export')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function export(Request $request)
|
||||
public function export(Request $request): Response
|
||||
{
|
||||
$query = $this->getDefaultQuery();
|
||||
|
||||
@@ -127,8 +112,29 @@ class ExportController extends AbstractController
|
||||
}
|
||||
|
||||
$entries = $this->getEntries($query);
|
||||
$response = $renderer->render($entries, $query);
|
||||
|
||||
return $renderer->render($entries, $query);
|
||||
if ($query->isMarkAsExported()) {
|
||||
$this->timesheetRepository->setExported($entries);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
protected function getDefaultQuery(): ExportQuery
|
||||
{
|
||||
$begin = $this->dateFactory->createDateTime('first day of this month 00:00:00');
|
||||
$end = $this->dateFactory->createDateTime('last day of this month 23:59:59');
|
||||
|
||||
$query = new ExportQuery();
|
||||
$query->setOrder(ExportQuery::ORDER_ASC);
|
||||
$query->setBegin($begin);
|
||||
$query->setEnd($end);
|
||||
$query->setState(ExportQuery::STATE_STOPPED);
|
||||
$query->setExported(ExportQuery::STATE_NOT_EXPORTED);
|
||||
$query->setCurrentUser($this->getUser());
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,14 +22,16 @@ use App\Repository\TimesheetRepository;
|
||||
use App\Timesheet\UserDateTimeFactory;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\SubmitButton;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* Controller used to manage invoices.
|
||||
* Controller used to create invoices and manage invoice templates.
|
||||
*
|
||||
* @Route(path="/invoice")
|
||||
* @Security("is_granted('view_invoice') or is_granted('view_invoice_template')")
|
||||
* @Security("is_granted('view_invoice')")
|
||||
*/
|
||||
class InvoiceController extends AbstractController
|
||||
{
|
||||
@@ -58,26 +60,7 @@ class InvoiceController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @return InvoiceQuery
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function getDefaultQuery()
|
||||
{
|
||||
$begin = $this->dateTimeFactory->createDateTime('first day of this month');
|
||||
$end = $this->dateTimeFactory->createDateTime('last day of this month');
|
||||
|
||||
$query = new InvoiceQuery();
|
||||
$query->setOrder(InvoiceQuery::ORDER_ASC);
|
||||
$query->setBegin($begin);
|
||||
$query->setEnd($end);
|
||||
$query->setState(InvoiceQuery::STATE_STOPPED);
|
||||
$query->setCurrentUser($this->getUser());
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/", name="invoice", methods={"GET"})
|
||||
* @Route(path="/", name="invoice", methods={"GET", "POST"})
|
||||
* @Security("is_granted('view_invoice')")
|
||||
*
|
||||
* @param Request $request
|
||||
@@ -87,9 +70,14 @@ class InvoiceController extends AbstractController
|
||||
public function indexAction(Request $request, TimesheetRepository $repository)
|
||||
{
|
||||
if (!$this->invoiceRepository->hasTemplate()) {
|
||||
return $this->redirectToRoute('admin_invoice_template_create');
|
||||
if ($this->isGranted('manage_invoice_template')) {
|
||||
return $this->redirectToRoute('admin_invoice_template_create');
|
||||
}
|
||||
$this->flashWarning('invoice.first_template');
|
||||
}
|
||||
|
||||
$showPreview = false;
|
||||
$maxItemsPreview = 500;
|
||||
$entries = [];
|
||||
|
||||
$query = $this->getDefaultQuery();
|
||||
@@ -97,8 +85,22 @@ class InvoiceController extends AbstractController
|
||||
$form->setData($query);
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
if ($form->isValid()) {
|
||||
$entries = $this->getEntries($query, $repository);
|
||||
if ($this->isGranted('create_invoice')) {
|
||||
if ($form->isValid()) {
|
||||
/** @var SubmitButton $createButton */
|
||||
$createButton = $form->get('create');
|
||||
if ($createButton->isClicked()) {
|
||||
return $this->renderInvoice($query, $repository);
|
||||
}
|
||||
|
||||
/** @var SubmitButton $previewButton */
|
||||
$previewButton = $form->get('preview');
|
||||
if ($previewButton->isClicked()) {
|
||||
$showPreview = true;
|
||||
$query->setPageSize($maxItemsPreview);
|
||||
$entries = $this->getEntries($query, $repository);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$model = $this->prepareModel($query, $entries);
|
||||
@@ -106,34 +108,29 @@ class InvoiceController extends AbstractController
|
||||
return $this->render('invoice/index.html.twig', [
|
||||
'model' => $model,
|
||||
'form' => $form->createView(),
|
||||
'preview_max' => $maxItemsPreview,
|
||||
'preview_show' => $showPreview,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/print", name="invoice_print", methods={"POST"})
|
||||
* @Security("is_granted('create_invoice')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function printAction(Request $request, TimesheetRepository $repository)
|
||||
protected function getDefaultQuery(): InvoiceQuery
|
||||
{
|
||||
if (!$this->invoiceRepository->hasTemplate()) {
|
||||
return $this->redirectToRoute('admin_invoice_template_create');
|
||||
}
|
||||
$begin = $this->dateTimeFactory->createDateTime('first day of this month');
|
||||
$end = $this->dateTimeFactory->createDateTime('last day of this month');
|
||||
|
||||
$query = $this->getDefaultQuery();
|
||||
$form = $this->getToolbarForm($query, 'POST');
|
||||
$query = new InvoiceQuery();
|
||||
$query->setOrder(InvoiceQuery::ORDER_ASC);
|
||||
$query->setBegin($begin);
|
||||
$query->setEnd($end);
|
||||
$query->setExported(InvoiceQuery::STATE_NOT_EXPORTED);
|
||||
$query->setState(InvoiceQuery::STATE_STOPPED);
|
||||
$query->setCurrentUser($this->getUser());
|
||||
|
||||
$form->handleRequest($request);
|
||||
return $query;
|
||||
}
|
||||
|
||||
if (!$form->isSubmitted() || !$form->isValid()) {
|
||||
return $this->redirectToRoute('invoice');
|
||||
}
|
||||
|
||||
/** @var InvoiceQuery $query */
|
||||
$query = $form->getData();
|
||||
protected function renderInvoice(InvoiceQuery $query, TimesheetRepository $repository)
|
||||
{
|
||||
$entries = $this->getEntries($query, $repository);
|
||||
$model = $this->prepareModel($query, $entries);
|
||||
|
||||
@@ -144,23 +141,28 @@ class InvoiceController extends AbstractController
|
||||
|
||||
foreach ($this->service->getRenderer() as $renderer) {
|
||||
if ($renderer->supports($document)) {
|
||||
return $renderer->render($document, $model);
|
||||
$response = $renderer->render($document, $model);
|
||||
if ($query->isMarkAsExported()) {
|
||||
$repository->setExported($entries);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
$this->flashError('Cannot render invoice: ' . $model->getTemplate()->getRenderer() . ' (' . $document->getName() . ')');
|
||||
$this->flashError(
|
||||
sprintf('Cannot render invoice: %s (%s)', $model->getTemplate()->getRenderer(), $document->getName())
|
||||
);
|
||||
|
||||
return $this->render('invoice/index.html.twig', [
|
||||
'model' => $model,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
return $this->redirectToRoute('invoice');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InvoiceQuery $query
|
||||
* @param TimesheetRepository $repository
|
||||
* @return Timesheet[]
|
||||
*/
|
||||
protected function getEntries(InvoiceQuery $query, TimesheetRepository $repository)
|
||||
protected function getEntries(InvoiceQuery $query, TimesheetRepository $repository): iterable
|
||||
{
|
||||
// customer needs to be defined, as we need the currency for the invoice
|
||||
if (null === $query->getCustomer()) {
|
||||
@@ -181,11 +183,11 @@ class InvoiceController extends AbstractController
|
||||
|
||||
/**
|
||||
* @param InvoiceQuery $query
|
||||
* @param array $entries
|
||||
* @param Timesheet[] $entries
|
||||
* @return InvoiceModel
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function prepareModel(InvoiceQuery $query, array $entries)
|
||||
protected function prepareModel(InvoiceQuery $query, array $entries): InvoiceModel
|
||||
{
|
||||
$model = new InvoiceModel();
|
||||
$model
|
||||
@@ -214,33 +216,23 @@ class InvoiceController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/template", defaults={"page": 1}, name="admin_invoice_template", methods={"GET", "POST"})
|
||||
* @Route(path="/template/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_invoice_template_paginated", methods={"GET", "POST"})
|
||||
* @Security("is_granted('view_invoice_template')")
|
||||
*
|
||||
* @param int $page
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
* @Route(path="/template", name="admin_invoice_template", methods={"GET", "POST"})
|
||||
* @Security("is_granted('manage_invoice_template')")
|
||||
*/
|
||||
public function listTemplateAction($page)
|
||||
public function listTemplateAction(): Response
|
||||
{
|
||||
$templates = $this->invoiceRepository->findByQuery(new BaseQuery());
|
||||
|
||||
return $this->render('invoice/templates.html.twig', [
|
||||
'entries' => $templates,
|
||||
'page' => $page,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/template/{id}/edit", name="admin_invoice_template_edit", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', template)")
|
||||
*
|
||||
* @param InvoiceTemplate $template
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
* @throws \Exception
|
||||
* @Security("is_granted('manage_invoice_template')")
|
||||
*/
|
||||
public function editTemplateAction(InvoiceTemplate $template, Request $request)
|
||||
public function editTemplateAction(InvoiceTemplate $template, Request $request): Response
|
||||
{
|
||||
return $this->renderTemplateForm($template, $request);
|
||||
}
|
||||
@@ -248,14 +240,9 @@ class InvoiceController extends AbstractController
|
||||
/**
|
||||
* @Route(path="/template/create", name="admin_invoice_template_create", methods={"GET", "POST"})
|
||||
* @Route(path="/template/create/{id}", name="admin_invoice_template_copy", methods={"GET", "POST"})
|
||||
* @Security("is_granted('create_invoice_template')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @param InvoiceTemplate|null $copyFrom
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
* @throws \Exception
|
||||
* @Security("is_granted('manage_invoice_template')")
|
||||
*/
|
||||
public function createTemplateAction(Request $request, ?InvoiceTemplate $copyFrom)
|
||||
public function createTemplateAction(Request $request, ?InvoiceTemplate $copyFrom): Response
|
||||
{
|
||||
if (!$this->invoiceRepository->hasTemplate()) {
|
||||
$this->flashWarning('invoice.first_template');
|
||||
@@ -281,16 +268,10 @@ class InvoiceController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* The route to delete an existing template.
|
||||
*
|
||||
* @Route(path="/template/{id}/delete", name="admin_invoice_template_delete", methods={"GET", "POST"})
|
||||
* @Security("is_granted('delete', template)")
|
||||
*
|
||||
* @param InvoiceTemplate $template
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
* @Security("is_granted('manage_invoice_template')")
|
||||
*/
|
||||
public function deleteTemplate(InvoiceTemplate $template, Request $request)
|
||||
public function deleteTemplate(InvoiceTemplate $template, Request $request): Response
|
||||
{
|
||||
try {
|
||||
$this->invoiceRepository->removeTemplate($template);
|
||||
@@ -299,15 +280,10 @@ class InvoiceController extends AbstractController
|
||||
$this->flashError('action.delete.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('admin_invoice_template_paginated', ['page' => $request->get('page')]);
|
||||
return $this->redirectToRoute('admin_invoice_template');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InvoiceTemplate $template
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
protected function renderTemplateForm(InvoiceTemplate $template, Request $request)
|
||||
protected function renderTemplateForm(InvoiceTemplate $template, Request $request): Response
|
||||
{
|
||||
$editForm = $this->createEditForm($template);
|
||||
|
||||
@@ -341,11 +317,7 @@ class InvoiceController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InvoiceTemplate $template
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
*/
|
||||
private function createEditForm(InvoiceTemplate $template)
|
||||
private function createEditForm(InvoiceTemplate $template): FormInterface
|
||||
{
|
||||
if ($template->getId() === null) {
|
||||
$url = $this->generateUrl('admin_invoice_template_create');
|
||||
|
||||
@@ -84,7 +84,7 @@ final class MenuSubscriber implements EventSubscriberInterface
|
||||
);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('view_export')) {
|
||||
if ($auth->isGranted('create_export')) {
|
||||
$menu->addItem(
|
||||
new MenuItemModel('export', 'menu.export', 'export', [], $this->getIcon('export'))
|
||||
);
|
||||
|
||||
@@ -12,9 +12,12 @@ namespace App\Export\Renderer;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Twig\DateExtensions;
|
||||
use App\Twig\Extensions;
|
||||
use DateTime;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\Date;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Border;
|
||||
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
||||
@@ -22,72 +25,88 @@ use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
abstract class AbstractSpreadsheetRenderer
|
||||
{
|
||||
public const DATETIME_FORMAT = 'yyyy-mm-dd hh:mm';
|
||||
public const TIME_FORMAT = 'hh:mm';
|
||||
public const DURATION_FORMAT = '[hh]:mm';
|
||||
public const RATE_FORMAT_DEFAULT = '#.##0,00 [$%1$s];-#.##0,00 [$%1$s]';
|
||||
public const RATE_FORMAT_LEFT = '_("%1$s"* #,##0.00_);_("%1$s"* \(#,##0.00\);_("%1$s"* "-"??_);_(@_)';
|
||||
public const RATE_FORMAT = self::RATE_FORMAT_LEFT;
|
||||
|
||||
/**
|
||||
* @var DateExtensions
|
||||
*/
|
||||
protected $dateExtension;
|
||||
/**
|
||||
* @var Extensions
|
||||
*/
|
||||
protected $extension;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
protected $translator;
|
||||
|
||||
/**
|
||||
* @param TranslatorInterface $translator
|
||||
* @param DateExtensions $dateExtension
|
||||
* @param Extensions $extensions
|
||||
*/
|
||||
public function __construct(
|
||||
TranslatorInterface $translator,
|
||||
DateExtensions $dateExtension,
|
||||
Extensions $extensions
|
||||
) {
|
||||
public function __construct(TranslatorInterface $translator, DateExtensions $dateExtension)
|
||||
{
|
||||
$this->translator = $translator;
|
||||
$this->dateExtension = $dateExtension;
|
||||
$this->extension = $extensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $date
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getFormattedDateTime(\DateTime $date)
|
||||
protected function setFormattedDateTime(Worksheet $sheet, $column, $row, ?DateTime $date)
|
||||
{
|
||||
return $this->dateExtension->dateShort($date) . ' ' . $this->dateExtension->time($date);
|
||||
}
|
||||
if (null === $date) {
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, '');
|
||||
|
||||
/**
|
||||
* @param int $amount
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getFormattedMoney($amount, $currency)
|
||||
{
|
||||
return $this->extension->money($amount, $currency);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $timesheet
|
||||
* @return string
|
||||
*/
|
||||
protected function getUsername(Timesheet $timesheet)
|
||||
{
|
||||
if (!empty($timesheet->getUser()->getAlias())) {
|
||||
return $timesheet->getUser()->getAlias();
|
||||
return;
|
||||
}
|
||||
|
||||
return $timesheet->getUser()->getUsername();
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, Date::PHPToExcel($date));
|
||||
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(self::DATETIME_FORMAT);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $seconds
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getFormattedDuration($seconds)
|
||||
protected function setFormattedTime(Worksheet $sheet, $column, $row, ?DateTime $date)
|
||||
{
|
||||
return $this->extension->duration($seconds);
|
||||
if (null === $date) {
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, '');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, Date::PHPToExcel($date));
|
||||
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(self::TIME_FORMAT);
|
||||
}
|
||||
|
||||
protected function setFormattedDate(Worksheet $sheet, $column, $row, ?DateTime $date)
|
||||
{
|
||||
if (null === $date) {
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, '');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, Date::PHPToExcel($date));
|
||||
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_DATE_YYYYMMDD2);
|
||||
}
|
||||
|
||||
protected function setDurationTotal(Worksheet $sheet, $column, $row, $startCoordinate, $endCoordinate)
|
||||
{
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, sprintf('=SUM(%s:%s)', $startCoordinate, $endCoordinate));
|
||||
$style = $sheet->getStyleByColumnAndRow($column, $row);
|
||||
$style->getNumberFormat()->setFormatCode(self::DURATION_FORMAT);
|
||||
}
|
||||
|
||||
protected function setDuration(Worksheet $sheet, $column, $row, $duration)
|
||||
{
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, sprintf('=%s/86400', $duration));
|
||||
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(self::DURATION_FORMAT);
|
||||
}
|
||||
|
||||
protected function setRateTotal(Worksheet $sheet, $column, $row, $startCoordinate, $endCoordinate)
|
||||
{
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, sprintf('=SUM(%s:%s)', $startCoordinate, $endCoordinate));
|
||||
}
|
||||
|
||||
protected function setRate(Worksheet $sheet, $column, $row, $rate, $currency)
|
||||
{
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, $rate);
|
||||
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(
|
||||
sprintf(self::RATE_FORMAT_LEFT, $currency)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,8 +132,11 @@ abstract class AbstractSpreadsheetRenderer
|
||||
$recordsHeaderColumn = 1;
|
||||
$recordsHeaderRow = 1;
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.date'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.begin'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.end'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.duration'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.rate'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.user'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.customer'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.project'));
|
||||
@@ -122,45 +144,67 @@ abstract class AbstractSpreadsheetRenderer
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.description'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.exported'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.tags'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.hourlyRate'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.fixedRate'));
|
||||
foreach ($publicMetaFields as $metaFieldName) {
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans($metaFieldName));
|
||||
}
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.hourlyRate'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.fixedRate'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.duration'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn, $recordsHeaderRow, $this->translator->trans('label.rate'));
|
||||
|
||||
$entryHeaderRow = $recordsHeaderRow + 1;
|
||||
|
||||
$durationTotal = 0;
|
||||
$currency = false;
|
||||
$rateTotal = 0;
|
||||
$durationColumn = null;
|
||||
$rateColumn = null;
|
||||
|
||||
foreach ($timesheets as $timesheet) {
|
||||
$entryHeaderColumn = 1;
|
||||
|
||||
$durationTotal += $timesheet->getDuration();
|
||||
$rateTotal += $timesheet->getRate();
|
||||
if ($currency === false) {
|
||||
$currency = $timesheet->getProject()->getCustomer()->getCurrency();
|
||||
}
|
||||
if ($currency !== $timesheet->getProject()->getCustomer()->getCurrency()) {
|
||||
$currency = null;
|
||||
}
|
||||
|
||||
$customerCurrency = $timesheet->getProject()->getCustomer()->getCurrency();
|
||||
|
||||
$exported = $timesheet->isExported() ? 'entryState.exported' : 'entryState.not_exported';
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $this->getFormattedDateTime($timesheet->getBegin()));
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $this->getFormattedDateTime($timesheet->getEnd()));
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $this->getUsername($timesheet));
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $timesheet->getProject()->getCustomer()->getName());
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $timesheet->getProject()->getName());
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $timesheet->getActivity()->getName());
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $timesheet->getDescription());
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $this->translator->trans($exported));
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, implode(',', $timesheet->getTagsAsArray()));
|
||||
$this->setFormattedDate($sheet, $entryHeaderColumn, $entryHeaderRow, $timesheet->getBegin());
|
||||
$entryHeaderColumn++;
|
||||
|
||||
$this->setFormattedTime($sheet, $entryHeaderColumn, $entryHeaderRow, $timesheet->getBegin());
|
||||
$entryHeaderColumn++;
|
||||
|
||||
$this->setFormattedTime($sheet, $entryHeaderColumn, $entryHeaderRow, $timesheet->getEnd());
|
||||
$entryHeaderColumn++;
|
||||
|
||||
$this->setDuration($sheet, $entryHeaderColumn, $entryHeaderRow, $timesheet->getDuration());
|
||||
$durationColumn = $entryHeaderColumn;
|
||||
$entryHeaderColumn++;
|
||||
|
||||
$this->setRate($sheet, $entryHeaderColumn, $entryHeaderRow, $timesheet->getRate(), $customerCurrency);
|
||||
$rateColumn = $entryHeaderColumn;
|
||||
$entryHeaderColumn++;
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn, $entryHeaderRow, $this->getUsername($timesheet));
|
||||
$entryHeaderColumn++;
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn, $entryHeaderRow, $timesheet->getProject()->getCustomer()->getName());
|
||||
$entryHeaderColumn++;
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn, $entryHeaderRow, $timesheet->getProject()->getName());
|
||||
$entryHeaderColumn++;
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn, $entryHeaderRow, $timesheet->getActivity()->getName());
|
||||
$entryHeaderColumn++;
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn, $entryHeaderRow, $timesheet->getDescription());
|
||||
$entryHeaderColumn++;
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn, $entryHeaderRow, $this->translator->trans($exported));
|
||||
$entryHeaderColumn++;
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn, $entryHeaderRow, implode(',', $timesheet->getTagsAsArray()));
|
||||
$entryHeaderColumn++;
|
||||
|
||||
$this->setRate($sheet, $entryHeaderColumn, $entryHeaderRow, $timesheet->getHourlyRate(), $customerCurrency);
|
||||
$entryHeaderColumn++;
|
||||
|
||||
$this->setRate($sheet, $entryHeaderColumn, $entryHeaderRow, $timesheet->getFixedRate(), $customerCurrency);
|
||||
$entryHeaderColumn++;
|
||||
|
||||
foreach ($publicMetaFields as $metaFieldName) {
|
||||
$metaField = $timesheet->getMetaField($metaFieldName);
|
||||
$metaFieldValue = '';
|
||||
@@ -169,28 +213,40 @@ abstract class AbstractSpreadsheetRenderer
|
||||
}
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $metaFieldValue);
|
||||
}
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $this->getFormattedMoney($timesheet->getHourlyRate(), $customerCurrency));
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $this->getFormattedMoney($timesheet->getFixedRate(), $customerCurrency));
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $this->getFormattedDuration($timesheet->getDuration()));
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn, $entryHeaderRow, $this->getFormattedMoney($timesheet->getRate(), $customerCurrency));
|
||||
|
||||
$entryHeaderRow++;
|
||||
}
|
||||
|
||||
$cellDurationTotal = $recordsHeaderColumn - 1;
|
||||
$cellRateTotal = $recordsHeaderColumn;
|
||||
if (null !== $durationColumn) {
|
||||
$startCoordinate = $sheet->getCellByColumnAndRow($durationColumn, 2)->getCoordinate();
|
||||
$endCoordinate = $sheet->getCellByColumnAndRow($durationColumn, $entryHeaderRow - 1)->getCoordinate();
|
||||
$this->setDurationTotal($sheet, $durationColumn, $entryHeaderRow, $startCoordinate, $endCoordinate);
|
||||
$style = $sheet->getStyleByColumnAndRow($durationColumn, $entryHeaderRow);
|
||||
$style->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
|
||||
$style->getFont()->setBold(true);
|
||||
}
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($cellDurationTotal, $entryHeaderRow, $this->getFormattedDuration($durationTotal));
|
||||
$sheet->setCellValueByColumnAndRow($cellRateTotal, $entryHeaderRow, $this->getFormattedMoney($rateTotal, $currency));
|
||||
$sheet->getCellByColumnAndRow($cellDurationTotal, $entryHeaderRow)->getStyle()->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
|
||||
$sheet->getCellByColumnAndRow($cellDurationTotal, $entryHeaderRow)->getStyle()->getFont()->setBold(true);
|
||||
|
||||
$sheet->getCellByColumnAndRow($cellRateTotal, $entryHeaderRow)->getStyle()->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
|
||||
$sheet->getCellByColumnAndRow($cellRateTotal, $entryHeaderRow)->getStyle()->getFont()->setBold(true);
|
||||
if (null !== $rateColumn) {
|
||||
$startCoordinate = $sheet->getCellByColumnAndRow($rateColumn, 2)->getCoordinate();
|
||||
$endCoordinate = $sheet->getCellByColumnAndRow($rateColumn, $entryHeaderRow - 1)->getCoordinate();
|
||||
$this->setRateTotal($sheet, $rateColumn, $entryHeaderRow, $startCoordinate, $endCoordinate);
|
||||
$style = $sheet->getStyleByColumnAndRow($rateColumn, $entryHeaderRow);
|
||||
$style->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
|
||||
$style->getFont()->setBold(true);
|
||||
}
|
||||
|
||||
return $spreadsheet;
|
||||
}
|
||||
|
||||
protected function getUsername(Timesheet $timesheet): string
|
||||
{
|
||||
if (!empty($timesheet->getUser()->getAlias())) {
|
||||
return $timesheet->getUser()->getAlias();
|
||||
}
|
||||
|
||||
return $timesheet->getUser()->getUsername();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet[] $timesheets
|
||||
* @param TimesheetQuery $query
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace App\Export\Renderer;
|
||||
use App\Export\RendererInterface;
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
|
||||
final class CsvRenderer extends AbstractSpreadsheetRenderer implements RendererInterface
|
||||
{
|
||||
@@ -72,4 +73,9 @@ final class CsvRenderer extends AbstractSpreadsheetRenderer implements RendererI
|
||||
{
|
||||
return 'csv';
|
||||
}
|
||||
|
||||
protected function setDuration(Worksheet $sheet, $column, $row, $duration)
|
||||
{
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, sprintf('=%s', $duration));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,75 +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\Export\Renderer;
|
||||
|
||||
use App\Export\RendererInterface;
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
|
||||
final class OdsRenderer extends AbstractSpreadsheetRenderer implements RendererInterface
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getFileExtension(): string
|
||||
{
|
||||
return '.ods';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getContentType(): string
|
||||
{
|
||||
return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Spreadsheet $spreadsheet
|
||||
* @return string
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function saveSpreadsheet(Spreadsheet $spreadsheet): string
|
||||
{
|
||||
$filename = tempnam(sys_get_temp_dir(), 'kimai-export-ods');
|
||||
if (false === $filename) {
|
||||
throw new \Exception('Could not open temporary file');
|
||||
}
|
||||
|
||||
$writer = IOFactory::createWriter($spreadsheet, 'Ods');
|
||||
$writer->save($filename);
|
||||
|
||||
return $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getId(): string
|
||||
{
|
||||
return 'ods';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getIcon(): string
|
||||
{
|
||||
return 'ods';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'ods';
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,9 @@ use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\ActivityFormTypeQuery;
|
||||
use App\Repository\Query\CustomerFormTypeQuery;
|
||||
use App\Repository\Query\ProjectFormTypeQuery;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
@@ -48,9 +50,6 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
*/
|
||||
protected function addUserChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('user', UserType::class, [
|
||||
@@ -59,17 +58,26 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
]);
|
||||
}
|
||||
|
||||
protected function addCustomerChoice(FormBuilderInterface $builder)
|
||||
protected function addUsersChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('users', UserType::class, [
|
||||
'label' => 'label.user',
|
||||
'multiple' => true,
|
||||
'required' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function addCustomerChoice(FormBuilderInterface $builder, bool $required = false)
|
||||
{
|
||||
// just a fake field for having this field at the right position in the frontend
|
||||
$builder->add('customer', HiddenType::class);
|
||||
|
||||
$builder->addEventListener(
|
||||
FormEvents::PRE_SUBMIT,
|
||||
function (FormEvent $event) use ($builder) {
|
||||
function (FormEvent $event) use ($builder, $required) {
|
||||
$data = $event->getData();
|
||||
$event->getForm()->add('customer', CustomerType::class, [
|
||||
'required' => false,
|
||||
'required' => $required,
|
||||
'project_enabled' => true,
|
||||
'query_builder' => function (CustomerRepository $repo) use ($builder, $data) {
|
||||
$query = new CustomerFormTypeQuery();
|
||||
@@ -85,10 +93,6 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
* @param string $label
|
||||
*/
|
||||
protected function addVisibilityChoice(FormBuilderInterface $builder, string $label = 'label.visible')
|
||||
{
|
||||
$builder->add('visibility', VisibilityType::class, [
|
||||
@@ -98,9 +102,6 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
*/
|
||||
protected function addPageSizeChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('pageSize', PageSizeType::class, [
|
||||
@@ -108,9 +109,6 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
*/
|
||||
protected function addUserRoleChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('role', UserRoleType::class, [
|
||||
@@ -118,9 +116,6 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
*/
|
||||
protected function addDateRangeChoice(FormBuilderInterface $builder, $allowEmpty = true)
|
||||
{
|
||||
$builder->add('daterange', DateRangeType::class, [
|
||||
@@ -129,9 +124,6 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
*/
|
||||
protected function addProjectChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
// just a fake field for having this field at the right position in the frontend
|
||||
@@ -162,9 +154,6 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
*/
|
||||
protected function addActivityChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
// just a fake field for having this field at the right position in the frontend
|
||||
@@ -193,9 +182,6 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
*/
|
||||
protected function addHiddenPagination(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('page', HiddenType::class, [
|
||||
@@ -203,13 +189,38 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
*/
|
||||
protected function addTagInputField(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('tags', TagsInputType::class, [
|
||||
'required' => false
|
||||
]);
|
||||
}
|
||||
|
||||
protected function addTimesheetStateChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('state', ChoiceType::class, [
|
||||
'label' => 'label.entryState',
|
||||
'required' => false,
|
||||
'placeholder' => null,
|
||||
'choices' => [
|
||||
'entryState.all' => TimesheetQuery::STATE_ALL,
|
||||
'entryState.running' => TimesheetQuery::STATE_RUNNING,
|
||||
'entryState.stopped' => TimesheetQuery::STATE_STOPPED
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
protected function addExportStateChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('exported', ChoiceType::class, [
|
||||
'label' => 'label.exported',
|
||||
'required' => false,
|
||||
'placeholder' => null,
|
||||
'choices' => [
|
||||
'entryState.all' => TimesheetQuery::STATE_ALL,
|
||||
'entryState.exported' => TimesheetQuery::STATE_EXPORTED,
|
||||
'entryState.not_exported' => TimesheetQuery::STATE_NOT_EXPORTED
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
namespace App\Form\Toolbar;
|
||||
|
||||
use App\Repository\Query\ExportQuery;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
@@ -27,13 +27,21 @@ class ExportToolbarForm extends AbstractToolbarForm
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$this->addExportStateChoice($builder);
|
||||
$this->addUserChoice($builder);
|
||||
$this->addTimesheetStateChoice($builder);
|
||||
$this->addUsersChoice($builder);
|
||||
$this->addDateRangeChoice($builder);
|
||||
$this->addCustomerChoice($builder);
|
||||
$this->addProjectChoice($builder);
|
||||
$this->addActivityChoice($builder);
|
||||
$this->addExportType($builder);
|
||||
$this->addTagInputField($builder);
|
||||
$builder->add('markAsExported', CheckboxType::class, [
|
||||
'label' => 'label.mark_as_exported',
|
||||
'required' => false,
|
||||
]);
|
||||
$builder->add('preview', SubmitType::class, [
|
||||
'label' => 'button.preview',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,23 +52,6 @@ class ExportToolbarForm extends AbstractToolbarForm
|
||||
$builder->add('type', HiddenType::class, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
*/
|
||||
protected function addExportStateChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('exported', ChoiceType::class, [
|
||||
'label' => 'label.exported',
|
||||
'required' => false,
|
||||
'placeholder' => null,
|
||||
'choices' => [
|
||||
'entryState.all' => TimesheetQuery::STATE_ALL,
|
||||
'entryState.exported' => TimesheetQuery::STATE_EXPORTED,
|
||||
'entryState.not_exported' => TimesheetQuery::STATE_NOT_EXPORTED
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
||||
@@ -11,6 +11,8 @@ namespace App\Form\Toolbar;
|
||||
|
||||
use App\Form\Type\InvoiceTemplateType;
|
||||
use App\Repository\Query\InvoiceQuery;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
@@ -25,18 +27,30 @@ class InvoiceToolbarForm extends AbstractToolbarForm
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$this->addTemplateChoice($builder);
|
||||
$this->addUserChoice($builder);
|
||||
$this->addUsersChoice($builder);
|
||||
$this->addDateRangeChoice($builder);
|
||||
$this->addCustomerChoice($builder);
|
||||
$this->addCustomerChoice($builder, true);
|
||||
$this->addProjectChoice($builder);
|
||||
$this->addActivityChoice($builder);
|
||||
$this->addTagInputField($builder);
|
||||
$this->addExportStateChoice($builder);
|
||||
$builder->add('markAsExported', CheckboxType::class, [
|
||||
'label' => 'label.mark_as_exported',
|
||||
'required' => false,
|
||||
]);
|
||||
$builder->add('create', SubmitType::class, [
|
||||
'label' => 'button.print',
|
||||
'attr' => ['formtarget' => 'invoice'],
|
||||
]);
|
||||
$builder->add('preview', SubmitType::class, [
|
||||
'label' => 'button.preview',
|
||||
]);
|
||||
}
|
||||
|
||||
protected function addTemplateChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('template', InvoiceTemplateType::class, [
|
||||
'required' => false,
|
||||
'required' => true,
|
||||
'placeholder' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
namespace App\Form\Toolbar;
|
||||
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
@@ -37,23 +36,6 @@ class TimesheetToolbarForm extends AbstractToolbarForm
|
||||
$this->addHiddenPagination($builder);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
*/
|
||||
protected function addTimesheetStateChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('state', ChoiceType::class, [
|
||||
'label' => 'label.entryState',
|
||||
'required' => false,
|
||||
'placeholder' => null,
|
||||
'choices' => [
|
||||
'entryState.all' => TimesheetQuery::STATE_ALL,
|
||||
'entryState.running' => TimesheetQuery::STATE_RUNNING,
|
||||
'entryState.stopped' => TimesheetQuery::STATE_STOPPED
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
||||
@@ -123,6 +123,8 @@ trait RendererTrait
|
||||
'activity.id' => $activity->getId(),
|
||||
'activity.name' => $activity->getName(),
|
||||
'activity.comment' => $activity->getComment(),
|
||||
'activity.fixed_rate' => $activity->getFixedRate(),
|
||||
'activity.hourly_rate' => $activity->getHourlyRate(),
|
||||
]);
|
||||
|
||||
foreach ($activity->getVisibleMetaFields() as $metaField) {
|
||||
@@ -138,6 +140,8 @@ trait RendererTrait
|
||||
'project.name' => $project->getName(),
|
||||
'project.comment' => $project->getComment(),
|
||||
'project.order_number' => $project->getOrderNumber(),
|
||||
'project.fixed_rate' => $project->getFixedRate(),
|
||||
'project.hourly_rate' => $project->getHourlyRate(),
|
||||
]);
|
||||
|
||||
foreach ($project->getVisibleMetaFields() as $metaField) {
|
||||
@@ -158,6 +162,8 @@ trait RendererTrait
|
||||
'customer.country' => $customer->getCountry(),
|
||||
'customer.homepage' => $customer->getHomepage(),
|
||||
'customer.comment' => $customer->getComment(),
|
||||
'customer.fixed_rate' => $customer->getFixedRate(),
|
||||
'customer.hourly_rate' => $customer->getHourlyRate(),
|
||||
]);
|
||||
|
||||
foreach ($customer->getVisibleMetaFields() as $metaField) {
|
||||
|
||||
@@ -50,7 +50,7 @@ class BaseQuery
|
||||
/**
|
||||
* @var User
|
||||
*/
|
||||
private $user;
|
||||
private $currentUser;
|
||||
/**
|
||||
* @var Team[]
|
||||
*/
|
||||
@@ -73,7 +73,7 @@ class BaseQuery
|
||||
|
||||
public function getCurrentUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
return $this->currentUser;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,7 +82,7 @@ class BaseQuery
|
||||
*/
|
||||
public function setCurrentUser(User $user)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->currentUser = $user;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -15,23 +15,32 @@ class ExportQuery extends TimesheetQuery
|
||||
* @var string
|
||||
*/
|
||||
private $type;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* @var bool
|
||||
*/
|
||||
private $markAsExported = false;
|
||||
|
||||
public function getType(): ?string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* @return ExportQuery
|
||||
*/
|
||||
public function setType(string $type)
|
||||
public function setType(string $type): ExportQuery
|
||||
{
|
||||
$this->type = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isMarkAsExported(): bool
|
||||
{
|
||||
return $this->markAsExported;
|
||||
}
|
||||
|
||||
public function setMarkAsExported(bool $markAsExported): ExportQuery
|
||||
{
|
||||
$this->markAsExported = $markAsExported;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,32 +11,38 @@ namespace App\Repository\Query;
|
||||
|
||||
use App\Entity\InvoiceTemplate;
|
||||
|
||||
/**
|
||||
* Can be used for invoice queries.
|
||||
*/
|
||||
class InvoiceQuery extends TimesheetQuery
|
||||
{
|
||||
/**
|
||||
* @var InvoiceTemplate
|
||||
*/
|
||||
private $template;
|
||||
|
||||
/**
|
||||
* @return InvoiceTemplate
|
||||
* @var bool
|
||||
*/
|
||||
public function getTemplate()
|
||||
private $markAsExported = false;
|
||||
|
||||
public function getTemplate(): ?InvoiceTemplate
|
||||
{
|
||||
return $this->template;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InvoiceTemplate $template
|
||||
* @return InvoiceQuery
|
||||
*/
|
||||
public function setTemplate($template)
|
||||
public function setTemplate(InvoiceTemplate $template): InvoiceQuery
|
||||
{
|
||||
$this->template = $template;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isMarkAsExported(): bool
|
||||
{
|
||||
return $this->markAsExported;
|
||||
}
|
||||
|
||||
public function setMarkAsExported(bool $markAsExported): InvoiceQuery
|
||||
{
|
||||
$this->markAsExported = $markAsExported;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,10 @@ class TimesheetQuery extends ActivityQuery
|
||||
* @var iterable
|
||||
*/
|
||||
protected $tags = [];
|
||||
/**
|
||||
* @var User[]
|
||||
*/
|
||||
private $users = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -58,6 +62,30 @@ class TimesheetQuery extends ActivityQuery
|
||||
$this->dateRange = new DateRange();
|
||||
}
|
||||
|
||||
public function addUser(User $user): self
|
||||
{
|
||||
$this->users[$user->getId()] = $user;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeUser(User $user): self
|
||||
{
|
||||
if (isset($this->users[$user->getId()])) {
|
||||
unset($this->users[$user->getId()]);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return User[]
|
||||
*/
|
||||
public function getUsers(): array
|
||||
{
|
||||
return array_values($this->users);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return User|null
|
||||
*/
|
||||
|
||||
@@ -538,7 +538,9 @@ class TimesheetRepository extends EntityRepository
|
||||
$user[] = $query->getUser();
|
||||
}
|
||||
|
||||
if (null === $query->getUser() && null !== $query->getCurrentUser()) {
|
||||
$user = array_merge($user, $query->getUsers());
|
||||
|
||||
if (empty($user) && null !== $query->getCurrentUser()) {
|
||||
$currentUser = $query->getCurrentUser();
|
||||
|
||||
if (!$currentUser->isSuperAdmin() && !$currentUser->isAdmin()) {
|
||||
@@ -678,4 +680,25 @@ class TimesheetRepository extends EntityRepository
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet[] $timesheets
|
||||
*/
|
||||
public function setExported(array $timesheets)
|
||||
{
|
||||
$em = $this->getEntityManager();
|
||||
$em->beginTransaction();
|
||||
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb
|
||||
->update(Timesheet::class, 't')
|
||||
->set('t.exported', ':exported')
|
||||
->where($qb->expr()->in('t.id', ':ids'))
|
||||
->setParameter('exported', true, \PDO::PARAM_BOOL)
|
||||
->setParameter('ids', $timesheets)
|
||||
->getQuery()
|
||||
->execute();
|
||||
|
||||
$em->commit();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,68 +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\Voter;
|
||||
|
||||
use App\Entity\InvoiceTemplate;
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
|
||||
/**
|
||||
* A voter to check permissions on InvoiceTemplateVote.
|
||||
*/
|
||||
class InvoiceTemplateVoter extends AbstractVoter
|
||||
{
|
||||
public const VIEW = 'view';
|
||||
public const EDIT = 'edit';
|
||||
public const DELETE = 'delete';
|
||||
|
||||
/**
|
||||
* support rules based on the given $subject (here: InvoiceTemplate)
|
||||
*/
|
||||
public const ALLOWED_ATTRIBUTES = [
|
||||
self::VIEW,
|
||||
self::EDIT,
|
||||
self::DELETE
|
||||
];
|
||||
|
||||
/**
|
||||
* @param string $attribute
|
||||
* @param InvoiceTemplate $subject
|
||||
* @return bool
|
||||
*/
|
||||
protected function supports($attribute, $subject)
|
||||
{
|
||||
if (!($subject instanceof InvoiceTemplate)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $attribute
|
||||
* @param InvoiceTemplate $subject
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
|
||||
{
|
||||
$user = $token->getUser();
|
||||
|
||||
if (!$user instanceof User) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->hasRolePermission($user, $attribute . '_invoice_template');
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,7 @@
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_granted('view_export') %}
|
||||
{% if is_granted('create_export') %}
|
||||
<li class="visible-xs-inline-block">
|
||||
<a href="{{ path('export') }}" class="ddt-large">
|
||||
<i class="{{ 'export'|icon }} fa-2x"></i>
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
{% if form_theme is defined %}
|
||||
{% form_theme form form_theme %}
|
||||
{% endif %}
|
||||
<div class="box box-primary">
|
||||
{{ form_start(form) }}
|
||||
<div class="box-header with-border">
|
||||
|
||||
13
templates/export/actions.html.twig
Normal file
13
templates/export/actions.html.twig
Normal file
@@ -0,0 +1,13 @@
|
||||
|
||||
{% macro export(view) %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
|
||||
{% set actions = {} %}
|
||||
{% if view == 'preview' %}
|
||||
{% set actions = {'off': {'id':'export-toggle-button'}} %}
|
||||
{% endif %}
|
||||
{% set actions = actions|merge({'help': {'url': 'export.html'|docu_link, 'target': '_blank'}}) %}
|
||||
|
||||
{% set event = trigger('actions.export', {'actions': actions, 'view': view}) %}
|
||||
{{ widgets.page_actions(event.payload.actions) }}
|
||||
{% endmacro %}
|
||||
@@ -2,7 +2,7 @@
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% import "macros/toolbar.html.twig" as toolbar %}
|
||||
{% import "macros/datatables.html.twig" as tables %}
|
||||
{% import "macros/actions.html.twig" as actions %}
|
||||
{% import "export/actions.html.twig" as actions %}
|
||||
|
||||
{% set columns = {
|
||||
'date': 'alwaysVisible',
|
||||
@@ -21,89 +21,96 @@
|
||||
|
||||
{% block page_title %}{{ 'export.title'|trans }}{% endblock %}
|
||||
{% block page_subtitle %}{{ 'export.subtitle'|trans }}{% endblock %}
|
||||
{% block page_actions %}{{ actions.export('index') }}{% endblock %}
|
||||
|
||||
{% block main_before %}
|
||||
{{ toolbar.toolbar(form, 'collapseExport', true) }}
|
||||
{{ tables.data_table_column_modal(tableName, columns) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block main_after %}
|
||||
<div class="clearfix"></div>
|
||||
{% endblock %}
|
||||
{% block page_actions %}{{ actions.export((preview_show ? 'preview' : 'index')) }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
|
||||
{% if entries is empty %}
|
||||
{{ widgets.callout('warning', 'error.no_entries_found') }}
|
||||
{% else %}
|
||||
{{ tables.data_table_header(tableName, columns) }}
|
||||
{% for entry in entries %}
|
||||
{% set currency = entry.project.customer.currency %}
|
||||
{% set duration = entry.duration|duration %}
|
||||
{% if entry.fixedRate is not null %}
|
||||
{% set rate = entry.fixedRate %}
|
||||
{% set duration = 1 %}
|
||||
{% elseif entry.hourlyRate is not null %}
|
||||
{% set rate = entry.hourlyRate %}
|
||||
{% else %}
|
||||
{% set rate = entry.user.preferenceValue('hourly_rate') %}
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td class="text-nowrap">{{ entry.begin|date_short }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'user') }}">{{ widgets.label_user(entry.user) }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'customer') }}">{{ widgets.label_customer(entry.project.customer) }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'project') }}">{{ widgets.label_project(entry.project) }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'activity') }}">{{ widgets.label_activity(entry.activity) }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'description') }} timesheet-description">
|
||||
{{ entry.description }}
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'unit_price') }} text-nowrap">
|
||||
{{ rate|money(currency) }}
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'duration') }} text-nowrap">
|
||||
{{ duration }}
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'total_rate') }} text-nowrap">
|
||||
{{ entry.rate|money(currency) }}
|
||||
</td>
|
||||
<td>
|
||||
{% if is_granted('edit_export', entry) %}
|
||||
{% if entry.exported %}
|
||||
<button type="button" class="btn btn-default exportBtn active" data-toggle="button" aria-pressed="true" autocomplete="off"
|
||||
data-exported-text="{{ 'entryState.exported'|trans }}" data-clean-text="{{ 'entryState.not_exported'|trans }}" data-timesheet="{{ entry.id }}">
|
||||
{{ 'entryState.exported'|trans }}
|
||||
</button>
|
||||
{% else %}
|
||||
<button type="button" class="btn btn-default exportBtn" data-toggle="button" aria-pressed="false" autocomplete="off"
|
||||
data-exported-text="{{ 'entryState.exported'|trans }}" data-clean-text="{{ 'entryState.not_exported'|trans }}" data-timesheet="{{ entry.id }}">
|
||||
{{ 'entryState.not_exported'|trans }}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if entry.exported %}
|
||||
{{ 'entryState.exported'|trans }}
|
||||
{% else %}
|
||||
{{ 'entryState.not_exported'|trans }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{{ tables.data_table_footer(entries) }}
|
||||
|
||||
{% if is_granted('create_export') %}
|
||||
<div class="row no-print">
|
||||
<div class="col-xs-12">
|
||||
<div class="btn-group pull-right" id="export-buttons" role="group">
|
||||
{% for button in renderer %}
|
||||
<button type="button" id="export-{{ button.id }}-button" class="btn btn-success startExportBtn" data-type="{{ button.id }}">
|
||||
<i class="{{ button.icon|icon }}"></i> {{ ('button.' ~ button.title)|trans }}
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
|
||||
{% form_theme form '@AdminLTE/layout/form-theme-horizontal.html.twig' %}
|
||||
{% block box_title %}{{ 'export.filter'|trans }}{% endblock %}
|
||||
{% block box_before %}{{ form_start(form) }}{% endblock %}
|
||||
{% block box_body %}
|
||||
{{ form_row(form.daterange) }}
|
||||
{{ form_row(form.customer) }}
|
||||
{{ form_row(form.project) }}
|
||||
{{ form_row(form.activity) }}
|
||||
{{ form_row(form.users) }}
|
||||
{{ form_row(form.tags) }}
|
||||
{{ form_row(form.exported) }}
|
||||
{{ form_row(form.state) }}
|
||||
{{ form_row(form.markAsExported) }}
|
||||
{% endblock %}
|
||||
{% block box_footer%}
|
||||
<div class="btn-group" id="export-buttons" role="group">
|
||||
{% for button in renderer %}
|
||||
<button type="button" id="export-{{ button.id }}-button" class="btn btn-success startExportBtn" data-type="{{ button.id }}">
|
||||
<i class="{{ button.icon|icon }}"></i> {{ ('button.' ~ button.title)|trans }}
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{{ form_widget(form.preview, {'attr': {'data-toggle': 'tooltip', 'title': 'limited_entries'|trans({'%max%': preview_max})}}) }}
|
||||
{% endblock %}
|
||||
{% block box_after %}{{ form_end(form) }}{% endblock %}
|
||||
{% endembed %}
|
||||
|
||||
{% if preview_show %}
|
||||
{% if entries is empty %}
|
||||
{{ widgets.callout('warning', 'error.no_entries_found') }}
|
||||
{% else %}
|
||||
{{ tables.data_table_header(tableName, columns) }}
|
||||
{% for entry in entries %}
|
||||
{% set currency = entry.project.customer.currency %}
|
||||
{% set duration = entry.duration|duration %}
|
||||
{% if entry.fixedRate is not null %}
|
||||
{% set rate = entry.fixedRate %}
|
||||
{% set duration = 1 %}
|
||||
{% elseif entry.hourlyRate is not null %}
|
||||
{% set rate = entry.hourlyRate %}
|
||||
{% else %}
|
||||
{% set rate = entry.user.preferenceValue('hourly_rate') %}
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td class="text-nowrap">{{ entry.begin|date_short }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'user') }}">{{ widgets.label_user(entry.user) }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'customer') }}">{{ widgets.label_customer(entry.project.customer) }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'project') }}">{{ widgets.label_project(entry.project) }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'activity') }}">{{ widgets.label_activity(entry.activity) }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'description') }} timesheet-description">
|
||||
{{ entry.description }}
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'unit_price') }} text-nowrap">
|
||||
{{ rate|money(currency) }}
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'duration') }} text-nowrap">
|
||||
{{ duration }}
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'total_rate') }} text-nowrap">
|
||||
{{ entry.rate|money(currency) }}
|
||||
</td>
|
||||
<td>
|
||||
{% if is_granted('edit_export', entry) %}
|
||||
{% if entry.exported %}
|
||||
<button type="button" class="btn btn-default exportBtn active" data-toggle="button" aria-pressed="true" autocomplete="off"
|
||||
data-exported-text="{{ 'entryState.exported'|trans }}" data-clean-text="{{ 'entryState.not_exported'|trans }}" data-timesheet="{{ entry.id }}">
|
||||
{{ 'entryState.exported'|trans }}
|
||||
</button>
|
||||
{% else %}
|
||||
<button type="button" class="btn btn-default exportBtn" data-toggle="button" aria-pressed="false" autocomplete="off"
|
||||
data-exported-text="{{ 'entryState.exported'|trans }}" data-clean-text="{{ 'entryState.not_exported'|trans }}" data-timesheet="{{ entry.id }}">
|
||||
{{ 'entryState.not_exported'|trans }}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if entry.exported %}
|
||||
{{ 'entryState.exported'|trans }}
|
||||
{% else %}
|
||||
{{ 'entryState.not_exported'|trans }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{{ tables.data_table_footer(entries) }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
@@ -260,8 +260,10 @@
|
||||
<tr>
|
||||
<td class="column-date text-nowrap" {% if not columns.date %}style="display: none"{% endif %}>
|
||||
{{ entry.begin|date_time }}
|
||||
<br>
|
||||
{{ entry.end|date_time }}
|
||||
{% if entry.end %}
|
||||
<br>
|
||||
{{ entry.end|date_time }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="column-username" {% if not columns.username %}style="display: none"{% endif %}>
|
||||
{{ widgets.username(entry.user) }}
|
||||
|
||||
@@ -144,8 +144,10 @@ mpdf-->
|
||||
<tr class="{{ cycle(['odd', 'even'], loop.index0) }}">
|
||||
<td>
|
||||
{{ entry.begin|date_time }}
|
||||
<br>
|
||||
{{ entry.end|date_time }}
|
||||
{% if entry.end %}
|
||||
<br>
|
||||
{{ entry.end|date_time }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ entry.project.customer.name }} - {{ entry.project.name }} - {{ entry.activity.name }}
|
||||
|
||||
47
templates/invoice/actions.html.twig
Normal file
47
templates/invoice/actions.html.twig
Normal file
@@ -0,0 +1,47 @@
|
||||
|
||||
{% macro invoices(view) %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
|
||||
{% set actions = {} %}
|
||||
{% if is_granted('manage_invoice_template') %}
|
||||
{% set actions = actions|merge({'list': path('admin_invoice_template')}) %}
|
||||
{% endif %}
|
||||
|
||||
{% set actions = actions|merge({'help': {'url': 'invoices.html'|docu_link, 'target': '_blank'}}) %}
|
||||
|
||||
{% set event = trigger('actions.invoices', {'actions': actions, 'view': view}) %}
|
||||
{{ widgets.page_actions(event.payload.actions) }}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro invoice_templates(view) %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
|
||||
{% set actions = {} %}
|
||||
{% if is_granted('view_invoice') %}
|
||||
{% set actions = actions|merge({'back': path('invoice')}) %}
|
||||
{% endif %}
|
||||
|
||||
{% if is_granted('manage_invoice_template') %}
|
||||
{% set actions = actions|merge({'create': path('admin_invoice_template_create')}) %}
|
||||
{% endif %}
|
||||
|
||||
{% set actions = actions|merge({'help': {'url': 'invoices.html'|docu_link, 'target': '_blank'}}) %}
|
||||
|
||||
{% set event = trigger('actions.invoice_templates', {'actions': actions, 'view': 'index'}) %}
|
||||
{{ widgets.page_actions(actions) }}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro invoice_template(template, view) %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
|
||||
{% set actions = {} %}
|
||||
|
||||
{% if is_granted('manage_invoice_template') %}
|
||||
{% set actions = actions|merge({'edit': {'url': path('admin_invoice_template_edit', {'id' : template.id}), 'class': 'modal-ajax-form'}}) %}
|
||||
{% set actions = actions|merge({'copy': path('admin_invoice_template_copy', {'id' : template.id})}) %}
|
||||
{% set actions = actions|merge({'trash': path('admin_invoice_template_delete', {'id' : template.id})}) %}
|
||||
{% endif %}
|
||||
|
||||
{% set event = trigger('actions.invoice_template', {'actions': actions, 'view': view, 'template': template}) %}
|
||||
{{ widgets.table_actions(event.payload.actions) }}
|
||||
{% endmacro %}
|
||||
@@ -2,11 +2,11 @@
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% import "macros/toolbar.html.twig" as toolbar %}
|
||||
{% import "macros/datatables.html.twig" as tables %}
|
||||
{% import "macros/actions.html.twig" as actions %}
|
||||
{% import "invoice/actions.html.twig" as actions %}
|
||||
|
||||
{% set columns = {
|
||||
'date': 'alwaysVisible',
|
||||
'user': 'hidden-sm',
|
||||
'user': 'hidden-xs hidden-sm',
|
||||
'description': 'hidden-xs hidden-sm',
|
||||
'unit_price': 'hidden-xs text-center',
|
||||
'amount': 'text-center',
|
||||
@@ -19,63 +19,66 @@
|
||||
{% block page_subtitle %}{{ 'invoice.subtitle'|trans }}{% endblock %}
|
||||
{% block page_actions %}{{ actions.invoices('index') }}{% endblock %}
|
||||
|
||||
{% block main_before %}
|
||||
{{ toolbar.toolbar(form, 'collapseInvoice', true) }}
|
||||
{{ tables.data_table_column_modal(tableName, columns) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block main_after %}
|
||||
<div class="clearfix"></div>
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
|
||||
{% set entries = {} %}
|
||||
{% if model.calculator is not empty and model.calculator.entries is not empty %}
|
||||
{% set entries = model.calculator.entries %}
|
||||
{% if is_granted('create_invoice') %}
|
||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
|
||||
{% form_theme form '@AdminLTE/layout/form-theme-horizontal.html.twig' %}
|
||||
{% block box_title %}{{ 'invoice.filter'|trans }}{% endblock %}
|
||||
{% block box_before %}{{ form_start(form) }}{% endblock %}
|
||||
{% block box_body %}
|
||||
{{ form_row(form.daterange) }}
|
||||
{{ form_row(form.customer) }}
|
||||
{{ form_row(form.project) }}
|
||||
{{ form_row(form.activity) }}
|
||||
{{ form_row(form.users) }}
|
||||
{{ form_row(form.tags) }}
|
||||
{{ form_row(form.exported) }}
|
||||
{{ form_row(form.template) }}
|
||||
{{ form_row(form.markAsExported) }}
|
||||
{% endblock %}
|
||||
{% block box_footer%}
|
||||
{{ form_widget(form.create, {'attr': {'class': 'btn btn-success'}}) }}
|
||||
{{ form_widget(form.preview, {'attr': {'data-toggle': 'tooltip', 'title': 'limited_entries'|trans({'%max%': preview_max})}}) }}
|
||||
{% endblock %}
|
||||
{% block box_after %}{{ form_end(form) }}{% endblock %}
|
||||
{% endembed %}
|
||||
{% else %}
|
||||
{{ widgets.callout('danger', 'http_error_403.suggestion'|trans({}, 'exceptions')) }}
|
||||
{% endif %}
|
||||
|
||||
{% if entries is empty %}
|
||||
{{ widgets.callout('warning', 'error.no_entries_found') }}
|
||||
{% else %}
|
||||
{{ widgets.callout('success', 'invoice.preview') }}
|
||||
|
||||
{{ tables.data_table_header(tableName, columns) }}
|
||||
{% for entry in entries %}
|
||||
{% set duration = entry.duration|duration %}
|
||||
{% if entry.fixedRate is not null %}
|
||||
{% set rate = entry.fixedRate %}
|
||||
{% set duration = 1 %}{# FIXME fixed rates #}
|
||||
{% elseif entry.hourlyRate is not null %}
|
||||
{% set rate = entry.hourlyRate %}
|
||||
{% else %}
|
||||
{% set rate = entry.user.preferenceValue('hourly_rate') %}
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td class="text-nowrap">{{ entry.begin|date_short }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'user') }}">{{ widgets.label_user(entry.user) }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'description') }} timesheet-description">
|
||||
{% if entry.description is not empty %}
|
||||
{{ entry.description|escape|desc2html }}
|
||||
{% else %}
|
||||
{{ entry.activity.name }} / {{ entry.project.name }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'unit_price') }} text-center">{{ rate|money(model.calculator.currency) }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'amount') }} text-center text-nowrap">{{ duration }}</td>
|
||||
<td class="text-right text-nowrap">{{ entry.rate|money(model.calculator.currency) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{{ tables.data_table_footer(entries) }}
|
||||
|
||||
{% if is_granted('create_invoice') %}
|
||||
<div class="row no-print">
|
||||
<div class="col-xs-12">
|
||||
<a href="{{ path('invoice_print') }}" class="btn btn-success pull-right toolbar-action" data-method="POST">
|
||||
<i class="{{ 'print'|icon }}"></i> {{ 'button.print'|trans }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% if preview_show %}
|
||||
{% if model.calculator is empty or model.calculator.entries is empty %}
|
||||
{{ widgets.callout('warning', 'error.no_entries_found') }}
|
||||
{% else %}
|
||||
{% set entries = model.calculator.entries %}
|
||||
{{ tables.data_table_header(tableName, columns) }}
|
||||
{% for entry in entries %}
|
||||
{% set duration = entry.duration|duration %}
|
||||
{% if entry.fixedRate is not null %}
|
||||
{% set rate = entry.fixedRate %}
|
||||
{% set duration = 1 %}{# FIXME fixed rates #}
|
||||
{% elseif entry.hourlyRate is not null %}
|
||||
{% set rate = entry.hourlyRate %}
|
||||
{% else %}
|
||||
{% set rate = entry.user.preferenceValue('hourly_rate') %}
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td class="text-nowrap">{{ entry.begin|date_short }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'user') }}">{{ widgets.label_user(entry.user) }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'description') }} timesheet-description">
|
||||
{% if entry.description is not empty %}
|
||||
{{ entry.description|escape|desc2html }}
|
||||
{% else %}
|
||||
{{ entry.activity.name }} / {{ entry.project.name }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'unit_price') }} text-center">{{ rate|money(model.calculator.currency) }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'amount') }} text-center text-nowrap">{{ duration }}</td>
|
||||
<td class="text-right text-nowrap">{{ entry.rate|money(model.calculator.currency) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{{ tables.data_table_footer(entries) }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% import "macros/datatables.html.twig" as tables %}
|
||||
{% import "macros/actions.html.twig" as actions %}
|
||||
{% import "invoice/actions.html.twig" as actions %}
|
||||
|
||||
{% block page_title %}{{ 'admin_invoice_template.title'|trans }}{% endblock %}
|
||||
{% block page_subtitle %}{{ 'admin_invoice_template.subtitle'|trans }}{% endblock %}
|
||||
@@ -25,28 +25,16 @@
|
||||
{{ tables.data_table_header(tableName, columns) }}
|
||||
|
||||
{% for entry in entries %}
|
||||
<tr{% if is_granted('edit', entry) %} class="modal-ajax-form open-edit" data-href="{{ path('admin_invoice_template_edit', {'id' : entry.id, 'page': page}) }}"{% endif %}>
|
||||
<tr class="modal-ajax-form open-edit" data-href="{{ path('admin_invoice_template_edit', {'id' : entry.id}) }}">
|
||||
<td>{{ entry.name }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'title') }}">{{ entry.title }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'due_days') }}">{{ entry.dueDays }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'vat') }}">{{ entry.vat }}</td>
|
||||
<td class="actions">
|
||||
{# TODO move me to actions macro #}
|
||||
{% set actions = {} %}
|
||||
{% if is_granted('edit', entry) %}
|
||||
{% set actions = {'edit': {'url': path('admin_invoice_template_edit', {'id' : entry.id, 'page': page}), 'class': 'modal-ajax-form'}} %}
|
||||
{% endif %}
|
||||
{% if is_granted('create_invoice_template') %}
|
||||
{% set actions = actions|merge({'copy': path('admin_invoice_template_copy', {'id' : entry.id, 'page': page})}) %}
|
||||
{% endif %}
|
||||
{% if is_granted('delete', entry) %}
|
||||
{% set actions = actions|merge({'trash': path('admin_invoice_template_delete', {'id' : entry.id, 'page': page})}) %}
|
||||
{% endif %}
|
||||
{% set event = trigger('actions.invoice_templates', {'actions': actions, 'view': 'index', 'template': entry}) %}
|
||||
{{ widgets.table_actions(actions) }}
|
||||
{{- actions.invoice_template(entry, 'index') -}}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
{{ tables.data_table_footer(entries, 'admin_invoice_template_paginated') }}
|
||||
{{ tables.data_table_footer(entries, 'admin_invoice_template') }}
|
||||
{% endblock %}
|
||||
|
||||
@@ -439,51 +439,6 @@
|
||||
{{ widgets.page_actions(event.payload.actions) }}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro export(view) %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
|
||||
{% set actions = {'filter': '#collapseExport', 'visibility': '#modal_export', 'off': {'id':'export-toggle-button'}} %}
|
||||
{% set actions = actions|merge({'help': {'url': 'export.html'|docu_link, 'target': '_blank'}}) %}
|
||||
|
||||
{% set event = trigger('actions.export', {'actions': actions, 'view': view}) %}
|
||||
{{ widgets.page_actions(event.payload.actions) }}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro invoices(view) %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
|
||||
{% set actions = {'filter': '#collapseInvoice', 'visibility': '#modal_invoice'} %}
|
||||
{% if is_granted('create_invoice_template') %}
|
||||
{% set actions = actions|merge({'create': path('admin_invoice_template_create')}) %}
|
||||
{% endif %}
|
||||
{% if is_granted('view_invoice_template') %}
|
||||
{% set actions = actions|merge({'list': path('admin_invoice_template')}) %}
|
||||
{% endif %}
|
||||
|
||||
{% set actions = actions|merge({'help': {'url': 'invoices.html'|docu_link, 'target': '_blank'}}) %}
|
||||
|
||||
{% set event = trigger('actions.invoices', {'actions': actions, 'view': view}) %}
|
||||
{{ widgets.page_actions(event.payload.actions) }}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro invoice_templates(view) %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
|
||||
{% set actions = {} %}
|
||||
{% if is_granted('view_invoice') %}
|
||||
{% set actions = actions|merge({'back': path('invoice')}) %}
|
||||
{% endif %}
|
||||
|
||||
{% if is_granted('create_invoice_template') %}
|
||||
{% set actions = actions|merge({'create': path('admin_invoice_template_create')}) %}
|
||||
{% endif %}
|
||||
|
||||
{% set actions = actions|merge({'help': {'url': 'invoices.html'|docu_link, 'target': '_blank'}}) %}
|
||||
|
||||
{% set event = trigger('actions.invoice_templates', {'actions': actions, 'view': 'index'}) %}
|
||||
{{ widgets.page_actions(actions) }}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro teams(view) %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
|
||||
|
||||
@@ -9,8 +9,10 @@
|
||||
|
||||
namespace App\Tests\Controller;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Tests\DataFixtures\TimesheetFixtures;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
|
||||
/**
|
||||
* @group integration
|
||||
@@ -27,7 +29,7 @@ class ExportControllerTest extends ControllerBaseTest
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||
|
||||
$this->request($client, '/export/');
|
||||
$this->request($client, '/export/?preview=');
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$this->assertHasNoEntriesWithFilter($client);
|
||||
@@ -47,7 +49,7 @@ class ExportControllerTest extends ControllerBaseTest
|
||||
;
|
||||
$this->importFixture($em, $fixture);
|
||||
|
||||
$this->request($client, '/export/');
|
||||
$this->request($client, '/export/?preview=');
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
// make sure all existing records are displayed
|
||||
@@ -55,7 +57,7 @@ class ExportControllerTest extends ControllerBaseTest
|
||||
$this->assertDataTableRowCount($client, 'datatable_export', 20);
|
||||
|
||||
// assert export type buttons are available
|
||||
$expected = ['csv', 'html', 'pdf', 'ods', 'xlsx'];
|
||||
$expected = ['csv', 'html', 'pdf', 'xlsx'];
|
||||
$node = $client->getCrawler()->filter('#export-buttons button');
|
||||
$this->assertEquals(count($expected), $node->count());
|
||||
foreach ($node->getIterator() as $button) {
|
||||
@@ -100,6 +102,7 @@ class ExportControllerTest extends ControllerBaseTest
|
||||
public function testExportAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||
/** @var EntityManager $em */
|
||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||
|
||||
$begin = new \DateTime('first day of this month');
|
||||
@@ -121,7 +124,8 @@ class ExportControllerTest extends ControllerBaseTest
|
||||
|
||||
// don't add daterange to make sure the current month is the default range
|
||||
$client->submit($form, [
|
||||
'type' => 'html'
|
||||
'type' => 'html',
|
||||
'markAsExported' => 1
|
||||
]);
|
||||
|
||||
$response = $client->getResponse();
|
||||
@@ -137,5 +141,11 @@ class ExportControllerTest extends ControllerBaseTest
|
||||
$node = $client->getCrawler()->filter('section.export div#export-records table.dataTable tbody tr');
|
||||
// 20 rows + the summary footer
|
||||
$this->assertEquals(21, $node->count());
|
||||
|
||||
$timesheets = $em->getRepository(Timesheet::class)->findAll();
|
||||
/** @var Timesheet $timesheet */
|
||||
foreach ($timesheets as $timesheet) {
|
||||
$this->assertTrue($timesheet->isExported());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,12 @@
|
||||
namespace App\Tests\Controller;
|
||||
|
||||
use App\Entity\InvoiceTemplate;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Form\Type\DateRangeType;
|
||||
use App\Tests\DataFixtures\InvoiceFixtures;
|
||||
use App\Tests\DataFixtures\TimesheetFixtures;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
|
||||
/**
|
||||
* @group integration
|
||||
@@ -28,7 +30,7 @@ class InvoiceControllerTest extends ControllerBaseTest
|
||||
|
||||
public function testIndexActionRedirectsToCreateTemplate()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
|
||||
$this->request($client, '/invoice/');
|
||||
$this->assertIsRedirect($client, '/invoice/template/create');
|
||||
@@ -42,7 +44,7 @@ class InvoiceControllerTest extends ControllerBaseTest
|
||||
$fixture = new InvoiceFixtures();
|
||||
$this->importFixture($em, $fixture);
|
||||
|
||||
$this->request($client, '/invoice/');
|
||||
$this->request($client, '/invoice/?preview=');
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$this->assertHasNoEntriesWithFilter($client);
|
||||
@@ -50,7 +52,7 @@ class InvoiceControllerTest extends ControllerBaseTest
|
||||
|
||||
public function testListTemplateAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
|
||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||
$fixture = new InvoiceFixtures();
|
||||
@@ -64,7 +66,7 @@ class InvoiceControllerTest extends ControllerBaseTest
|
||||
|
||||
public function testCreateTemplateAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$this->request($client, '/invoice/template/create');
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
@@ -88,7 +90,7 @@ class InvoiceControllerTest extends ControllerBaseTest
|
||||
|
||||
public function testCopyTemplateAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||
|
||||
$fixture = new InvoiceFixtures();
|
||||
@@ -117,6 +119,7 @@ class InvoiceControllerTest extends ControllerBaseTest
|
||||
public function testPrintAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||
/** @var EntityManager $em */
|
||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||
|
||||
$fixture = new InvoiceFixtures();
|
||||
@@ -138,66 +141,50 @@ class InvoiceControllerTest extends ControllerBaseTest
|
||||
$dateRange = $begin->format('Y-m-d') . DateRangeType::DATE_SPACER . $end->format('Y-m-d');
|
||||
|
||||
$form = $client->getCrawler()->filter('#invoice-print-form')->form();
|
||||
$node = $form->getFormNode();
|
||||
$node->setAttribute('action', $this->createUrl('/invoice/?preview='));
|
||||
$node->setAttribute('method', 'GET');
|
||||
$client->submit($form, [
|
||||
'template' => 1,
|
||||
'user' => '',
|
||||
'daterange' => $dateRange,
|
||||
'customer' => 1,
|
||||
]);
|
||||
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
// no datatable should be displayed
|
||||
// no warning should be displayed
|
||||
$node = $client->getCrawler()->filter('div.callout.callout-warning.lead');
|
||||
$this->assertEquals(0, $node->count());
|
||||
|
||||
$node = $client->getCrawler()->filter('div.callout.callout-success.lead');
|
||||
$this->assertNotEmpty($node->text());
|
||||
$this->assertContains('This is a preview of the data that will show up in your invoice document.', $node->text());
|
||||
|
||||
// but the datatable with all timesheets
|
||||
$this->assertDataTableRowCount($client, 'datatable_invoice', 20);
|
||||
|
||||
$form = $client->getCrawler()->filter('#invoice-print-form')->form();
|
||||
$node = $form->getFormNode();
|
||||
$node->setAttribute('action', $this->createUrl('/invoice/print'));
|
||||
$node->setAttribute('method', 'POST');
|
||||
$node->setAttribute('action', $this->createUrl('/invoice/?create='));
|
||||
$node->setAttribute('method', 'GET');
|
||||
$client->submit($form, [
|
||||
'template' => 1,
|
||||
'user' => '',
|
||||
'daterange' => $dateRange,
|
||||
'customer' => 1,
|
||||
'project' => 1,
|
||||
'markAsExported' => 1,
|
||||
]);
|
||||
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
$node = $client->getCrawler()->filter('body');
|
||||
$this->assertEquals(1, $node->count());
|
||||
$this->assertEquals('invoice_print', $node->getIterator()[0]->getAttribute('class'));
|
||||
}
|
||||
|
||||
public function testPrintActionRedirectsToCreateTemplate()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||
|
||||
$this->request($client, '/invoice/print', 'POST');
|
||||
$this->assertIsRedirect($client, '/invoice/template/create');
|
||||
}
|
||||
|
||||
public function testPrintActionRedirectsToIndex()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||
|
||||
$fixture = new InvoiceFixtures();
|
||||
$this->importFixture($em, $fixture);
|
||||
|
||||
$this->request($client, '/invoice/print', 'POST');
|
||||
$this->assertIsRedirect($client, '/invoice/');
|
||||
$timesheets = $em->getRepository(Timesheet::class)->findAll();
|
||||
/** @var Timesheet $timesheet */
|
||||
foreach ($timesheets as $timesheet) {
|
||||
$this->assertTrue($timesheet->isExported());
|
||||
}
|
||||
}
|
||||
|
||||
public function testEditTemplateAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
|
||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||
$fixture = new InvoiceFixtures();
|
||||
@@ -231,8 +218,8 @@ class InvoiceControllerTest extends ControllerBaseTest
|
||||
$fixture = new InvoiceFixtures();
|
||||
$this->importFixture($em, $fixture);
|
||||
|
||||
$this->request($client, '/invoice/template/1/delete?page=1');
|
||||
$this->assertIsRedirect($client, '/invoice/template/page/1');
|
||||
$this->request($client, '/invoice/template/1/delete');
|
||||
$this->assertIsRedirect($client, '/invoice/template');
|
||||
$client->followRedirect();
|
||||
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
@@ -210,6 +210,6 @@ class UserControllerTest extends ControllerBaseTest
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
|
||||
$this->assertAccessIsGranted($client, '/admin/user/permissions');
|
||||
$this->assertHasDataTable($client);
|
||||
$this->assertDataTableRowCount($client, 'datatable_user_admin_permissions', 84);
|
||||
$this->assertDataTableRowCount($client, 'datatable_user_admin_permissions', 80);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ use App\Entity\User;
|
||||
use App\Export\RendererInterface;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Twig\DateExtensions;
|
||||
use App\Twig\Extensions;
|
||||
use App\Utils\LocaleSettings;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
@@ -52,9 +51,8 @@ abstract class AbstractRendererTest extends KernelTestCase
|
||||
|
||||
$translator = $this->getMockBuilder(TranslatorInterface::class)->getMock();
|
||||
$dateExtension = new DateExtensions($localeSettings);
|
||||
$extensions = new Extensions($localeSettings);
|
||||
|
||||
return new $classname($translator, $dateExtension, $extensions);
|
||||
return new $classname($translator, $dateExtension);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -32,7 +32,7 @@ class CsvRendererTest extends AbstractRendererTest
|
||||
public function getTestModel()
|
||||
{
|
||||
return [
|
||||
['01:50 h', '€2,437.12', '€1,947.99', 7, 5, 1, 2, 2]
|
||||
['400', '2437.12', ' EUR 1,947.99 ', 7, 5, 1, 2, 2]
|
||||
];
|
||||
}
|
||||
|
||||
@@ -76,26 +76,27 @@ class CsvRendererTest extends AbstractRendererTest
|
||||
}
|
||||
|
||||
$expected = [
|
||||
0 => '2019.06.16 12:00',
|
||||
1 => '2019.06.16 12:06',
|
||||
2 => 'kevin',
|
||||
3 => 'Customer Name',
|
||||
4 => 'project name',
|
||||
5 => 'activity description',
|
||||
6 => '',
|
||||
7 => '',
|
||||
8 => 'foo,bar',
|
||||
9 => 'meta-bar',
|
||||
10 => 'meta-bar2',
|
||||
11 => '€0.00',
|
||||
12 => '€84.00',
|
||||
13 => '00:06 h',
|
||||
14 => '€0.00',
|
||||
0 => '2019-06-16',
|
||||
1 => '12:00',
|
||||
2 => '12:06',
|
||||
3 => '400',
|
||||
4 => '0',
|
||||
5 => 'kevin',
|
||||
6 => 'Customer Name',
|
||||
7 => 'project name',
|
||||
8 => 'activity description',
|
||||
9 => '',
|
||||
10 => '',
|
||||
11 => 'foo,bar',
|
||||
12 => '',
|
||||
13 => ' EUR 84.00 ',
|
||||
14 => 'meta-bar',
|
||||
15 => 'meta-bar2',
|
||||
];
|
||||
|
||||
self::assertEquals(7, count($all));
|
||||
self::assertEquals(count($expected), count($all[0]));
|
||||
self::assertEquals('foo', $all[4][8]);
|
||||
self::assertEquals('foo', $all[4][11]);
|
||||
|
||||
self::assertEquals($expected, $all[5]);
|
||||
}
|
||||
|
||||
@@ -1,52 +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\Tests\Export\Renderer;
|
||||
|
||||
use App\Export\Renderer\OdsRenderer;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
|
||||
/**
|
||||
* @covers \App\Export\Renderer\OdsRenderer
|
||||
* @covers \App\Export\Renderer\AbstractSpreadsheetRenderer
|
||||
* @covers \App\Export\Renderer\RendererTrait
|
||||
* @group integration
|
||||
*/
|
||||
class OdsRendererTest extends AbstractRendererTest
|
||||
{
|
||||
public function testConfiguration()
|
||||
{
|
||||
$sut = $this->getAbstractRenderer(OdsRenderer::class);
|
||||
|
||||
$this->assertEquals('ods', $sut->getId());
|
||||
$this->assertEquals('ods', $sut->getTitle());
|
||||
$this->assertEquals('ods', $sut->getIcon());
|
||||
}
|
||||
|
||||
public function testRender()
|
||||
{
|
||||
$sut = $this->getAbstractRenderer(OdsRenderer::class);
|
||||
|
||||
/** @var BinaryFileResponse $response */
|
||||
$response = $this->render($sut);
|
||||
|
||||
$file = $response->getFile();
|
||||
$this->assertEquals('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', $response->headers->get('Content-Type'));
|
||||
$this->assertEquals('attachment; filename=kimai-export.ods', $response->headers->get('Content-Disposition'));
|
||||
|
||||
$this->assertTrue(file_exists($file->getRealPath()));
|
||||
|
||||
ob_start();
|
||||
$response->sendContent();
|
||||
$content2 = ob_get_clean();
|
||||
$this->assertNotEmpty($content2);
|
||||
|
||||
$this->assertFalse(file_exists($file->getRealPath()));
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ class DebugRendererTest extends TestCase
|
||||
// TODO check values or formats?
|
||||
}
|
||||
|
||||
protected function assertModelStructure(array $model, $hasProject = true, $hasActivity = false)
|
||||
protected function assertModelStructure(array $model, $hasProject = true)
|
||||
{
|
||||
$keys = [
|
||||
'invoice.due_date',
|
||||
@@ -85,10 +85,14 @@ class DebugRendererTest extends TestCase
|
||||
'customer.number',
|
||||
'customer.homepage',
|
||||
'customer.comment',
|
||||
'customer.fixed_rate',
|
||||
'customer.hourly_rate',
|
||||
'customer.meta.foo-customer',
|
||||
'activity.id',
|
||||
'activity.name',
|
||||
'activity.comment',
|
||||
'activity.fixed_rate',
|
||||
'activity.hourly_rate',
|
||||
'activity.meta.foo-activity',
|
||||
];
|
||||
|
||||
@@ -98,18 +102,12 @@ class DebugRendererTest extends TestCase
|
||||
'project.name',
|
||||
'project.comment',
|
||||
'project.order_number',
|
||||
'project.fixed_rate',
|
||||
'project.hourly_rate',
|
||||
'project.meta.foo-project',
|
||||
]);
|
||||
}
|
||||
|
||||
if ($hasActivity) {
|
||||
$keys = array_merge($keys, [
|
||||
'activity.id',
|
||||
'activity.name',
|
||||
'activity.comment',
|
||||
]);
|
||||
}
|
||||
|
||||
$givenKeys = array_keys($model);
|
||||
sort($keys);
|
||||
sort($givenKeys);
|
||||
|
||||
@@ -37,6 +37,15 @@ class ExportQueryTest extends BaseQueryTest
|
||||
$this->assertState($sut);
|
||||
$this->assertExported($sut);
|
||||
$this->assertType($sut);
|
||||
$this->assertMarkAsExported($sut);
|
||||
}
|
||||
|
||||
protected function assertMarkAsExported(ExportQuery $sut)
|
||||
{
|
||||
$this->assertFalse($sut->isMarkAsExported());
|
||||
|
||||
$sut->setMarkAsExported(true);
|
||||
$this->assertTrue($sut->isMarkAsExported());
|
||||
}
|
||||
|
||||
protected function assertUser(ExportQuery $sut)
|
||||
|
||||
123
tests/Repository/Query/InvoiceQueryTest.php
Normal file
123
tests/Repository/Query/InvoiceQueryTest.php
Normal file
@@ -0,0 +1,123 @@
|
||||
<?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\Repository\Query;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\User;
|
||||
use App\Repository\Query\InvoiceQuery;
|
||||
|
||||
/**
|
||||
* @covers \App\Repository\Query\InvoiceQuery
|
||||
*/
|
||||
class InvoiceQueryTest extends BaseQueryTest
|
||||
{
|
||||
public function testQuery()
|
||||
{
|
||||
$sut = new InvoiceQuery();
|
||||
|
||||
$this->assertResultType($sut);
|
||||
$this->assertPage($sut);
|
||||
$this->assertPageSize($sut);
|
||||
$this->assertOrderBy($sut, 'begin');
|
||||
$this->assertOrder($sut, InvoiceQuery::ORDER_DESC);
|
||||
|
||||
$this->assertUser($sut);
|
||||
$this->assertCustomer($sut);
|
||||
$this->assertProject($sut);
|
||||
$this->assertActivity($sut);
|
||||
$this->assertState($sut);
|
||||
$this->assertExported($sut);
|
||||
$this->assertMarkAsExported($sut);
|
||||
}
|
||||
|
||||
protected function assertMarkAsExported(InvoiceQuery $sut)
|
||||
{
|
||||
$this->assertFalse($sut->isMarkAsExported());
|
||||
|
||||
$sut->setMarkAsExported(true);
|
||||
$this->assertTrue($sut->isMarkAsExported());
|
||||
}
|
||||
|
||||
protected function assertUser(InvoiceQuery $sut)
|
||||
{
|
||||
$this->assertNull($sut->getUser());
|
||||
|
||||
$expected = new User();
|
||||
$expected->setUsername('foo-bar');
|
||||
$sut->setUser($expected);
|
||||
$this->assertEquals($expected, $sut->getUser());
|
||||
}
|
||||
|
||||
protected function assertCustomer(InvoiceQuery $sut)
|
||||
{
|
||||
$this->assertNull($sut->getCustomer());
|
||||
|
||||
$expected = new Customer();
|
||||
$expected->setName('foo-bar');
|
||||
$sut->setCustomer($expected);
|
||||
$this->assertEquals($expected, $sut->getCustomer());
|
||||
}
|
||||
|
||||
protected function assertProject(InvoiceQuery $sut)
|
||||
{
|
||||
$this->assertNull($sut->getProject());
|
||||
|
||||
$expected = new Project();
|
||||
$expected->setName('foo-bar');
|
||||
$sut->setProject($expected);
|
||||
$this->assertEquals($expected, $sut->getProject());
|
||||
}
|
||||
|
||||
protected function assertActivity(InvoiceQuery $sut)
|
||||
{
|
||||
$this->assertNull($sut->getActivity());
|
||||
|
||||
$expected = new Activity();
|
||||
$expected->setName('foo-bar');
|
||||
$sut->setActivity($expected);
|
||||
$this->assertEquals($expected, $sut->getActivity());
|
||||
}
|
||||
|
||||
protected function assertState(InvoiceQuery $sut)
|
||||
{
|
||||
$this->assertEquals(InvoiceQuery::STATE_ALL, $sut->getState());
|
||||
|
||||
$sut->setState(PHP_INT_MAX);
|
||||
$this->assertEquals(InvoiceQuery::STATE_ALL, $sut->getState());
|
||||
|
||||
$sut->setState(InvoiceQuery::STATE_STOPPED);
|
||||
$this->assertEquals(InvoiceQuery::STATE_STOPPED, $sut->getState());
|
||||
|
||||
$sut->setState(InvoiceQuery::STATE_RUNNING);
|
||||
$this->assertEquals(InvoiceQuery::STATE_RUNNING, $sut->getState());
|
||||
|
||||
$sut->setState(InvoiceQuery::STATE_ALL);
|
||||
$this->assertEquals(InvoiceQuery::STATE_ALL, $sut->getState());
|
||||
}
|
||||
|
||||
protected function assertExported(InvoiceQuery $sut)
|
||||
{
|
||||
$this->assertEquals(InvoiceQuery::STATE_ALL, $sut->getExported());
|
||||
|
||||
$sut->setExported(PHP_INT_MAX);
|
||||
$this->assertEquals(InvoiceQuery::STATE_ALL, $sut->getExported());
|
||||
|
||||
$sut->setExported(InvoiceQuery::STATE_EXPORTED);
|
||||
$this->assertEquals(InvoiceQuery::STATE_EXPORTED, $sut->getExported());
|
||||
|
||||
$sut->setExported(InvoiceQuery::STATE_NOT_EXPORTED);
|
||||
$this->assertEquals(InvoiceQuery::STATE_NOT_EXPORTED, $sut->getExported());
|
||||
|
||||
$sut->setExported(InvoiceQuery::STATE_ALL);
|
||||
$this->assertEquals(InvoiceQuery::STATE_ALL, $sut->getExported());
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ class TimesheetQueryTest extends BaseQueryTest
|
||||
$this->assertOrder($sut, TimesheetQuery::ORDER_DESC);
|
||||
|
||||
$this->assertUser($sut);
|
||||
$this->assertUsers($sut);
|
||||
$this->assertCustomer($sut);
|
||||
$this->assertProject($sut);
|
||||
$this->assertActivity($sut);
|
||||
@@ -48,6 +49,30 @@ class TimesheetQueryTest extends BaseQueryTest
|
||||
$this->assertEquals($expected, $sut->getUser());
|
||||
}
|
||||
|
||||
protected function assertUsers(TimesheetQuery $sut)
|
||||
{
|
||||
$this->assertEmpty($sut->getUsers());
|
||||
|
||||
$user = $this->getMockBuilder(User::class)->disableOriginalConstructor()->getMock();
|
||||
$user->method('getId')->willReturn(1);
|
||||
$sut->addUser($user);
|
||||
|
||||
$user = $this->getMockBuilder(User::class)->disableOriginalConstructor()->getMock();
|
||||
$user->method('getId')->willReturn(1);
|
||||
$sut->addUser($user);
|
||||
|
||||
$user = $this->getMockBuilder(User::class)->disableOriginalConstructor()->getMock();
|
||||
$user->method('getId')->willReturn(13);
|
||||
$sut->addUser($user);
|
||||
|
||||
$user = $this->getMockBuilder(User::class)->disableOriginalConstructor()->getMock();
|
||||
$user->method('getId')->willReturn(27);
|
||||
$sut->addUser($user);
|
||||
$sut->removeUser($user);
|
||||
|
||||
$this->assertCount(2, $sut->getUsers());
|
||||
}
|
||||
|
||||
protected function assertCustomer(TimesheetQuery $sut)
|
||||
{
|
||||
$this->assertNull($sut->getCustomer());
|
||||
|
||||
@@ -73,7 +73,7 @@ abstract class AbstractVoterTest extends TestCase
|
||||
$customers = ['view_customer', 'edit_customer', 'budget_customer', 'delete_customer', 'create_customer'];
|
||||
$customersTeam = ['view_customer', 'edit_teamlead_customer', 'budget_teamlead_customer'];
|
||||
$invoice = ['view_invoice', 'create_invoice'];
|
||||
$invoiceTemplate = ['view_invoice_template', 'create_invoice_template', 'edit_invoice_template', 'delete_invoice_template'];
|
||||
$invoiceTemplate = ['manage_invoice_template'];
|
||||
$timesheet = ['view_own_timesheet', 'start_own_timesheet', 'stop_own_timesheet', 'create_own_timesheet', 'edit_own_timesheet', 'export_own_timesheet', 'delete_own_timesheet'];
|
||||
$timesheetOthers = ['view_other_timesheet', 'start_other_timesheet', 'stop_other_timesheet', 'create_other_timesheet', 'edit_other_timesheet', 'export_other_timesheet', 'delete_other_timesheet'];
|
||||
$profile = ['view_own_profile', 'edit_own_profile', 'password_own_profile', 'preferences_own_profile', 'api-token_own_profile'];
|
||||
|
||||
@@ -1,66 +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\Tests\Voter;
|
||||
|
||||
use App\Entity\InvoiceTemplate;
|
||||
use App\Entity\User;
|
||||
use App\Voter\InvoiceTemplateVoter;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
|
||||
|
||||
/**
|
||||
* @covers \App\Voter\InvoiceTemplateVoter
|
||||
*/
|
||||
class InvoiceTemplateVoterTest extends AbstractVoterTest
|
||||
{
|
||||
/**
|
||||
* @dataProvider getTestData
|
||||
*/
|
||||
public function testVote(User $user, $subject, $attribute, $result)
|
||||
{
|
||||
$token = new UsernamePasswordToken($user, 'foo', 'bar', $user->getRoles());
|
||||
$sut = $this->getVoter(InvoiceTemplateVoter::class, $user);
|
||||
|
||||
$this->assertEquals($result, $sut->vote($token, $subject, [$attribute]));
|
||||
}
|
||||
|
||||
public function getTestData()
|
||||
{
|
||||
$user0 = $this->getUser(0, null);
|
||||
$user1 = $this->getUser(1, User::ROLE_USER);
|
||||
$user2 = $this->getUser(2, User::ROLE_TEAMLEAD);
|
||||
$user3 = $this->getUser(3, User::ROLE_ADMIN);
|
||||
$user4 = $this->getUser(4, User::ROLE_SUPER_ADMIN);
|
||||
|
||||
$result = VoterInterface::ACCESS_GRANTED;
|
||||
foreach ([$user3, $user4] as $user) {
|
||||
yield [$user, new InvoiceTemplate(), 'view', $result];
|
||||
yield [$user, new InvoiceTemplate(), 'edit', $result];
|
||||
yield [$user, new InvoiceTemplate(), 'delete', $result];
|
||||
}
|
||||
|
||||
$result = VoterInterface::ACCESS_DENIED;
|
||||
foreach ([$user0, $user1, $user2] as $user) {
|
||||
yield [$user, new InvoiceTemplate(), 'view', $result];
|
||||
yield [$user, new InvoiceTemplate(), 'edit', $result];
|
||||
yield [$user, new InvoiceTemplate(), 'delete', $result];
|
||||
}
|
||||
|
||||
$result = VoterInterface::ACCESS_ABSTAIN;
|
||||
foreach ([$user0, $user1, $user2] as $user) {
|
||||
yield [$user, new InvoiceTemplate(), 'view_invoice_template', $result];
|
||||
yield [$user, new InvoiceTemplate(), 'edit_invoice_template', $result];
|
||||
yield [$user, new InvoiceTemplate(), 'delete_invoice_template', $result];
|
||||
yield [$user, new \stdClass(), 'view', $result];
|
||||
yield [$user, null, 'edit', $result];
|
||||
yield [$user, $user, 'delete', $result];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ class RolePermissionVoterTest extends AbstractVoterTest
|
||||
$user4 = $this->getUser(4, User::ROLE_SUPER_ADMIN);
|
||||
|
||||
$invoice = [
|
||||
'create_invoice_template' => null,
|
||||
'manage_invoice_template' => null,
|
||||
'view_invoice' => null,
|
||||
'create_invoice' => null,
|
||||
];
|
||||
|
||||
@@ -4,6 +4,7 @@ includes:
|
||||
- %rootDir%/../phpstan-phpunit/extension.neon
|
||||
|
||||
parameters:
|
||||
tmpDir: %rootDir%/../../../var/cache/phpstan
|
||||
ignoreErrors:
|
||||
- '#Access to an undefined property Faker\\Generator::\$stateAbbr.#'
|
||||
- '#Access to an undefined property Faker\\Generator::\$catchPhrase.#'
|
||||
|
||||
@@ -601,10 +601,6 @@
|
||||
<source>invoice.subtitle</source>
|
||||
<target>م بإنشاء فواتير من بيانات ورقة التوقيت الخاصة بك.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice.preview">
|
||||
<source>invoice.preview</source>
|
||||
<target>هذه معاينة للبيانات التي ستظهر في مستند الفاتورة.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="button.print">
|
||||
<source>button.print</source>
|
||||
<target>طباعة</target>
|
||||
|
||||
@@ -760,9 +760,17 @@
|
||||
<source>invoice.subtitle</source>
|
||||
<target>Hier erstellen Sie Rechnungen für ihre aufgezeichneten Zeiten.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice.preview">
|
||||
<source>invoice.preview</source>
|
||||
<target>Dies ist eine Vorschau der Daten, die in Ihrer Rechnung enthalten sein werden.</target>
|
||||
<trans-unit id="invoice.filter">
|
||||
<source>invoice.filter</source>
|
||||
<target>Rechnungsdaten filtern</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="limited_entries">
|
||||
<source>limited_entries</source>
|
||||
<target>Limitiert auf %max% Einträge</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="button.preview">
|
||||
<source>button.preview</source>
|
||||
<target>Vorschau</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="button.print">
|
||||
<source>button.print</source>
|
||||
@@ -788,6 +796,10 @@
|
||||
<source>invoice_print</source>
|
||||
<target>Rechnung</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.mark_as_exported">
|
||||
<source>label.mark_as_exported</source>
|
||||
<target>Als exportiert markieren</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.template">
|
||||
<source>label.template</source>
|
||||
<target>Vorlage</target>
|
||||
@@ -888,6 +900,10 @@
|
||||
<source>export.subtitle</source>
|
||||
<target>Exportieren Sie geloggte Zeiten in veschiedene Zielformate</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="export.filter">
|
||||
<source>export.filter</source>
|
||||
<target>Daten für Export filtern</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="export.period">
|
||||
<source>export.period</source>
|
||||
<target>Zeitraum</target>
|
||||
|
||||
@@ -760,9 +760,17 @@
|
||||
<source>invoice.subtitle</source>
|
||||
<target>Create invoices from your recorded timesheet entries.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice.preview">
|
||||
<source>invoice.preview</source>
|
||||
<target>This is a preview of the data that will show up in your invoice document.</target>
|
||||
<trans-unit id="invoice.filter">
|
||||
<source>invoice.filter</source>
|
||||
<target>Filter invoice data</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="limited_entries">
|
||||
<source>limited_entries</source>
|
||||
<target>Limited to %max% entries</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="button.preview">
|
||||
<source>button.preview</source>
|
||||
<target>Preview</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="button.print">
|
||||
<source>button.print</source>
|
||||
@@ -788,6 +796,10 @@
|
||||
<source>invoice_print</source>
|
||||
<target>Invoice</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.mark_as_exported">
|
||||
<source>label.mark_as_exported</source>
|
||||
<target>Mark as exported</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.template">
|
||||
<source>label.template</source>
|
||||
<target>Template</target>
|
||||
@@ -888,6 +900,10 @@
|
||||
<source>export.subtitle</source>
|
||||
<target>Export timesheet data to various formats</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="export.filter">
|
||||
<source>export.filter</source>
|
||||
<target>Filter data for export</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="export.period">
|
||||
<source>export.period</source>
|
||||
<target>Period</target>
|
||||
|
||||
@@ -740,10 +740,6 @@
|
||||
<source>invoice.subtitle</source>
|
||||
<target>Crear facturas a partir de registros de sus partes de horas.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice.preview">
|
||||
<source>invoice.preview</source>
|
||||
<target>Esta es una vista previa de los datos que se mostrarán en su documento de factura.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="button.print">
|
||||
<source>button.print</source>
|
||||
<target>Imprimir</target>
|
||||
|
||||
@@ -740,10 +740,6 @@
|
||||
<source>invoice.subtitle</source>
|
||||
<target>Générer des factures à partir des fiches de temps.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice.preview">
|
||||
<source>invoice.preview</source>
|
||||
<target>Il s'agit d'un aperçu des données qui apparaîtront dans la facture.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="button.print">
|
||||
<source>button.print</source>
|
||||
<target>Imprimer</target>
|
||||
|
||||
@@ -740,10 +740,6 @@
|
||||
<source>invoice.subtitle</source>
|
||||
<target>Számlák készítése az időbejegyzéseidből</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice.preview">
|
||||
<source>invoice.preview</source>
|
||||
<target>A számlán szereplő adatok előnézete.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="button.print">
|
||||
<source>button.print</source>
|
||||
<target>Nyomtatás</target>
|
||||
|
||||
@@ -740,10 +740,6 @@
|
||||
<source>invoice.subtitle</source>
|
||||
<target>タイムシートに記録したエントリから請求書を作成します。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice.preview">
|
||||
<source>invoice.preview</source>
|
||||
<target>これは請求書に出力されるデータのプレビューです。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="button.print">
|
||||
<source>button.print</source>
|
||||
<target>出力</target>
|
||||
|
||||
@@ -740,10 +740,6 @@
|
||||
<source>invoice.subtitle</source>
|
||||
<target>Criar faturas a partir de suas entradas de quadro de horários registradas.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice.preview">
|
||||
<source>invoice.preview</source>
|
||||
<target>Esta é uma prévia dos dados que serão exibidos no documento da fatura.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="button.print">
|
||||
<source>button.print</source>
|
||||
<target>Imprimir</target>
|
||||
|
||||
@@ -754,10 +754,6 @@
|
||||
<source>invoice.subtitle</source>
|
||||
<target>Vytvorte faktúry z vašich zaznamenaných položiek časového rozvrhu.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice.preview">
|
||||
<source>invoice.preview</source>
|
||||
<target>Toto je ukážka údajov, ktoré sa zobrazia vo vašom fakturačnom dokumente.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="button.print">
|
||||
<source>button.print</source>
|
||||
<target>Tlačiť</target>
|
||||
|
||||
@@ -739,11 +739,6 @@ För närvarande har %user% användare %records% tidsrekord som räknas upp till
|
||||
<source>invoice.subtitle</source>
|
||||
<target state="translated">Skapa fakturor från dina inspelade tidskriftsuppgifter.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice.preview">
|
||||
<source>invoice.preview</source>
|
||||
<target state="translated">Detta är en förhandsvisning av de data som kommer att visas i ditt fakturatokument.</target>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="button.print">
|
||||
<source>button.print</source>
|
||||
<target state="translated">Skriv ut</target>
|
||||
|
||||
Reference in New Issue
Block a user