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') %}