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).**
|
**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
|
### 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.
|
- 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)
|
- 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)
|
## [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;
|
namespace App\Entity;
|
||||||
|
|
||||||
use App\Invoice\InvoiceItemInterface;
|
use App\Export\ExportItemInterface;
|
||||||
use DateTime;
|
use DateTime;
|
||||||
use DateTimeZone;
|
use DateTimeZone;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
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"} => IDX_4F60C6B1502DF58741561401 => ???
|
||||||
* columns={"start_time","end_time","user"} => IDX_4F60C6B1502DF587415614018D93D649 => ???
|
* 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
|
* @var int
|
||||||
*
|
*
|
||||||
@@ -385,7 +388,7 @@ class Timesheet implements EntityWithMetaFields, InvoiceItemInterface
|
|||||||
/**
|
/**
|
||||||
* @return string[]
|
* @return string[]
|
||||||
*/
|
*/
|
||||||
public function getTagsAsArray()
|
public function getTagsAsArray(): array
|
||||||
{
|
{
|
||||||
return array_map(
|
return array_map(
|
||||||
function (Tag $element) {
|
function (Tag $element) {
|
||||||
@@ -437,6 +440,18 @@ class Timesheet implements EntityWithMetaFields, InvoiceItemInterface
|
|||||||
return $this;
|
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
|
* @internal only here for symfony forms
|
||||||
* @return Collection|MetaTableTypeInterface[]
|
* @return Collection|MetaTableTypeInterface[]
|
||||||
|
|||||||
@@ -10,13 +10,13 @@
|
|||||||
namespace App\Export\Base;
|
namespace App\Export\Base;
|
||||||
|
|
||||||
use App\Entity\MetaTableTypeInterface;
|
use App\Entity\MetaTableTypeInterface;
|
||||||
use App\Entity\Timesheet;
|
|
||||||
use App\Event\ActivityMetaDisplayEvent;
|
use App\Event\ActivityMetaDisplayEvent;
|
||||||
use App\Event\CustomerMetaDisplayEvent;
|
use App\Event\CustomerMetaDisplayEvent;
|
||||||
use App\Event\MetaDisplayEventInterface;
|
use App\Event\MetaDisplayEventInterface;
|
||||||
use App\Event\ProjectMetaDisplayEvent;
|
use App\Event\ProjectMetaDisplayEvent;
|
||||||
use App\Event\TimesheetMetaDisplayEvent;
|
use App\Event\TimesheetMetaDisplayEvent;
|
||||||
use App\Event\UserPreferenceDisplayEvent;
|
use App\Event\UserPreferenceDisplayEvent;
|
||||||
|
use App\Export\ExportItemInterface;
|
||||||
use App\Repository\Query\CustomerQuery;
|
use App\Repository\Query\CustomerQuery;
|
||||||
use App\Repository\Query\TimesheetQuery;
|
use App\Repository\Query\TimesheetQuery;
|
||||||
use App\Twig\DateExtensions;
|
use App\Twig\DateExtensions;
|
||||||
@@ -61,6 +61,30 @@ abstract class AbstractSpreadsheetRenderer
|
|||||||
* @var AuthorizationCheckerInterface
|
* @var AuthorizationCheckerInterface
|
||||||
*/
|
*/
|
||||||
protected $voter;
|
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)
|
public function __construct(TranslatorInterface $translator, DateExtensions $dateExtension, EventDispatcherInterface $dispatcher, AuthorizationCheckerInterface $voter)
|
||||||
{
|
{
|
||||||
@@ -72,7 +96,11 @@ abstract class AbstractSpreadsheetRenderer
|
|||||||
|
|
||||||
protected function isRenderRate(TimesheetQuery $query): bool
|
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)
|
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
|
* @param TimesheetQuery $query
|
||||||
* @return Spreadsheet
|
* @return Spreadsheet
|
||||||
* @throws \PhpOffice\PhpSpreadsheet\Exception
|
* @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();
|
$spreadsheet = new Spreadsheet();
|
||||||
$sheet = $spreadsheet->getActiveSheet();
|
$sheet = $spreadsheet->getActiveSheet();
|
||||||
|
|
||||||
$showRates = $this->isRenderRate($query);
|
|
||||||
$recordsHeaderColumn = 1;
|
$recordsHeaderColumn = 1;
|
||||||
$recordsHeaderRow = 1;
|
$recordsHeaderRow = 1;
|
||||||
|
|
||||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.date'));
|
$columns = $this->getColumns($exportItems, $query, $this->columns);
|
||||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.begin'));
|
|
||||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.end'));
|
foreach ($columns as $label => $settings) {
|
||||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.duration'));
|
if (isset($settings['header'])) {
|
||||||
if ($showRates) {
|
$amount = $settings['header']($sheet, $recordsHeaderRow, $recordsHeaderColumn);
|
||||||
$sheet->setCellValueByColumnAndRow(
|
$recordsHeaderColumn += $amount;
|
||||||
$recordsHeaderColumn++,
|
} else {
|
||||||
$recordsHeaderRow,
|
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.' . $label));
|
||||||
$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()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$entryHeaderRow = $recordsHeaderRow + 1;
|
$entryHeaderRow = $recordsHeaderRow + 1;
|
||||||
@@ -228,111 +483,22 @@ abstract class AbstractSpreadsheetRenderer
|
|||||||
$durationColumn = null;
|
$durationColumn = null;
|
||||||
$rateColumn = null;
|
$rateColumn = null;
|
||||||
|
|
||||||
foreach ($timesheets as $timesheet) {
|
foreach ($exportItems as $exportItem) {
|
||||||
$entryHeaderColumn = 1;
|
$entryHeaderColumn = 1;
|
||||||
|
|
||||||
$customerCurrency = $timesheet->getProject()->getCustomer()->getCurrency();
|
foreach ($columns as $label => $settings) {
|
||||||
$exported = $timesheet->isExported() ? 'entryState.exported' : 'entryState.not_exported';
|
if ($label === 'duration') {
|
||||||
|
$durationColumn = $entryHeaderColumn;
|
||||||
$this->setFormattedDate($sheet, $entryHeaderColumn, $entryHeaderRow, $timesheet->getBegin());
|
} elseif ($label === 'rate') {
|
||||||
$entryHeaderColumn++;
|
$rateColumn = $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();
|
|
||||||
}
|
}
|
||||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $metaFieldValue);
|
|
||||||
}
|
if (!array_key_exists('render', $settings) || !is_callable($settings['render'])) {
|
||||||
foreach ($customerMetaFields as $metaField) {
|
throw new \RuntimeException(sprintf('Missing renderer for export column %s', $label));
|
||||||
$metaField = $timesheet->getProject()->getCustomer()->getMetaField($metaField->getName());
|
|
||||||
$metaFieldValue = '';
|
|
||||||
if (null !== $metaField) {
|
|
||||||
$metaFieldValue = $metaField->getValue();
|
|
||||||
}
|
}
|
||||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $metaFieldValue);
|
|
||||||
}
|
$amount = $settings['render']($sheet, $entryHeaderRow, $entryHeaderColumn, $exportItem);
|
||||||
foreach ($projectMetaFields as $metaField) {
|
$entryHeaderColumn += (null === $amount) ? 1 : $amount;
|
||||||
$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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$entryHeaderRow++;
|
$entryHeaderRow++;
|
||||||
@@ -360,15 +526,15 @@ abstract class AbstractSpreadsheetRenderer
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Timesheet[] $timesheets
|
* @param ExportItemInterface[] $exportItems
|
||||||
* @param TimesheetQuery $query
|
* @param TimesheetQuery $query
|
||||||
* @return Response
|
* @return Response
|
||||||
* @throws \PhpOffice\PhpSpreadsheet\Exception
|
* @throws \PhpOffice\PhpSpreadsheet\Exception
|
||||||
* @throws \PhpOffice\PhpSpreadsheet\Writer\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);
|
$filename = $this->saveSpreadsheet($spreadsheet);
|
||||||
|
|
||||||
return $this->getFileResponse($filename, 'kimai-export' . $this->getFileExtension());
|
return $this->getFileResponse($filename, 'kimai-export' . $this->getFileExtension());
|
||||||
|
|||||||
@@ -10,13 +10,13 @@
|
|||||||
namespace App\Export\Base;
|
namespace App\Export\Base;
|
||||||
|
|
||||||
use App\Entity\MetaTableTypeInterface;
|
use App\Entity\MetaTableTypeInterface;
|
||||||
use App\Entity\Timesheet;
|
|
||||||
use App\Event\ActivityMetaDisplayEvent;
|
use App\Event\ActivityMetaDisplayEvent;
|
||||||
use App\Event\CustomerMetaDisplayEvent;
|
use App\Event\CustomerMetaDisplayEvent;
|
||||||
use App\Event\MetaDisplayEventInterface;
|
use App\Event\MetaDisplayEventInterface;
|
||||||
use App\Event\ProjectMetaDisplayEvent;
|
use App\Event\ProjectMetaDisplayEvent;
|
||||||
use App\Event\TimesheetMetaDisplayEvent;
|
use App\Event\TimesheetMetaDisplayEvent;
|
||||||
use App\Event\UserPreferenceDisplayEvent;
|
use App\Event\UserPreferenceDisplayEvent;
|
||||||
|
use App\Export\ExportItemInterface;
|
||||||
use App\Repository\Query\CustomerQuery;
|
use App\Repository\Query\CustomerQuery;
|
||||||
use App\Repository\Query\TimesheetQuery;
|
use App\Repository\Query\TimesheetQuery;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
@@ -54,7 +54,7 @@ class HtmlRenderer
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Timesheet[] $timesheets
|
* @param ExportItemInterface[] $timesheets
|
||||||
* @param TimesheetQuery $query
|
* @param TimesheetQuery $query
|
||||||
* @return Response
|
* @return Response
|
||||||
* @throws \Twig\Error\LoaderError
|
* @throws \Twig\Error\LoaderError
|
||||||
@@ -63,6 +63,7 @@ class HtmlRenderer
|
|||||||
*/
|
*/
|
||||||
public function render(array $timesheets, TimesheetQuery $query): Response
|
public function render(array $timesheets, TimesheetQuery $query): Response
|
||||||
{
|
{
|
||||||
|
/** @var CustomerQuery $customerQuery */
|
||||||
$customerQuery = $query->copyTo(new CustomerQuery());
|
$customerQuery = $query->copyTo(new CustomerQuery());
|
||||||
|
|
||||||
$timesheetMetaFields = $this->findMetaColumns(new TimesheetMetaDisplayEvent($query, TimesheetMetaDisplayEvent::EXPORT));
|
$timesheetMetaFields = $this->findMetaColumns(new TimesheetMetaDisplayEvent($query, TimesheetMetaDisplayEvent::EXPORT));
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
namespace App\Export\Base;
|
namespace App\Export\Base;
|
||||||
|
|
||||||
use App\Entity\Timesheet;
|
use App\Export\ExportItemInterface;
|
||||||
use App\Repository\Query\TimesheetQuery;
|
use App\Repository\Query\TimesheetQuery;
|
||||||
use App\Timesheet\UserDateTimeFactory;
|
use App\Timesheet\UserDateTimeFactory;
|
||||||
use App\Utils\HtmlToPdfConverter;
|
use App\Utils\HtmlToPdfConverter;
|
||||||
@@ -47,7 +47,7 @@ class PDFRenderer
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Timesheet[] $timesheets
|
* @param ExportItemInterface[] $timesheets
|
||||||
* @param TimesheetQuery $query
|
* @param TimesheetQuery $query
|
||||||
* @return Response
|
* @return Response
|
||||||
* @throws \Twig\Error\LoaderError
|
* @throws \Twig\Error\LoaderError
|
||||||
|
|||||||
@@ -9,28 +9,48 @@
|
|||||||
|
|
||||||
namespace App\Export\Base;
|
namespace App\Export\Base;
|
||||||
|
|
||||||
use App\Entity\Timesheet;
|
use App\Export\ExportItemInterface;
|
||||||
|
|
||||||
trait RendererTrait
|
trait RendererTrait
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @param Timesheet[] $timesheets
|
* @param ExportItemInterface[] $exportItems
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
protected function calculateSummary(array $timesheets)
|
protected function calculateSummary(array $exportItems)
|
||||||
{
|
{
|
||||||
$summary = [];
|
$summary = [];
|
||||||
|
|
||||||
foreach ($timesheets as $timesheet) {
|
foreach ($exportItems as $exportItem) {
|
||||||
$id = $timesheet->getProject()->getCustomer()->getId() . '_' . $timesheet->getProject()->getId();
|
$customerId = 'none';
|
||||||
$activityId = $timesheet->getActivity()->getId();
|
$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])) {
|
if (!isset($summary[$id])) {
|
||||||
$summary[$id] = [
|
$summary[$id] = [
|
||||||
'customer' => $timesheet->getProject()->getCustomer()->getName(),
|
'customer' => $customerName,
|
||||||
'project' => $timesheet->getProject()->getName(),
|
'project' => $projectName,
|
||||||
'activities' => [],
|
'activities' => [],
|
||||||
'currency' => $timesheet->getProject()->getCustomer()->getCurrency(),
|
'currency' => $currency,
|
||||||
'rate' => 0,
|
'rate' => 0,
|
||||||
'duration' => 0,
|
'duration' => 0,
|
||||||
];
|
];
|
||||||
@@ -38,21 +58,21 @@ trait RendererTrait
|
|||||||
|
|
||||||
if (!isset($summary[$id]['activities'][$activityId])) {
|
if (!isset($summary[$id]['activities'][$activityId])) {
|
||||||
$summary[$id]['activities'][$activityId] = [
|
$summary[$id]['activities'][$activityId] = [
|
||||||
'activity' => $timesheet->getActivity()->getName(),
|
'activity' => $activityName,
|
||||||
'currency' => $timesheet->getProject()->getCustomer()->getCurrency(),
|
'currency' => $currency,
|
||||||
'rate' => 0,
|
'rate' => 0,
|
||||||
'duration' => 0,
|
'duration' => 0,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
$duration = $timesheet->getDuration();
|
$duration = $exportItem->getDuration();
|
||||||
if (null === $duration) {
|
if (null === $duration) {
|
||||||
$duration = 0;
|
$duration = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
$summary[$id]['rate'] += $timesheet->getRate();
|
$summary[$id]['rate'] += $exportItem->getRate();
|
||||||
$summary[$id]['duration'] += $duration;
|
$summary[$id]['duration'] += $duration;
|
||||||
$summary[$id]['activities'][$activityId]['rate'] += $timesheet->getRate();
|
$summary[$id]['activities'][$activityId]['rate'] += $exportItem->getRate();
|
||||||
$summary[$id]['activities'][$activityId]['duration'] += $duration;
|
$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;
|
namespace App\Export;
|
||||||
|
|
||||||
use App\Entity\Timesheet;
|
|
||||||
use App\Repository\Query\TimesheetQuery;
|
use App\Repository\Query\TimesheetQuery;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
interface RendererInterface
|
interface RendererInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @param Timesheet[] $timesheets
|
* @param ExportItemInterface[] $exportItems
|
||||||
* @param TimesheetQuery $query
|
* @param TimesheetQuery $query
|
||||||
* @return Response
|
* @return Response
|
||||||
*/
|
*/
|
||||||
public function render(array $timesheets, TimesheetQuery $query): Response;
|
public function render(array $exportItems, TimesheetQuery $query): Response;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return string
|
* @return string
|
||||||
|
|||||||
@@ -11,16 +11,7 @@ namespace App\Export\Timesheet;
|
|||||||
|
|
||||||
use App\Export\Base\CsvRenderer as BaseCsvRenderer;
|
use App\Export\Base\CsvRenderer as BaseCsvRenderer;
|
||||||
use App\Export\TimesheetExportInterface;
|
use App\Export\TimesheetExportInterface;
|
||||||
use App\Repository\Query\TimesheetQuery;
|
|
||||||
|
|
||||||
final class CsvRenderer extends BaseCsvRenderer implements TimesheetExportInterface
|
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\Base\XlsxRenderer as BaseXlsxRenderer;
|
||||||
use App\Export\TimesheetExportInterface;
|
use App\Export\TimesheetExportInterface;
|
||||||
use App\Repository\Query\TimesheetQuery;
|
|
||||||
|
|
||||||
final class XlsxRenderer extends BaseXlsxRenderer implements TimesheetExportInterface
|
final class XlsxRenderer extends BaseXlsxRenderer implements TimesheetExportInterface
|
||||||
{
|
{
|
||||||
protected function isRenderRate(TimesheetQuery $query): bool
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,13 +10,16 @@
|
|||||||
namespace App\Invoice\Calculator;
|
namespace App\Invoice\Calculator;
|
||||||
|
|
||||||
use App\Entity\Timesheet;
|
use App\Entity\Timesheet;
|
||||||
|
use App\Export\ExportItemInterface;
|
||||||
use App\Invoice\InvoiceItem;
|
use App\Invoice\InvoiceItem;
|
||||||
use App\Invoice\InvoiceItemInterface;
|
use App\Invoice\InvoiceItemInterface;
|
||||||
use App\Invoice\InvoiceItemWithAmountInterface;
|
use App\Invoice\InvoiceItemWithAmountInterface;
|
||||||
use App\Invoice\InvoiceItemWithTypeInterface;
|
|
||||||
|
|
||||||
abstract class AbstractMergedCalculator extends AbstractCalculator
|
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
|
* @deprecated since 1.3 - will be removed with 2.0
|
||||||
*/
|
*/
|
||||||
@@ -39,19 +42,19 @@ abstract class AbstractMergedCalculator extends AbstractCalculator
|
|||||||
$amount = $entry->getAmount();
|
$amount = $entry->getAmount();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($entry instanceof InvoiceItemWithTypeInterface) {
|
$type = Timesheet::TYPE_TIMESHEET;
|
||||||
$type = $entry->getInvoiceType();
|
$category = Timesheet::CATEGORY_WORK;
|
||||||
$category = $entry->getInvoiceCategory();
|
|
||||||
} else {
|
if ($entry instanceof ExportItemInterface) {
|
||||||
$type = InvoiceItem::TYPE_TIMESHEET;
|
$type = $entry->getType();
|
||||||
$category = InvoiceItem::CATEGORY_TIMESHEET_WORK;
|
$category = $entry->getCategory();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (null !== $invoiceItem->getType() && $type !== $invoiceItem->getType()) {
|
if (null !== $invoiceItem->getType() && $type !== $invoiceItem->getType()) {
|
||||||
$type = InvoiceItem::TYPE_MIXED;
|
$type = self::TYPE_MIXED;
|
||||||
}
|
}
|
||||||
if (null !== $invoiceItem->getCategory() && $category !== $invoiceItem->getCategory()) {
|
if (null !== $invoiceItem->getCategory() && $category !== $invoiceItem->getCategory()) {
|
||||||
$category = InvoiceItem::CATEGORY_MIXED;
|
$category = self::CATEGORY_MIXED;
|
||||||
}
|
}
|
||||||
|
|
||||||
$invoiceItem->setType($type);
|
$invoiceItem->setType($type);
|
||||||
|
|||||||
@@ -18,11 +18,6 @@ use App\Entity\User;
|
|||||||
*/
|
*/
|
||||||
final class InvoiceItem
|
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
|
* @var float
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ use App\Entity\MetaTableTypeInterface;
|
|||||||
use App\Entity\Project;
|
use App\Entity\Project;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated will be removed with 2.0 - use ExportItemInterface instead
|
||||||
|
*/
|
||||||
interface InvoiceItemInterface
|
interface InvoiceItemInterface
|
||||||
{
|
{
|
||||||
public function getActivity(): ?Activity;
|
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
|
* @return User|null
|
||||||
*/
|
*/
|
||||||
public function getUser()
|
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
|
* @param User|int|null $user
|
||||||
* @return TimesheetQuery
|
* @return TimesheetQuery
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -291,7 +291,9 @@
|
|||||||
<td class="column-project" {% if not columns.project %}style="display: none"{% endif %}>{{ entry.project.name }}
|
<td class="column-project" {% if not columns.project %}style="display: none"{% endif %}>{{ entry.project.name }}
|
||||||
</td>
|
</td>
|
||||||
<td class="column-activity" {% if not columns.activity %}style="display: none"{% endif %}>
|
<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>
|
||||||
<td class="column-description" {% if not columns.description %}style="display: none"{% endif %}>
|
<td class="column-description" {% if not columns.description %}style="display: none"{% endif %}>
|
||||||
{% if entry.description is not empty %}
|
{% if entry.description is not empty %}
|
||||||
@@ -327,7 +329,9 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% for id, field in activityMetaFields %}
|
{% for id, field in activityMetaFields %}
|
||||||
<td class="column-{{ 'a_' ~ field.name }} text-nowrap" {% if not columns['a_' ~ field.name] %}style="display: none"{% endif %}>
|
<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>
|
</td>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% for id, field in userPreferences %}
|
{% for id, field in userPreferences %}
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ mpdf-->
|
|||||||
<td>{{ entry.user.displayName }}</td>
|
<td>{{ entry.user.displayName }}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td>
|
<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 %}
|
{% if entry.description is not empty %}
|
||||||
<br>
|
<br>
|
||||||
<i>{{ entry.description|escape|desc2html }}</i>
|
<i>{{ entry.description|escape|desc2html }}</i>
|
||||||
@@ -209,7 +209,7 @@ mpdf-->
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<td class="totals duration">{{ duration|duration }}</td>
|
<td class="totals duration">{{ duration|duration }}</td>
|
||||||
{% if showRateColumn %}
|
{% 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 %}
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
{% set tableName = 'timesheet_admin' %}
|
{% set tableName = 'timesheet_admin' %}
|
||||||
{% set paginationRoute = 'admin_timesheet_paginated' %}
|
{% set paginationRoute = 'admin_timesheet_paginated' %}
|
||||||
{% set editRoute = 'admin_timesheet_edit' %}
|
{% set editRoute = 'admin_timesheet_edit' %}
|
||||||
{% set canSeeRate = true %}
|
{% set canSeeRate = is_granted('view_rate_other_timesheet') %}
|
||||||
{% set canSeeUsername = true %}
|
{% set canSeeUsername = true %}
|
||||||
{% set allowMarkdown = false %}
|
{% set allowMarkdown = false %}
|
||||||
|
|
||||||
|
|||||||
@@ -12,23 +12,23 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12">
|
<div class="col-xs-12">
|
||||||
<h2 class="page-header">
|
<h2 class="page-header">
|
||||||
<span contenteditable="true">
|
{% if not showUserColumn %}
|
||||||
{% if query.begin is not empty and query.end is not empty %}
|
{{ widgets.username(query.user) }}:
|
||||||
{% if query.begin|date('m') != query.end|date('m') or query.begin|date('Y') != query.end|date('Y') %}
|
{% endif %}
|
||||||
{{ query.begin|date_short }} - {{ query.end|date_short }}
|
{% if query.begin is not empty %}
|
||||||
{% elseif query.end is not empty %}
|
{{ query.begin|date_short }}
|
||||||
{{ query.end|month_name|trans }} {{ query.end|date('Y') }}
|
{% else %}
|
||||||
{% elseif query.begin is not empty %}
|
{% set last = entries|last %}
|
||||||
{{ query.begin|month_name|trans }} {{ query.begin|date('Y') }}
|
{{ last.begin|date_short }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
–
|
||||||
</span>
|
{% if query.end is not empty %}
|
||||||
|
{{ query.end|date_short }}
|
||||||
|
{% else %}
|
||||||
|
{% set first = entries|first %}
|
||||||
|
{{ first.end|date_short }}
|
||||||
|
{% endif %}
|
||||||
</h2>
|
</h2>
|
||||||
{% if not showUserColumn %}
|
|
||||||
<p>
|
|
||||||
{{ 'label.user'|trans }}: {{ widgets.username(query.user) }}
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<th>{{ 'label.description'|trans }}</th>
|
<th>{{ 'label.description'|trans }}</th>
|
||||||
{% for field in metaColumns %}
|
{% for field in metaColumns %}
|
||||||
<th>{{ field.label|trans }}</th>
|
<th class="text-nowrap">{{ field.label|trans }}</th>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<th>{{ 'label.hours'|trans }}</th>
|
<th>{{ 'label.hours'|trans }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<span class="small">
|
<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.project'|trans }}: {{ entry.project.name }} |
|
||||||
{{ 'label.customer'|trans }}: {{ entry.project.customer.name }}
|
{{ 'label.customer'|trans }}: {{ entry.project.customer.name }}
|
||||||
</span>
|
</span>
|
||||||
@@ -86,7 +86,7 @@
|
|||||||
<th></th>
|
<th></th>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<th>{{ 'invoice.total_working_time'|trans }}</th>
|
<th>{{ 'invoice.total_working_time'|trans }}</th>
|
||||||
<th>{{ timeWorked|duration }}</th>
|
<th class="text-nowrap">{{ timeWorked|duration }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>
|
</tfoot>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -46,6 +46,6 @@ class HtmlRendererTest extends AbstractRendererTest
|
|||||||
|
|
||||||
$content = $response->getContent();
|
$content = $response->getContent();
|
||||||
|
|
||||||
$this->assertStringContainsString('<th>01:50 h</th>', $content);
|
$this->assertStringContainsString('>01:50 h<', $content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user