diff --git a/UPGRADING.md b/UPGRADING.md index 7cd0c99f..292babd5 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -12,9 +12,14 @@ Perform EACH version specific task between your version and the new one, otherwi **New database tables and fields were created, don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).** -### Plugins +### ExpenseBundle -- Expenses plugin needs to be updated, due to incompatibilities in the underlying frameworks. +**ATTENTION** due to incompatibilities in the underlying frameworks users of the ExpenseBundle need to do one more step: + +If you use the ExpensePlugin, you should delete it before updating: `rm -r var/plugins/ExpenseBundle`. +You will run into an error otherwise during the update. + +After the Kimai update was successful, you have to re-install the latest version, which is compatible with Kimai 1.7 only. ### Hosting @@ -24,6 +29,7 @@ Perform EACH version specific task between your version and the new one, otherwi - Projects now have a start and end date and the API will only return those, which are either unconfigured or currently active, you might want to reload the list of projects once the user entered begin and end datetime OR use the new `ignoreDates` parameter. - Doctrine bundle was updated to v2, check your code for [the usage of RegistryInterface and ObjectManager](https://github.com/doctrine/DoctrineBundle/blob/master/UPGRADE-2.0.md) +- Removed the webserver bundle and the command `server:run` - see [docs](https://www.kimai.org/documentation/developers.html) ## [1.6](https://github.com/kevinpapst/kimai2/releases/tag/1.6), [1.6.1](https://github.com/kevinpapst/kimai2/releases/tag/1.6.1), [1.6.2](https://github.com/kevinpapst/kimai2/releases/tag/1.6.2) diff --git a/src/Entity/Timesheet.php b/src/Entity/Timesheet.php index cacf0781..14f53cdf 100644 --- a/src/Entity/Timesheet.php +++ b/src/Entity/Timesheet.php @@ -9,7 +9,7 @@ namespace App\Entity; -use App\Invoice\InvoiceItemInterface; +use App\Export\ExportItemInterface; use DateTime; use DateTimeZone; use Doctrine\Common\Collections\ArrayCollection; @@ -39,8 +39,11 @@ use Symfony\Component\Validator\Constraints as Assert; * columns={"start_time","end_time"} => IDX_4F60C6B1502DF58741561401 => ??? * columns={"start_time","end_time","user"} => IDX_4F60C6B1502DF587415614018D93D649 => ??? */ -class Timesheet implements EntityWithMetaFields, InvoiceItemInterface +class Timesheet implements EntityWithMetaFields, ExportItemInterface { + public const TYPE_TIMESHEET = 'timesheet'; + public const CATEGORY_WORK = 'work'; + /** * @var int * @@ -385,7 +388,7 @@ class Timesheet implements EntityWithMetaFields, InvoiceItemInterface /** * @return string[] */ - public function getTagsAsArray() + public function getTagsAsArray(): array { return array_map( function (Tag $element) { @@ -437,6 +440,18 @@ class Timesheet implements EntityWithMetaFields, InvoiceItemInterface return $this; } + public function getType(): string + { + // this will be improved in a future version + return self::TYPE_TIMESHEET; + } + + public function getCategory(): string + { + // this will be improved in a future version + return self::CATEGORY_WORK; + } + /** * @internal only here for symfony forms * @return Collection|MetaTableTypeInterface[] diff --git a/src/Export/Base/AbstractSpreadsheetRenderer.php b/src/Export/Base/AbstractSpreadsheetRenderer.php index c9de2f86..d9716e90 100644 --- a/src/Export/Base/AbstractSpreadsheetRenderer.php +++ b/src/Export/Base/AbstractSpreadsheetRenderer.php @@ -10,13 +10,13 @@ namespace App\Export\Base; use App\Entity\MetaTableTypeInterface; -use App\Entity\Timesheet; use App\Event\ActivityMetaDisplayEvent; use App\Event\CustomerMetaDisplayEvent; use App\Event\MetaDisplayEventInterface; use App\Event\ProjectMetaDisplayEvent; use App\Event\TimesheetMetaDisplayEvent; use App\Event\UserPreferenceDisplayEvent; +use App\Export\ExportItemInterface; use App\Repository\Query\CustomerQuery; use App\Repository\Query\TimesheetQuery; use App\Twig\DateExtensions; @@ -61,6 +61,30 @@ abstract class AbstractSpreadsheetRenderer * @var AuthorizationCheckerInterface */ protected $voter; + /** + * @var array + */ + protected $columns = [ + 'date' => [], + 'begin' => [], + 'end' => [], + 'duration' => [], + 'rate' => [], + 'user' => [], + 'customer' => [], + 'project' => [], + 'activity' => [], + 'description' => [], + 'exported' => [], + 'tags' => [], + 'hourlyRate' => [], + 'fixedRate' => [], + 'timesheet-meta' => [], + 'customer-meta' => [], + 'project-meta' => [], + 'activity-meta' => [], + 'user-meta' => [], + ]; public function __construct(TranslatorInterface $translator, DateExtensions $dateExtension, EventDispatcherInterface $dispatcher, AuthorizationCheckerInterface $voter) { @@ -72,7 +96,11 @@ abstract class AbstractSpreadsheetRenderer protected function isRenderRate(TimesheetQuery $query): bool { - return true; + if (null !== $query->getUser()) { + return $this->voter->isGranted('view_rate_own_timesheet'); + } + + return $this->voter->isGranted('view_rate_other_timesheet'); } protected function setFormattedDateTime(Worksheet $sheet, $column, $row, ?DateTime $date) @@ -152,75 +180,302 @@ abstract class AbstractSpreadsheetRenderer } /** - * @param Timesheet[] $timesheets + * @param ExportItemInterface[] $exportItems + * @param TimesheetQuery $query + * @param array $columns + * @return array + */ + protected function getColumns(array $exportItems, TimesheetQuery $query, array $columns): array + { + $showRates = $this->isRenderRate($query); + + if (isset($columns['date']) && !isset($columns['date']['render'])) { + $columns['date']['render'] = function (Worksheet $sheet, int $row, int $column, ExportItemInterface $entity) { + $this->setFormattedDate($sheet, $column, $row, $entity->getBegin()); + }; + } + + if (isset($columns['begin']) && !isset($columns['begin']['render'])) { + $columns['begin']['render'] = function (Worksheet $sheet, int $row, int $column, ExportItemInterface $entity) { + $this->setFormattedTime($sheet, $column, $row, $entity->getBegin()); + }; + } + + if (isset($columns['end']) && !isset($columns['end']['render'])) { + $columns['end']['render'] = function (Worksheet $sheet, int $row, int $column, ExportItemInterface $entity) { + $this->setFormattedTime($sheet, $column, $row, $entity->getEnd()); + }; + } + + if (isset($columns['duration']) && !isset($columns['duration']['render'])) { + $columns['duration']['render'] = function (Worksheet $sheet, int $row, int $column, ExportItemInterface $entity) { + $this->setDuration($sheet, $column, $row, $entity->getDuration()); + }; + } + + if ($showRates && isset($columns['rate']) && !isset($columns['rate']['render'])) { + $columns['rate']['render'] = function (Worksheet $sheet, int $row, int $column, ExportItemInterface $entity) { + $currency = ''; + if (null !== $entity->getProject()) { + $currency = $entity->getProject()->getCustomer()->getCurrency(); + } + $this->setRate($sheet, $column, $row, $entity->getRate(), $currency); + }; + } + + if (isset($columns['user']) && !isset($columns['user']['render'])) { + $columns['user']['render'] = function (Worksheet $sheet, int $row, int $column, ExportItemInterface $entity) { + $user = ''; + if (null !== $entity->getUser()) { + $user = $entity->getUser()->getDisplayName(); + } + $sheet->setCellValueByColumnAndRow($column, $row, $user); + }; + } + + if (isset($columns['customer']) && !isset($columns['customer']['render'])) { + $columns['customer']['render'] = function (Worksheet $sheet, int $row, int $column, ExportItemInterface $entity) { + $customer = ''; + if (null !== $entity->getProject()) { + $customer = $entity->getProject()->getCustomer()->getName(); + } + $sheet->setCellValueByColumnAndRow($column, $row, $customer); + }; + } + + if (isset($columns['project']) && !isset($columns['project']['render'])) { + $columns['project']['render'] = function (Worksheet $sheet, int $row, int $column, ExportItemInterface $entity) { + $project = ''; + if (null !== $entity->getProject()) { + $project = $entity->getProject()->getName(); + } + $sheet->setCellValueByColumnAndRow($column, $row, $project); + }; + } + + if (isset($columns['activity']) && !isset($columns['activity']['render'])) { + $columns['activity']['render'] = function (Worksheet $sheet, int $row, int $column, ExportItemInterface $entity) { + $activity = ''; + if (null !== $entity->getActivity()) { + $activity = $entity->getActivity()->getName(); + } + $sheet->setCellValueByColumnAndRow($column, $row, $activity); + }; + } + + if (isset($columns['description']) && !isset($columns['description']['render'])) { + $columns['description']['render'] = function (Worksheet $sheet, int $row, int $column, ExportItemInterface $entity) { + $sheet->setCellValueByColumnAndRow($column, $row, $entity->getDescription()); + }; + } + + if (isset($columns['exported']) && !isset($columns['exported']['render'])) { + $columns['exported']['render'] = function (Worksheet $sheet, int $row, int $column, ExportItemInterface $entity) { + $exported = $entity->isExported() ? 'entryState.exported' : 'entryState.not_exported'; + $sheet->setCellValueByColumnAndRow($column, $row, $this->translator->trans($exported)); + }; + } + + if (isset($columns['tags']) && !isset($columns['tags']['render'])) { + $columns['tags']['render'] = function (Worksheet $sheet, int $row, int $column, ExportItemInterface $entity) { + $sheet->setCellValueByColumnAndRow($column, $row, implode(',', $entity->getTagsAsArray())); + }; + } + + if ($showRates && isset($columns['hourlyRate']) && !isset($columns['hourlyRate']['render'])) { + $columns['hourlyRate']['render'] = function (Worksheet $sheet, int $row, int $column, ExportItemInterface $entity) { + $currency = ''; + if (null !== $entity->getProject()) { + $currency = $entity->getProject()->getCustomer()->getCurrency(); + } + $this->setRate($sheet, $column, $row, $entity->getHourlyRate(), $currency); + }; + } + + if ($showRates && isset($columns['fixedRate']) && !isset($columns['fixedRate']['render'])) { + $columns['fixedRate']['render'] = function (Worksheet $sheet, int $row, int $column, ExportItemInterface $entity) { + $currency = ''; + if (null !== $entity->getProject()) { + $currency = $entity->getProject()->getCustomer()->getCurrency(); + } + $this->setRate($sheet, $column, $row, $entity->getFixedRate(), $currency); + }; + } + + if (isset($columns['timesheet-meta'])) { + $timesheetMetaFields = $this->findMetaColumns(new TimesheetMetaDisplayEvent($query, TimesheetMetaDisplayEvent::EXPORT)); + + $columns['timesheet-meta'] = [ + 'header' => function (Worksheet $sheet, $row, $column) use ($timesheetMetaFields) { + foreach ($timesheetMetaFields as $metaField) { + $sheet->setCellValueByColumnAndRow($column++, $row, $this->translator->trans($metaField->getLabel())); + } + + return count($timesheetMetaFields); + }, + 'render' => function (Worksheet $sheet, int $row, int $column, ExportItemInterface $entity) use ($timesheetMetaFields) { + foreach ($timesheetMetaFields as $metaField) { + $metaFieldValue = ''; + $metaField = $entity->getMetaField($metaField->getName()); + if (null !== $metaField) { + $metaFieldValue = $metaField->getValue(); + } + $sheet->setCellValueByColumnAndRow($column++, $row, $metaFieldValue); + } + + return count($timesheetMetaFields); + } + ]; + } + + if (isset($columns['customer-meta'])) { + /** @var CustomerQuery $customerQuery */ + $customerQuery = $query->copyTo(new CustomerQuery()); + $customerMetaFields = $this->findMetaColumns(new CustomerMetaDisplayEvent($customerQuery, CustomerMetaDisplayEvent::EXPORT)); + + $columns['customer-meta'] = [ + 'header' => function (Worksheet $sheet, $row, $column) use ($customerMetaFields) { + foreach ($customerMetaFields as $metaField) { + $sheet->setCellValueByColumnAndRow($column++, $row, $this->translator->trans($metaField->getLabel())); + } + + return count($customerMetaFields); + }, + 'render' => function (Worksheet $sheet, int $row, int $column, ExportItemInterface $entity) use ($customerMetaFields) { + foreach ($customerMetaFields as $metaField) { + $metaFieldValue = ''; + if (null !== $entity->getProject()) { + $metaField = $entity->getProject()->getCustomer()->getMetaField($metaField->getName()); + if (null !== $metaField) { + $metaFieldValue = $metaField->getValue(); + } + } + $sheet->setCellValueByColumnAndRow($column++, $row, $metaFieldValue); + } + + return count($customerMetaFields); + } + ]; + } + + if (isset($columns['project-meta'])) { + $projectMetaFields = $this->findMetaColumns(new ProjectMetaDisplayEvent($query, ProjectMetaDisplayEvent::EXPORT)); + $columns['project-meta'] = [ + 'header' => function (Worksheet $sheet, $row, $column) use ($projectMetaFields) { + foreach ($projectMetaFields as $metaField) { + $sheet->setCellValueByColumnAndRow($column++, $row, $this->translator->trans($metaField->getLabel())); + } + + return count($projectMetaFields); + }, + 'render' => function (Worksheet $sheet, int $row, int $column, ExportItemInterface $entity) use ($projectMetaFields) { + foreach ($projectMetaFields as $metaField) { + $metaFieldValue = ''; + if (null !== $entity->getProject()) { + $metaField = $entity->getProject()->getMetaField($metaField->getName()); + if (null !== $metaField) { + $metaFieldValue = $metaField->getValue(); + } + } + $sheet->setCellValueByColumnAndRow($column++, $row, $metaFieldValue); + } + + return count($projectMetaFields); + } + ]; + } + + if (isset($columns['activity-meta'])) { + $activityMetaFields = $this->findMetaColumns(new ActivityMetaDisplayEvent($query, ActivityMetaDisplayEvent::EXPORT)); + $columns['activity-meta'] = [ + 'header' => function (Worksheet $sheet, $row, $column) use ($activityMetaFields) { + foreach ($activityMetaFields as $metaField) { + $sheet->setCellValueByColumnAndRow($column++, $row, $this->translator->trans($metaField->getLabel())); + } + + return count($activityMetaFields); + }, + 'render' => function (Worksheet $sheet, int $row, int $column, ExportItemInterface $entity) use ($activityMetaFields) { + foreach ($activityMetaFields as $metaField) { + $metaFieldValue = ''; + if (null !== $entity->getActivity()) { + $metaField = $entity->getActivity()->getMetaField($metaField->getName()); + if (null !== $metaField) { + $metaFieldValue = $metaField->getValue(); + } + } + $sheet->setCellValueByColumnAndRow($column++, $row, $metaFieldValue); + } + + return count($activityMetaFields); + } + ]; + } + + if (isset($columns['user-meta'])) { + $event = new UserPreferenceDisplayEvent(UserPreferenceDisplayEvent::EXPORT); + $this->dispatcher->dispatch($event); + $userPreferences = $event->getPreferences(); + $columns['user-meta'] = [ + 'header' => function (Worksheet $sheet, $row, $column) use ($userPreferences) { + foreach ($userPreferences as $metaField) { + $sheet->setCellValueByColumnAndRow($column++, $row, $this->translator->trans($metaField->getLabel())); + } + + return count($userPreferences); + }, + 'render' => function (Worksheet $sheet, int $row, int $column, ExportItemInterface $entity) use ($userPreferences) { + foreach ($userPreferences as $preference) { + $metaFieldValue = ''; + if (null !== $entity->getUser()) { + $metaField = $entity->getUser()->getPreference($preference->getName()); + if (null !== $metaField) { + $metaFieldValue = $metaField->getValue(); + } + } + $sheet->setCellValueByColumnAndRow($column++, $row, $metaFieldValue); + } + + return count($userPreferences); + } + ]; + } + + if (!$showRates) { + $removes = ['rate', 'fixedRate', 'hourlyRate']; + foreach ($removes as $removeMe) { + if (array_key_exists($removeMe, $columns)) { + unset($columns[$removeMe]); + } + } + } + + return $columns; + } + + /** + * @param ExportItemInterface[] $exportItems * @param TimesheetQuery $query * @return Spreadsheet * @throws \PhpOffice\PhpSpreadsheet\Exception */ - protected function fromArrayToSpreadsheet(array $timesheets, TimesheetQuery $query): Spreadsheet + protected function fromArrayToSpreadsheet(array $exportItems, TimesheetQuery $query): Spreadsheet { - $customerQuery = $query->copyTo(new CustomerQuery()); - - $timesheetMetaFields = $this->findMetaColumns(new TimesheetMetaDisplayEvent($query, TimesheetMetaDisplayEvent::EXPORT)); - $customerMetaFields = $this->findMetaColumns(new CustomerMetaDisplayEvent($customerQuery, CustomerMetaDisplayEvent::EXPORT)); - $projectMetaFields = $this->findMetaColumns(new ProjectMetaDisplayEvent($query, ProjectMetaDisplayEvent::EXPORT)); - $activityMetaFields = $this->findMetaColumns(new ActivityMetaDisplayEvent($query, ActivityMetaDisplayEvent::EXPORT)); - - $event = new UserPreferenceDisplayEvent(UserPreferenceDisplayEvent::EXPORT); - $this->dispatcher->dispatch($event); - $userPreferences = $event->getPreferences(); - $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); - $showRates = $this->isRenderRate($query); $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')); - if ($showRates) { - $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')); - $sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.activity')); - $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')); - if ($showRates) { - $sheet->setCellValueByColumnAndRow( - $recordsHeaderColumn++, - $recordsHeaderRow, - $this->translator->trans('label.hourlyRate') - ); - $sheet->setCellValueByColumnAndRow( - $recordsHeaderColumn++, - $recordsHeaderRow, - $this->translator->trans('label.fixedRate') - ); - } - foreach ($timesheetMetaFields as $metaField) { - $sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans($metaField->getLabel())); - } - foreach ($customerMetaFields as $metaField) { - $sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans($metaField->getLabel())); - } - foreach ($projectMetaFields as $metaField) { - $sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans($metaField->getLabel())); - } - foreach ($activityMetaFields as $metaField) { - $sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans($metaField->getLabel())); - } - foreach ($userPreferences as $preference) { - $sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans($preference->getLabel())); + $columns = $this->getColumns($exportItems, $query, $this->columns); + + foreach ($columns as $label => $settings) { + if (isset($settings['header'])) { + $amount = $settings['header']($sheet, $recordsHeaderRow, $recordsHeaderColumn); + $recordsHeaderColumn += $amount; + } else { + $sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.' . $label)); + } } $entryHeaderRow = $recordsHeaderRow + 1; @@ -228,111 +483,22 @@ abstract class AbstractSpreadsheetRenderer $durationColumn = null; $rateColumn = null; - foreach ($timesheets as $timesheet) { + foreach ($exportItems as $exportItem) { $entryHeaderColumn = 1; - $customerCurrency = $timesheet->getProject()->getCustomer()->getCurrency(); - $exported = $timesheet->isExported() ? 'entryState.exported' : 'entryState.not_exported'; - - $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++; - - if ($showRates) { - $this->setRate($sheet, $entryHeaderColumn, $entryHeaderRow, $timesheet->getRate(), $customerCurrency); - $rateColumn = $entryHeaderColumn; - $entryHeaderColumn++; - } - - $sheet->setCellValueByColumnAndRow($entryHeaderColumn, $entryHeaderRow, $timesheet->getUser()->getDisplayName()); - $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++; - - if ($showRates) { - $this->setRate( - $sheet, - $entryHeaderColumn, - $entryHeaderRow, - $timesheet->getHourlyRate(), - $customerCurrency - ); - $entryHeaderColumn++; - - $this->setRate( - $sheet, - $entryHeaderColumn, - $entryHeaderRow, - $timesheet->getFixedRate(), - $customerCurrency - ); - $entryHeaderColumn++; - } - - foreach ($timesheetMetaFields as $metaField) { - $metaField = $timesheet->getMetaField($metaField->getName()); - $metaFieldValue = ''; - if (null !== $metaField) { - $metaFieldValue = $metaField->getValue(); + foreach ($columns as $label => $settings) { + if ($label === 'duration') { + $durationColumn = $entryHeaderColumn; + } elseif ($label === 'rate') { + $rateColumn = $entryHeaderColumn; } - $sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $metaFieldValue); - } - foreach ($customerMetaFields as $metaField) { - $metaField = $timesheet->getProject()->getCustomer()->getMetaField($metaField->getName()); - $metaFieldValue = ''; - if (null !== $metaField) { - $metaFieldValue = $metaField->getValue(); + + if (!array_key_exists('render', $settings) || !is_callable($settings['render'])) { + throw new \RuntimeException(sprintf('Missing renderer for export column %s', $label)); } - $sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $metaFieldValue); - } - foreach ($projectMetaFields as $metaField) { - $metaField = $timesheet->getProject()->getMetaField($metaField->getName()); - $metaFieldValue = ''; - if (null !== $metaField) { - $metaFieldValue = $metaField->getValue(); - } - $sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $metaFieldValue); - } - foreach ($activityMetaFields as $metaField) { - $metaField = $timesheet->getActivity()->getMetaField($metaField->getName()); - $metaFieldValue = ''; - if (null !== $metaField) { - $metaFieldValue = $metaField->getValue(); - } - $sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $metaFieldValue); - } - foreach ($userPreferences as $preference) { - $metaField = $timesheet->getUser()->getPreference($preference->getName()); - $metaFieldValue = ''; - if (null !== $metaField) { - $metaFieldValue = $metaField->getValue(); - } - $sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $metaFieldValue); + + $amount = $settings['render']($sheet, $entryHeaderRow, $entryHeaderColumn, $exportItem); + $entryHeaderColumn += (null === $amount) ? 1 : $amount; } $entryHeaderRow++; @@ -360,15 +526,15 @@ abstract class AbstractSpreadsheetRenderer } /** - * @param Timesheet[] $timesheets + * @param ExportItemInterface[] $exportItems * @param TimesheetQuery $query * @return Response * @throws \PhpOffice\PhpSpreadsheet\Exception * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception */ - public function render(array $timesheets, TimesheetQuery $query): Response + public function render(array $exportItems, TimesheetQuery $query): Response { - $spreadsheet = $this->fromArrayToSpreadsheet($timesheets, $query); + $spreadsheet = $this->fromArrayToSpreadsheet($exportItems, $query); $filename = $this->saveSpreadsheet($spreadsheet); return $this->getFileResponse($filename, 'kimai-export' . $this->getFileExtension()); diff --git a/src/Export/Base/HtmlRenderer.php b/src/Export/Base/HtmlRenderer.php index 602f239d..9cc646d5 100644 --- a/src/Export/Base/HtmlRenderer.php +++ b/src/Export/Base/HtmlRenderer.php @@ -10,13 +10,13 @@ namespace App\Export\Base; use App\Entity\MetaTableTypeInterface; -use App\Entity\Timesheet; use App\Event\ActivityMetaDisplayEvent; use App\Event\CustomerMetaDisplayEvent; use App\Event\MetaDisplayEventInterface; use App\Event\ProjectMetaDisplayEvent; use App\Event\TimesheetMetaDisplayEvent; use App\Event\UserPreferenceDisplayEvent; +use App\Export\ExportItemInterface; use App\Repository\Query\CustomerQuery; use App\Repository\Query\TimesheetQuery; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -54,7 +54,7 @@ class HtmlRenderer } /** - * @param Timesheet[] $timesheets + * @param ExportItemInterface[] $timesheets * @param TimesheetQuery $query * @return Response * @throws \Twig\Error\LoaderError @@ -63,6 +63,7 @@ class HtmlRenderer */ public function render(array $timesheets, TimesheetQuery $query): Response { + /** @var CustomerQuery $customerQuery */ $customerQuery = $query->copyTo(new CustomerQuery()); $timesheetMetaFields = $this->findMetaColumns(new TimesheetMetaDisplayEvent($query, TimesheetMetaDisplayEvent::EXPORT)); diff --git a/src/Export/Base/PDFRenderer.php b/src/Export/Base/PDFRenderer.php index 11d5cd30..49af2bbb 100644 --- a/src/Export/Base/PDFRenderer.php +++ b/src/Export/Base/PDFRenderer.php @@ -9,7 +9,7 @@ namespace App\Export\Base; -use App\Entity\Timesheet; +use App\Export\ExportItemInterface; use App\Repository\Query\TimesheetQuery; use App\Timesheet\UserDateTimeFactory; use App\Utils\HtmlToPdfConverter; @@ -47,7 +47,7 @@ class PDFRenderer } /** - * @param Timesheet[] $timesheets + * @param ExportItemInterface[] $timesheets * @param TimesheetQuery $query * @return Response * @throws \Twig\Error\LoaderError diff --git a/src/Export/Base/RendererTrait.php b/src/Export/Base/RendererTrait.php index 858973fc..91dd446b 100644 --- a/src/Export/Base/RendererTrait.php +++ b/src/Export/Base/RendererTrait.php @@ -9,28 +9,48 @@ namespace App\Export\Base; -use App\Entity\Timesheet; +use App\Export\ExportItemInterface; trait RendererTrait { /** - * @param Timesheet[] $timesheets + * @param ExportItemInterface[] $exportItems * @return array */ - protected function calculateSummary(array $timesheets) + protected function calculateSummary(array $exportItems) { $summary = []; - foreach ($timesheets as $timesheet) { - $id = $timesheet->getProject()->getCustomer()->getId() . '_' . $timesheet->getProject()->getId(); - $activityId = $timesheet->getActivity()->getId(); + foreach ($exportItems as $exportItem) { + $customerId = 'none'; + $customerName = ''; + $currency = null; + $projectId = 'none'; + $projectName = ''; + $activityId = 'none'; + $activityName = ''; + + if (null !== $exportItem->getProject()) { + $customerId = $exportItem->getProject()->getCustomer()->getId(); + $customerName = $exportItem->getProject()->getCustomer()->getName(); + $projectId = $exportItem->getProject()->getId(); + $projectName = $exportItem->getProject()->getName(); + $currency = $exportItem->getProject()->getCustomer()->getCurrency(); + } + + if (null !== $exportItem->getActivity()) { + $activityId = $exportItem->getActivity()->getId(); + $activityName = $exportItem->getActivity()->getName(); + } + + $id = $customerId . '_' . $projectId; if (!isset($summary[$id])) { $summary[$id] = [ - 'customer' => $timesheet->getProject()->getCustomer()->getName(), - 'project' => $timesheet->getProject()->getName(), + 'customer' => $customerName, + 'project' => $projectName, 'activities' => [], - 'currency' => $timesheet->getProject()->getCustomer()->getCurrency(), + 'currency' => $currency, 'rate' => 0, 'duration' => 0, ]; @@ -38,21 +58,21 @@ trait RendererTrait if (!isset($summary[$id]['activities'][$activityId])) { $summary[$id]['activities'][$activityId] = [ - 'activity' => $timesheet->getActivity()->getName(), - 'currency' => $timesheet->getProject()->getCustomer()->getCurrency(), + 'activity' => $activityName, + 'currency' => $currency, 'rate' => 0, 'duration' => 0, ]; } - $duration = $timesheet->getDuration(); + $duration = $exportItem->getDuration(); if (null === $duration) { $duration = 0; } - $summary[$id]['rate'] += $timesheet->getRate(); + $summary[$id]['rate'] += $exportItem->getRate(); $summary[$id]['duration'] += $duration; - $summary[$id]['activities'][$activityId]['rate'] += $timesheet->getRate(); + $summary[$id]['activities'][$activityId]['rate'] += $exportItem->getRate(); $summary[$id]['activities'][$activityId]['duration'] += $duration; } diff --git a/src/Export/ExportItemInterface.php b/src/Export/ExportItemInterface.php new file mode 100644 index 00000000..e5c59d31 --- /dev/null +++ b/src/Export/ExportItemInterface.php @@ -0,0 +1,52 @@ +getUser()) { - return $this->voter->isGranted('view_rate_own_timesheet'); - } - - return true; - } } diff --git a/src/Export/Timesheet/XlsxRenderer.php b/src/Export/Timesheet/XlsxRenderer.php index fac82227..c4d57e9f 100644 --- a/src/Export/Timesheet/XlsxRenderer.php +++ b/src/Export/Timesheet/XlsxRenderer.php @@ -11,12 +11,7 @@ namespace App\Export\Timesheet; use App\Export\Base\XlsxRenderer as BaseXlsxRenderer; use App\Export\TimesheetExportInterface; -use App\Repository\Query\TimesheetQuery; final class XlsxRenderer extends BaseXlsxRenderer implements TimesheetExportInterface { - protected function isRenderRate(TimesheetQuery $query): bool - { - return false; - } } diff --git a/src/Invoice/Calculator/AbstractMergedCalculator.php b/src/Invoice/Calculator/AbstractMergedCalculator.php index 6e203b8b..4f7e2748 100644 --- a/src/Invoice/Calculator/AbstractMergedCalculator.php +++ b/src/Invoice/Calculator/AbstractMergedCalculator.php @@ -10,13 +10,16 @@ namespace App\Invoice\Calculator; use App\Entity\Timesheet; +use App\Export\ExportItemInterface; use App\Invoice\InvoiceItem; use App\Invoice\InvoiceItemInterface; use App\Invoice\InvoiceItemWithAmountInterface; -use App\Invoice\InvoiceItemWithTypeInterface; abstract class AbstractMergedCalculator extends AbstractCalculator { + public const TYPE_MIXED = 'mixed'; + public const CATEGORY_MIXED = 'mixed'; + /** * @deprecated since 1.3 - will be removed with 2.0 */ @@ -39,19 +42,19 @@ abstract class AbstractMergedCalculator extends AbstractCalculator $amount = $entry->getAmount(); } - if ($entry instanceof InvoiceItemWithTypeInterface) { - $type = $entry->getInvoiceType(); - $category = $entry->getInvoiceCategory(); - } else { - $type = InvoiceItem::TYPE_TIMESHEET; - $category = InvoiceItem::CATEGORY_TIMESHEET_WORK; + $type = Timesheet::TYPE_TIMESHEET; + $category = Timesheet::CATEGORY_WORK; + + if ($entry instanceof ExportItemInterface) { + $type = $entry->getType(); + $category = $entry->getCategory(); } if (null !== $invoiceItem->getType() && $type !== $invoiceItem->getType()) { - $type = InvoiceItem::TYPE_MIXED; + $type = self::TYPE_MIXED; } if (null !== $invoiceItem->getCategory() && $category !== $invoiceItem->getCategory()) { - $category = InvoiceItem::CATEGORY_MIXED; + $category = self::CATEGORY_MIXED; } $invoiceItem->setType($type); diff --git a/src/Invoice/InvoiceItem.php b/src/Invoice/InvoiceItem.php index c250cc12..c44067f4 100644 --- a/src/Invoice/InvoiceItem.php +++ b/src/Invoice/InvoiceItem.php @@ -18,11 +18,6 @@ use App\Entity\User; */ final class InvoiceItem { - public const TYPE_TIMESHEET = 'timesheet'; - public const CATEGORY_TIMESHEET_WORK = 'work'; - public const TYPE_MIXED = 'mixed'; - public const CATEGORY_MIXED = 'mixed'; - /** * @var float */ diff --git a/src/Invoice/InvoiceItemInterface.php b/src/Invoice/InvoiceItemInterface.php index 5c98aa1d..2382dd1d 100644 --- a/src/Invoice/InvoiceItemInterface.php +++ b/src/Invoice/InvoiceItemInterface.php @@ -14,6 +14,9 @@ use App\Entity\MetaTableTypeInterface; use App\Entity\Project; use App\Entity\User; +/** + * @deprecated will be removed with 2.0 - use ExportItemInterface instead + */ interface InvoiceItemInterface { public function getActivity(): ?Activity; diff --git a/src/Invoice/InvoiceItemWithTypeInterface.php b/src/Invoice/InvoiceItemWithTypeInterface.php deleted file mode 100644 index 4cc6ffb8..00000000 --- a/src/Invoice/InvoiceItemWithTypeInterface.php +++ /dev/null @@ -1,17 +0,0 @@ -{{ entry.project.name }} - {{ entry.activity.name }} + {% if entry.activity is not null %} + {{ entry.activity.name }} + {% endif %} {% if entry.description is not empty %} @@ -327,7 +329,9 @@ {% endfor %} {% for id, field in activityMetaFields %} - {{ tables.datatable_meta_column(entry.activity, field) }} + {% if entry.activity is not null %} + {{ tables.datatable_meta_column(entry.activity, field) }} + {% endif %} {% endfor %} {% for id, field in userPreferences %} diff --git a/templates/export/renderer/pdf.html.twig b/templates/export/renderer/pdf.html.twig index a9b49c3a..c0a1a4e6 100644 --- a/templates/export/renderer/pdf.html.twig +++ b/templates/export/renderer/pdf.html.twig @@ -182,7 +182,7 @@ mpdf--> {{ entry.user.displayName }} {% endif %} - {{ entry.project.customer.name }} - {{ entry.project.name }} - {{ entry.activity.name }} + {{ entry.project.customer.name }} - {{ entry.project.name }}{% if entry.activity is not null %} - {{ entry.activity.name }}{% endif %} {% if entry.description is not empty %}
{{ entry.description|escape|desc2html }} @@ -209,7 +209,7 @@ mpdf--> {% endif %} {{ duration|duration }} {% if showRateColumn %} - {{ rate|money(currency) }} + {% if currency is not null %}{{ rate|money(currency) }}{% endif %} {% endif %} diff --git a/templates/timesheet-team/index.html.twig b/templates/timesheet-team/index.html.twig index 011e0941..c0a15868 100644 --- a/templates/timesheet-team/index.html.twig +++ b/templates/timesheet-team/index.html.twig @@ -4,7 +4,7 @@ {% set tableName = 'timesheet_admin' %} {% set paginationRoute = 'admin_timesheet_paginated' %} {% set editRoute = 'admin_timesheet_edit' %} -{% set canSeeRate = true %} +{% set canSeeRate = is_granted('view_rate_other_timesheet') %} {% set canSeeUsername = true %} {% set allowMarkdown = false %} diff --git a/templates/timesheet/export.html.twig b/templates/timesheet/export.html.twig index d1e7192a..35319b96 100644 --- a/templates/timesheet/export.html.twig +++ b/templates/timesheet/export.html.twig @@ -12,23 +12,23 @@
- {% if not showUserColumn %} -

- {{ 'label.user'|trans }}: {{ widgets.username(query.user) }} -

- {% endif %}
@@ -43,7 +43,7 @@ {% endif %} {{ 'label.description'|trans }} {% for field in metaColumns %} - {{ field.label|trans }} + {{ field.label|trans }} {% endfor %} {{ 'label.hours'|trans }} @@ -64,7 +64,7 @@ {% endif %} - {{ 'label.activity'|trans }}: {{ entry.activity.name }} | + {% if entry.activity is not null %}{{ 'label.activity'|trans }}: {{ entry.activity.name }} |{% endif %} {{ 'label.project'|trans }}: {{ entry.project.name }} | {{ 'label.customer'|trans }}: {{ entry.project.customer.name }} @@ -86,7 +86,7 @@ {% endfor %} {{ 'invoice.total_working_time'|trans }} - {{ timeWorked|duration }} + {{ timeWorked|duration }} diff --git a/tests/Export/Timesheet/HtmlRendererTest.php b/tests/Export/Timesheet/HtmlRendererTest.php index d5ff0129..11d51f4e 100644 --- a/tests/Export/Timesheet/HtmlRendererTest.php +++ b/tests/Export/Timesheet/HtmlRendererTest.php @@ -46,6 +46,6 @@ class HtmlRendererTest extends AbstractRendererTest $content = $response->getContent(); - $this->assertStringContainsString('01:50 h', $content); + $this->assertStringContainsString('>01:50 h<', $content); } }