re-written exporter for simpler extendability (#1349)
This commit is contained in:
10
UPGRADING.md
10
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)
|
||||
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
52
src/Export/ExportItemInterface.php
Normal file
52
src/Export/ExportItemInterface.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export;
|
||||
|
||||
use App\Entity\MetaTableTypeInterface;
|
||||
use App\Invoice\InvoiceItemInterface;
|
||||
|
||||
interface ExportItemInterface extends InvoiceItemInterface
|
||||
{
|
||||
/**
|
||||
* A name representation for this type of export.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string;
|
||||
|
||||
/**
|
||||
* A name representation for the category of this type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCategory(): string;
|
||||
|
||||
/**
|
||||
* Whether this item was already exported.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isExported(): bool;
|
||||
|
||||
/**
|
||||
* Returns the named meta field or null.
|
||||
*
|
||||
* @param string $name
|
||||
* @return MetaTableTypeInterface|null
|
||||
*/
|
||||
public function getMetaField(string $name): ?MetaTableTypeInterface;
|
||||
|
||||
/**
|
||||
* Returns all assigned tag names.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getTagsAsArray(): array;
|
||||
}
|
||||
@@ -9,18 +9,17 @@
|
||||
|
||||
namespace App\Export;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
interface RendererInterface
|
||||
{
|
||||
/**
|
||||
* @param Timesheet[] $timesheets
|
||||
* @param ExportItemInterface[] $exportItems
|
||||
* @param TimesheetQuery $query
|
||||
* @return Response
|
||||
*/
|
||||
public function render(array $timesheets, TimesheetQuery $query): Response;
|
||||
public function render(array $exportItems, TimesheetQuery $query): Response;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
|
||||
@@ -11,16 +11,7 @@ namespace App\Export\Timesheet;
|
||||
|
||||
use App\Export\Base\CsvRenderer as BaseCsvRenderer;
|
||||
use App\Export\TimesheetExportInterface;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
|
||||
final class CsvRenderer extends BaseCsvRenderer implements TimesheetExportInterface
|
||||
{
|
||||
protected function isRenderRate(TimesheetQuery $query): bool
|
||||
{
|
||||
if (null !== $query->getUser()) {
|
||||
return $this->voter->isGranted('view_rate_own_timesheet');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Invoice;
|
||||
|
||||
interface InvoiceItemWithTypeInterface
|
||||
{
|
||||
public function getInvoiceType(): string;
|
||||
|
||||
public function getInvoiceCategory(): string;
|
||||
}
|
||||
@@ -91,6 +91,8 @@ class TimesheetQuery extends ActivityQuery
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit the data exclusively to the user (eg. users own timesheets).
|
||||
*
|
||||
* @return User|null
|
||||
*/
|
||||
public function getUser()
|
||||
@@ -99,6 +101,8 @@ class TimesheetQuery extends ActivityQuery
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit the data exclusively to the user (eg. users own timesheets).
|
||||
*
|
||||
* @param User|int|null $user
|
||||
* @return TimesheetQuery
|
||||
*/
|
||||
|
||||
@@ -291,7 +291,9 @@
|
||||
<td class="column-project" {% if not columns.project %}style="display: none"{% endif %}>{{ entry.project.name }}
|
||||
</td>
|
||||
<td class="column-activity" {% if not columns.activity %}style="display: none"{% endif %}>
|
||||
{{ entry.activity.name }}
|
||||
{% if entry.activity is not null %}
|
||||
{{ entry.activity.name }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="column-description" {% if not columns.description %}style="display: none"{% endif %}>
|
||||
{% if entry.description is not empty %}
|
||||
@@ -327,7 +329,9 @@
|
||||
{% endfor %}
|
||||
{% for id, field in activityMetaFields %}
|
||||
<td class="column-{{ 'a_' ~ field.name }} text-nowrap" {% if not columns['a_' ~ field.name] %}style="display: none"{% endif %}>
|
||||
{{ tables.datatable_meta_column(entry.activity, field) }}
|
||||
{% if entry.activity is not null %}
|
||||
{{ tables.datatable_meta_column(entry.activity, field) }}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
{% for id, field in userPreferences %}
|
||||
|
||||
@@ -182,7 +182,7 @@ mpdf-->
|
||||
<td>{{ entry.user.displayName }}</td>
|
||||
{% endif %}
|
||||
<td>
|
||||
{{ 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 %}
|
||||
<br>
|
||||
<i>{{ entry.description|escape|desc2html }}</i>
|
||||
@@ -209,7 +209,7 @@ mpdf-->
|
||||
{% endif %}
|
||||
<td class="totals duration">{{ duration|duration }}</td>
|
||||
{% if showRateColumn %}
|
||||
<td class="totals cost">{{ rate|money(currency) }}</td>
|
||||
<td class="totals cost">{% if currency is not null %}{{ rate|money(currency) }}{% endif %}</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
|
||||
@@ -12,23 +12,23 @@
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<h2 class="page-header">
|
||||
<span contenteditable="true">
|
||||
{% if query.begin is not empty and query.end is not empty %}
|
||||
{% if query.begin|date('m') != query.end|date('m') or query.begin|date('Y') != query.end|date('Y') %}
|
||||
{{ query.begin|date_short }} - {{ query.end|date_short }}
|
||||
{% elseif query.end is not empty %}
|
||||
{{ query.end|month_name|trans }} {{ query.end|date('Y') }}
|
||||
{% elseif query.begin is not empty %}
|
||||
{{ query.begin|month_name|trans }} {{ query.begin|date('Y') }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</span>
|
||||
{% if not showUserColumn %}
|
||||
{{ widgets.username(query.user) }}:
|
||||
{% endif %}
|
||||
{% if query.begin is not empty %}
|
||||
{{ query.begin|date_short }}
|
||||
{% else %}
|
||||
{% set last = entries|last %}
|
||||
{{ last.begin|date_short }}
|
||||
{% endif %}
|
||||
–
|
||||
{% if query.end is not empty %}
|
||||
{{ query.end|date_short }}
|
||||
{% else %}
|
||||
{% set first = entries|first %}
|
||||
{{ first.end|date_short }}
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% if not showUserColumn %}
|
||||
<p>
|
||||
{{ 'label.user'|trans }}: {{ widgets.username(query.user) }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
{% endif %}
|
||||
<th>{{ 'label.description'|trans }}</th>
|
||||
{% for field in metaColumns %}
|
||||
<th>{{ field.label|trans }}</th>
|
||||
<th class="text-nowrap">{{ field.label|trans }}</th>
|
||||
{% endfor %}
|
||||
<th>{{ 'label.hours'|trans }}</th>
|
||||
</tr>
|
||||
@@ -64,7 +64,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
<span class="small">
|
||||
{{ '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 }}
|
||||
</span>
|
||||
@@ -86,7 +86,7 @@
|
||||
<th></th>
|
||||
{% endfor %}
|
||||
<th>{{ 'invoice.total_working_time'|trans }}</th>
|
||||
<th>{{ timeWorked|duration }}</th>
|
||||
<th class="text-nowrap">{{ timeWorked|duration }}</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
@@ -46,6 +46,6 @@ class HtmlRendererTest extends AbstractRendererTest
|
||||
|
||||
$content = $response->getContent();
|
||||
|
||||
$this->assertStringContainsString('<th>01:50 h</th>', $content);
|
||||
$this->assertStringContainsString('>01:50 h<', $content);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user