diff --git a/UPGRADING.md b/UPGRADING.md index 790d551b..a09006cb 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -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) diff --git a/composer.lock b/composer.lock index 3425f72e..ad785d86 100644 --- a/composer.lock +++ b/composer.lock @@ -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", diff --git a/config/packages/kimai.yaml b/config/packages/kimai.yaml index 9169a900..7da4ecf6 100644 --- a/config/packages/kimai.yaml +++ b/config/packages/kimai.yaml @@ -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"]) diff --git a/phpstan.neon b/phpstan.neon index ab1c75db..9ab5cbb4 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -3,6 +3,7 @@ includes: - vendor/phpstan/phpstan-doctrine/extension.neon parameters: + tmpDir: %rootDir%/../../../var/cache/phpstan autoload_directories: - %rootDir%/../../../src/Migrations # symfony: diff --git a/src/Controller/ExportController.php b/src/Controller/ExportController.php index df826590..1177a18c 100644 --- a/src/Controller/ExportController.php +++ b/src/Controller/ExportController.php @@ -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; } /** diff --git a/src/Controller/InvoiceController.php b/src/Controller/InvoiceController.php index 202ad39e..77d7ecee 100644 --- a/src/Controller/InvoiceController.php +++ b/src/Controller/InvoiceController.php @@ -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'); diff --git a/src/EventSubscriber/MenuSubscriber.php b/src/EventSubscriber/MenuSubscriber.php index a4d578b5..375912a3 100644 --- a/src/EventSubscriber/MenuSubscriber.php +++ b/src/EventSubscriber/MenuSubscriber.php @@ -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')) ); diff --git a/src/Export/Renderer/AbstractSpreadsheetRenderer.php b/src/Export/Renderer/AbstractSpreadsheetRenderer.php index 168433b4..f65f93a5 100644 --- a/src/Export/Renderer/AbstractSpreadsheetRenderer.php +++ b/src/Export/Renderer/AbstractSpreadsheetRenderer.php @@ -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 diff --git a/src/Export/Renderer/CsvRenderer.php b/src/Export/Renderer/CsvRenderer.php index 875c2b67..7028350f 100644 --- a/src/Export/Renderer/CsvRenderer.php +++ b/src/Export/Renderer/CsvRenderer.php @@ -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)); + } } diff --git a/src/Export/Renderer/OdsRenderer.php b/src/Export/Renderer/OdsRenderer.php deleted file mode 100644 index 816d97c7..00000000 --- a/src/Export/Renderer/OdsRenderer.php +++ /dev/null @@ -1,75 +0,0 @@ -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'; - } -} diff --git a/src/Form/Toolbar/AbstractToolbarForm.php b/src/Form/Toolbar/AbstractToolbarForm.php index 43224602..88b94b27 100644 --- a/src/Form/Toolbar/AbstractToolbarForm.php +++ b/src/Form/Toolbar/AbstractToolbarForm.php @@ -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 + ], + ]); + } } diff --git a/src/Form/Toolbar/ExportToolbarForm.php b/src/Form/Toolbar/ExportToolbarForm.php index 4ea47b99..8036da5b 100644 --- a/src/Form/Toolbar/ExportToolbarForm.php +++ b/src/Form/Toolbar/ExportToolbarForm.php @@ -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} */ diff --git a/src/Form/Toolbar/InvoiceToolbarForm.php b/src/Form/Toolbar/InvoiceToolbarForm.php index 206584b5..dae504a3 100644 --- a/src/Form/Toolbar/InvoiceToolbarForm.php +++ b/src/Form/Toolbar/InvoiceToolbarForm.php @@ -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, ]); } diff --git a/src/Form/Toolbar/TimesheetToolbarForm.php b/src/Form/Toolbar/TimesheetToolbarForm.php index e2e11b1c..c1dde24d 100644 --- a/src/Form/Toolbar/TimesheetToolbarForm.php +++ b/src/Form/Toolbar/TimesheetToolbarForm.php @@ -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} */ diff --git a/src/Invoice/Renderer/RendererTrait.php b/src/Invoice/Renderer/RendererTrait.php index f4872c8c..bc5c0014 100644 --- a/src/Invoice/Renderer/RendererTrait.php +++ b/src/Invoice/Renderer/RendererTrait.php @@ -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) { diff --git a/src/Repository/Query/BaseQuery.php b/src/Repository/Query/BaseQuery.php index 9afcf6a5..80e7a2d0 100644 --- a/src/Repository/Query/BaseQuery.php +++ b/src/Repository/Query/BaseQuery.php @@ -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; } diff --git a/src/Repository/Query/ExportQuery.php b/src/Repository/Query/ExportQuery.php index 01bf7475..10609dbf 100644 --- a/src/Repository/Query/ExportQuery.php +++ b/src/Repository/Query/ExportQuery.php @@ -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; + } } diff --git a/src/Repository/Query/InvoiceQuery.php b/src/Repository/Query/InvoiceQuery.php index edae5c2f..92aa1e4d 100644 --- a/src/Repository/Query/InvoiceQuery.php +++ b/src/Repository/Query/InvoiceQuery.php @@ -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; + } } diff --git a/src/Repository/Query/TimesheetQuery.php b/src/Repository/Query/TimesheetQuery.php index f7e5b6d5..0838cfe2 100644 --- a/src/Repository/Query/TimesheetQuery.php +++ b/src/Repository/Query/TimesheetQuery.php @@ -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 */ diff --git a/src/Repository/TimesheetRepository.php b/src/Repository/TimesheetRepository.php index e6794809..fdab2fae 100644 --- a/src/Repository/TimesheetRepository.php +++ b/src/Repository/TimesheetRepository.php @@ -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(); + } } diff --git a/src/Voter/InvoiceTemplateVoter.php b/src/Voter/InvoiceTemplateVoter.php deleted file mode 100644 index a5a1214e..00000000 --- a/src/Voter/InvoiceTemplateVoter.php +++ /dev/null @@ -1,68 +0,0 @@ -getUser(); - - if (!$user instanceof User) { - return false; - } - - return $this->hasRolePermission($user, $attribute . '_invoice_template'); - } -} diff --git a/templates/base.html.twig b/templates/base.html.twig index 918e4e6e..92e0c350 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -85,7 +85,7 @@ {% endif %} - {% if is_granted('view_export') %} + {% if is_granted('create_export') %}
  • diff --git a/templates/default/_form.html.twig b/templates/default/_form.html.twig index f154cb79..592d3fea 100644 --- a/templates/default/_form.html.twig +++ b/templates/default/_form.html.twig @@ -1,3 +1,6 @@ +{% if form_theme is defined %} + {% form_theme form form_theme %} +{% endif %}
    {{ form_start(form) }}
    diff --git a/templates/export/actions.html.twig b/templates/export/actions.html.twig new file mode 100644 index 00000000..ab935549 --- /dev/null +++ b/templates/export/actions.html.twig @@ -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 %} diff --git a/templates/export/index.html.twig b/templates/export/index.html.twig index 2f4e7f73..e88be299 100644 --- a/templates/export/index.html.twig +++ b/templates/export/index.html.twig @@ -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 %} -
    -{% 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 %} - - {{ entry.begin|date_short }} - {{ widgets.label_user(entry.user) }} - {{ widgets.label_customer(entry.project.customer) }} - {{ widgets.label_project(entry.project) }} - {{ widgets.label_activity(entry.activity) }} - - {{ entry.description }} - - - {{ rate|money(currency) }} - - - {{ duration }} - - - {{ entry.rate|money(currency) }} - - - {% if is_granted('edit_export', entry) %} - {% if entry.exported %} - - {% else %} - - {% endif %} - {% else %} - {% if entry.exported %} - {{ 'entryState.exported'|trans }} - {% else %} - {{ 'entryState.not_exported'|trans }} - {% endif %} - {% endif %} - - - {% endfor %} - {{ tables.data_table_footer(entries) }} - - {% if is_granted('create_export') %} -
    -
    -
    - {% for button in renderer %} - - {% endfor %} -
    + {% 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%} +
    + {% for button in renderer %} + + {% endfor %}
    -
    + {{ 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 %} + + {{ entry.begin|date_short }} + {{ widgets.label_user(entry.user) }} + {{ widgets.label_customer(entry.project.customer) }} + {{ widgets.label_project(entry.project) }} + {{ widgets.label_activity(entry.activity) }} + + {{ entry.description }} + + + {{ rate|money(currency) }} + + + {{ duration }} + + + {{ entry.rate|money(currency) }} + + + {% if is_granted('edit_export', entry) %} + {% if entry.exported %} + + {% else %} + + {% endif %} + {% else %} + {% if entry.exported %} + {{ 'entryState.exported'|trans }} + {% else %} + {{ 'entryState.not_exported'|trans }} + {% endif %} + {% endif %} + + + {% endfor %} + {{ tables.data_table_footer(entries) }} {% endif %} {% endif %} diff --git a/templates/export/renderer/default.html.twig b/templates/export/renderer/default.html.twig index 3c6a2ca4..d83c64c0 100644 --- a/templates/export/renderer/default.html.twig +++ b/templates/export/renderer/default.html.twig @@ -260,8 +260,10 @@ {{ entry.begin|date_time }} -
    - {{ entry.end|date_time }} + {% if entry.end %} +
    + {{ entry.end|date_time }} + {% endif %} {{ widgets.username(entry.user) }} diff --git a/templates/export/renderer/pdf.html.twig b/templates/export/renderer/pdf.html.twig index 7b88172e..72c0a6db 100644 --- a/templates/export/renderer/pdf.html.twig +++ b/templates/export/renderer/pdf.html.twig @@ -144,8 +144,10 @@ mpdf--> {{ entry.begin|date_time }} -
    - {{ entry.end|date_time }} + {% if entry.end %} +
    + {{ entry.end|date_time }} + {% endif %} {{ entry.project.customer.name }} - {{ entry.project.name }} - {{ entry.activity.name }} diff --git a/templates/invoice/actions.html.twig b/templates/invoice/actions.html.twig new file mode 100644 index 00000000..e8f4ddf1 --- /dev/null +++ b/templates/invoice/actions.html.twig @@ -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 %} diff --git a/templates/invoice/index.html.twig b/templates/invoice/index.html.twig index 42ea7a79..3ba1db71 100644 --- a/templates/invoice/index.html.twig +++ b/templates/invoice/index.html.twig @@ -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 %} -
    -{% 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 %} - - {{ entry.begin|date_short }} - {{ widgets.label_user(entry.user) }} - - {% if entry.description is not empty %} - {{ entry.description|escape|desc2html }} - {% else %} - {{ entry.activity.name }} / {{ entry.project.name }} - {% endif %} - - {{ rate|money(model.calculator.currency) }} - {{ duration }} - {{ entry.rate|money(model.calculator.currency) }} - - {% endfor %} - {{ tables.data_table_footer(entries) }} - - {% if is_granted('create_invoice') %} -
    + {% 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 %} + + {{ entry.begin|date_short }} + {{ widgets.label_user(entry.user) }} + + {% if entry.description is not empty %} + {{ entry.description|escape|desc2html }} + {% else %} + {{ entry.activity.name }} / {{ entry.project.name }} + {% endif %} + + {{ rate|money(model.calculator.currency) }} + {{ duration }} + {{ entry.rate|money(model.calculator.currency) }} + + {% endfor %} + {{ tables.data_table_footer(entries) }} {% endif %} {% endif %} diff --git a/templates/invoice/templates.html.twig b/templates/invoice/templates.html.twig index 5a5db648..b2328465 100644 --- a/templates/invoice/templates.html.twig +++ b/templates/invoice/templates.html.twig @@ -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 %} - + {{ entry.name }} {{ entry.title }} {{ entry.dueDays }} {{ entry.vat }} - {# 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') -}} {% endfor %} - {{ tables.data_table_footer(entries, 'admin_invoice_template_paginated') }} + {{ tables.data_table_footer(entries, 'admin_invoice_template') }} {% endblock %} diff --git a/templates/macros/actions.html.twig b/templates/macros/actions.html.twig index 688b8c15..7bcffffd 100644 --- a/templates/macros/actions.html.twig +++ b/templates/macros/actions.html.twig @@ -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 %} diff --git a/tests/Controller/ExportControllerTest.php b/tests/Controller/ExportControllerTest.php index 39006165..593e630f 100644 --- a/tests/Controller/ExportControllerTest.php +++ b/tests/Controller/ExportControllerTest.php @@ -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()); + } } } diff --git a/tests/Controller/InvoiceControllerTest.php b/tests/Controller/InvoiceControllerTest.php index a39caa54..f0a1411e 100644 --- a/tests/Controller/InvoiceControllerTest.php +++ b/tests/Controller/InvoiceControllerTest.php @@ -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()); diff --git a/tests/Controller/UserControllerTest.php b/tests/Controller/UserControllerTest.php index b53637ac..5dc3b370 100644 --- a/tests/Controller/UserControllerTest.php +++ b/tests/Controller/UserControllerTest.php @@ -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); } } diff --git a/tests/Export/Renderer/AbstractRendererTest.php b/tests/Export/Renderer/AbstractRendererTest.php index bf3978cf..534fb7ef 100644 --- a/tests/Export/Renderer/AbstractRendererTest.php +++ b/tests/Export/Renderer/AbstractRendererTest.php @@ -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); } /** diff --git a/tests/Export/Renderer/CsvRendererTest.php b/tests/Export/Renderer/CsvRendererTest.php index 2333c57c..379a9839 100644 --- a/tests/Export/Renderer/CsvRendererTest.php +++ b/tests/Export/Renderer/CsvRendererTest.php @@ -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]); } diff --git a/tests/Export/Renderer/OdsRendererTest.php b/tests/Export/Renderer/OdsRendererTest.php deleted file mode 100644 index cd44d424..00000000 --- a/tests/Export/Renderer/OdsRendererTest.php +++ /dev/null @@ -1,52 +0,0 @@ -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())); - } -} diff --git a/tests/Invoice/Renderer/DebugRendererTest.php b/tests/Invoice/Renderer/DebugRendererTest.php index 3d0aa4dc..989d3905 100644 --- a/tests/Invoice/Renderer/DebugRendererTest.php +++ b/tests/Invoice/Renderer/DebugRendererTest.php @@ -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); diff --git a/tests/Repository/Query/ExportQueryTest.php b/tests/Repository/Query/ExportQueryTest.php index 20122a1f..1d84097e 100644 --- a/tests/Repository/Query/ExportQueryTest.php +++ b/tests/Repository/Query/ExportQueryTest.php @@ -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) diff --git a/tests/Repository/Query/InvoiceQueryTest.php b/tests/Repository/Query/InvoiceQueryTest.php new file mode 100644 index 00000000..d2cda8db --- /dev/null +++ b/tests/Repository/Query/InvoiceQueryTest.php @@ -0,0 +1,123 @@ +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()); + } +} diff --git a/tests/Repository/Query/TimesheetQueryTest.php b/tests/Repository/Query/TimesheetQueryTest.php index 803afa23..3861c0fe 100644 --- a/tests/Repository/Query/TimesheetQueryTest.php +++ b/tests/Repository/Query/TimesheetQueryTest.php @@ -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()); diff --git a/tests/Voter/AbstractVoterTest.php b/tests/Voter/AbstractVoterTest.php index 2ba196bd..ebf8ecf8 100644 --- a/tests/Voter/AbstractVoterTest.php +++ b/tests/Voter/AbstractVoterTest.php @@ -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']; diff --git a/tests/Voter/InvoiceTemplateVoterTest.php b/tests/Voter/InvoiceTemplateVoterTest.php deleted file mode 100644 index 35463906..00000000 --- a/tests/Voter/InvoiceTemplateVoterTest.php +++ /dev/null @@ -1,66 +0,0 @@ -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]; - } - } -} diff --git a/tests/Voter/RolePermissionVoterTest.php b/tests/Voter/RolePermissionVoterTest.php index b8dd1583..c99425d4 100644 --- a/tests/Voter/RolePermissionVoterTest.php +++ b/tests/Voter/RolePermissionVoterTest.php @@ -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, ]; diff --git a/tests/phpstan.neon b/tests/phpstan.neon index 86776f6d..fc982aa0 100644 --- a/tests/phpstan.neon +++ b/tests/phpstan.neon @@ -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.#' diff --git a/translations/messages.ar.xliff b/translations/messages.ar.xliff index 063bcd74..6758f364 100755 --- a/translations/messages.ar.xliff +++ b/translations/messages.ar.xliff @@ -601,10 +601,6 @@ invoice.subtitle م بإنشاء فواتير من بيانات ورقة التوقيت الخاصة بك. - - invoice.preview - هذه معاينة للبيانات التي ستظهر في مستند الفاتورة. - button.print طباعة diff --git a/translations/messages.de.xliff b/translations/messages.de.xliff index 56bf9c47..c3ff2aa2 100644 --- a/translations/messages.de.xliff +++ b/translations/messages.de.xliff @@ -760,9 +760,17 @@ invoice.subtitle Hier erstellen Sie Rechnungen für ihre aufgezeichneten Zeiten. - - invoice.preview - Dies ist eine Vorschau der Daten, die in Ihrer Rechnung enthalten sein werden. + + invoice.filter + Rechnungsdaten filtern + + + limited_entries + Limitiert auf %max% Einträge + + + button.preview + Vorschau button.print @@ -788,6 +796,10 @@ invoice_print Rechnung + + label.mark_as_exported + Als exportiert markieren + label.template Vorlage @@ -888,6 +900,10 @@ export.subtitle Exportieren Sie geloggte Zeiten in veschiedene Zielformate + + export.filter + Daten für Export filtern + export.period Zeitraum diff --git a/translations/messages.en.xliff b/translations/messages.en.xliff index 4d85e5c1..cb62613d 100644 --- a/translations/messages.en.xliff +++ b/translations/messages.en.xliff @@ -760,9 +760,17 @@ invoice.subtitle Create invoices from your recorded timesheet entries. - - invoice.preview - This is a preview of the data that will show up in your invoice document. + + invoice.filter + Filter invoice data + + + limited_entries + Limited to %max% entries + + + button.preview + Preview button.print @@ -788,6 +796,10 @@ invoice_print Invoice + + label.mark_as_exported + Mark as exported + label.template Template @@ -888,6 +900,10 @@ export.subtitle Export timesheet data to various formats + + export.filter + Filter data for export + export.period Period diff --git a/translations/messages.es.xliff b/translations/messages.es.xliff index b7028551..f0fca4e7 100644 --- a/translations/messages.es.xliff +++ b/translations/messages.es.xliff @@ -740,10 +740,6 @@ invoice.subtitle Crear facturas a partir de registros de sus partes de horas. - - invoice.preview - Esta es una vista previa de los datos que se mostrarán en su documento de factura. - button.print Imprimir diff --git a/translations/messages.fr.xliff b/translations/messages.fr.xliff index a3fd66f1..40045562 100644 --- a/translations/messages.fr.xliff +++ b/translations/messages.fr.xliff @@ -740,10 +740,6 @@ invoice.subtitle Générer des factures à partir des fiches de temps. - - invoice.preview - Il s'agit d'un aperçu des données qui apparaîtront dans la facture. - button.print Imprimer diff --git a/translations/messages.hu.xliff b/translations/messages.hu.xliff index 34da0981..ad4d5634 100644 --- a/translations/messages.hu.xliff +++ b/translations/messages.hu.xliff @@ -740,10 +740,6 @@ invoice.subtitle Számlák készítése az időbejegyzéseidből - - invoice.preview - A számlán szereplő adatok előnézete. - button.print Nyomtatás diff --git a/translations/messages.ja.xliff b/translations/messages.ja.xliff index 39d5898e..bc4d6a41 100644 --- a/translations/messages.ja.xliff +++ b/translations/messages.ja.xliff @@ -740,10 +740,6 @@ invoice.subtitle タイムシートに記録したエントリから請求書を作成します。 - - invoice.preview - これは請求書に出力されるデータのプレビューです。 - button.print 出力 diff --git a/translations/messages.pt_BR.xliff b/translations/messages.pt_BR.xliff index 258b191e..4fe9529b 100644 --- a/translations/messages.pt_BR.xliff +++ b/translations/messages.pt_BR.xliff @@ -740,10 +740,6 @@ invoice.subtitle Criar faturas a partir de suas entradas de quadro de horários registradas. - - invoice.preview - Esta é uma prévia dos dados que serão exibidos no documento da fatura. - button.print Imprimir diff --git a/translations/messages.sk.xliff b/translations/messages.sk.xliff index 4cf05883..7e1e1734 100644 --- a/translations/messages.sk.xliff +++ b/translations/messages.sk.xliff @@ -754,10 +754,6 @@ invoice.subtitle Vytvorte faktúry z vašich zaznamenaných položiek časového rozvrhu. - - invoice.preview - Toto je ukážka údajov, ktoré sa zobrazia vo vašom fakturačnom dokumente. - button.print Tlačiť diff --git a/translations/messages.sv.xliff b/translations/messages.sv.xliff index 295bfcb5..e6a937a3 100755 --- a/translations/messages.sv.xliff +++ b/translations/messages.sv.xliff @@ -739,11 +739,6 @@ För närvarande har %user% användare %records% tidsrekord som räknas upp till invoice.subtitle Skapa fakturor från dina inspelade tidskriftsuppgifter. - - invoice.preview - Detta är en förhandsvisning av de data som kommer att visas i ditt fakturatokument. - - button.print Skriv ut