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 }}
- {{ 'label.user'|trans }}: {{ widgets.username(query.user) }} -
- {% endif %}