Release 1.6.2 (#1289)

* include user teams in user entity
* prevent unauthorized access via API
* improve teamlead permission handling in team timesheets
* add team data to user entity
* add security tests
* highlight menu for invoice template copy
* unified handling of invoice data across all templates
* access to the current users data in invoice templates
* permission improvement in invoice form
* allow to skip record rows
* allow to add new invoice locations without overwriting the global ones
* allow to order user preferences
* change permission for normal users with access to view_other_timesheets
* properly validate invoice template field length
* allow to replace multiple variables in cell values text
* upgraded office invoice template
* doctrine deprecation fix
* upgrade phpoffice/phpword
* fix future begin check for default rounding rules
* dashboard widget counter: respect visibility and teams - fixes #1161
* fix future begin check for default rounding rules
* added new events for pre and post invoice rendering
* fix permission issue for users without team seeing all records
* prevent error in spreadsheet renderer for empty invoices
This commit is contained in:
Kevin Papst
2019-12-02 10:57:03 +01:00
committed by GitHub
parent 47414cfd0e
commit 984c852ab6
78 changed files with 1279 additions and 625 deletions

View File

@@ -13,6 +13,7 @@ use App\Entity\Timesheet;
use App\Invoice\InvoiceItem;
use App\Invoice\InvoiceItemInterface;
use App\Invoice\InvoiceItemWithAmountInterface;
use App\Invoice\InvoiceItemWithTypeInterface;
abstract class AbstractMergedCalculator extends AbstractCalculator
{
@@ -37,6 +38,25 @@ abstract class AbstractMergedCalculator extends AbstractCalculator
if ($entry instanceof InvoiceItemWithAmountInterface) {
$amount = $entry->getAmount();
}
if ($entry instanceof InvoiceItemWithTypeInterface) {
$type = $entry->getInvoiceType();
$category = $entry->getInvoiceCategory();
} else {
$type = InvoiceItem::TYPE_TIMESHEET;
$category = InvoiceItem::CATEGORY_TIMESHEET_WORK;
}
if (null !== $invoiceItem->getType() && $type !== $invoiceItem->getType()) {
$type = InvoiceItem::TYPE_MIXED;
}
if (null !== $invoiceItem->getCategory() && $category !== $invoiceItem->getCategory()) {
$category = InvoiceItem::CATEGORY_MIXED;
}
$invoiceItem->setType($type);
$invoiceItem->setCategory($category);
$invoiceItem->setAmount($invoiceItem->getAmount() + $amount);
$invoiceItem->setUser($entry->getUser());
$invoiceItem->setRate($invoiceItem->getRate() + $entry->getRate());

View File

@@ -0,0 +1,104 @@
<?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;
use App\Twig\DateExtensions;
use App\Twig\Extensions;
use Symfony\Contracts\Translation\TranslatorInterface;
final class DefaultInvoiceFormatter implements InvoiceFormatter
{
/**
* @var DateExtensions
*/
private $dateExtension;
/**
* @var Extensions
*/
private $extension;
/**
* @var TranslatorInterface
*/
private $translator;
/**
* @param TranslatorInterface $translator
* @param DateExtensions $dateExtension
* @param Extensions $extensions
*/
public function __construct(TranslatorInterface $translator, DateExtensions $dateExtension, Extensions $extensions)
{
$this->translator = $translator;
$this->dateExtension = $dateExtension;
$this->extension = $extensions;
}
/**
* @param \DateTime $date
* @return mixed
*/
public function getFormattedDateTime(\DateTime $date)
{
return $this->dateExtension->dateShort($date);
}
/**
* @param \DateTime $date
* @return mixed
*/
public function getFormattedTime(\DateTime $date)
{
return $this->dateExtension->time($date);
}
/**
* @param int $amount
* @param string $currency
* @return string
*/
public function getFormattedMoney($amount, $currency)
{
return $this->extension->money($amount, $currency);
}
/**
* @param \DateTime $date
* @return mixed
*/
public function getFormattedMonthName(\DateTime $date)
{
return $this->translator->trans($this->dateExtension->monthName($date));
}
/**
* @param int $seconds
* @return mixed
*/
public function getFormattedDuration($seconds)
{
return $this->extension->duration($seconds);
}
/**
* @param int $seconds
* @return mixed
*/
public function getFormattedDecimalDuration($seconds)
{
return $this->extension->durationDecimal($seconds);
}
public function getCurrencySymbol(string $currency): string
{
return $this->extension->currency($currency);
}
}

View File

@@ -0,0 +1,55 @@
<?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;
/**
* @internal this is subject to change
*/
interface InvoiceFormatter
{
/**
* @param \DateTime $date
* @return mixed
*/
public function getFormattedDateTime(\DateTime $date);
/**
* @param \DateTime $date
* @return mixed
*/
public function getFormattedTime(\DateTime $date);
/**
* @param int $amount
* @param string|null $currency
* @return mixed
*/
public function getFormattedMoney($amount, $currency);
/**
* @param \DateTime $date
* @return mixed
*/
public function getFormattedMonthName(\DateTime $date);
/**
* @param int $seconds
* @return mixed
*/
public function getFormattedDuration($seconds);
/**
* @param int $seconds
* @return mixed
*/
public function getFormattedDecimalDuration($seconds);
public function getCurrencySymbol(string $currency): string;
}

View File

@@ -18,6 +18,11 @@ 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
*/
@@ -66,6 +71,14 @@ final class InvoiceItem
* @var array
*/
private $additionalFields = [];
/**
* @var string
*/
private $type;
/**
* @var string
*/
private $category;
public function addAdditionalField(string $name, ?string $value): InvoiceItem
{
@@ -215,4 +228,28 @@ final class InvoiceItem
return $this;
}
public function getType(): ?string
{
return $this->type;
}
public function setType(string $type): InvoiceItem
{
$this->type = $type;
return $this;
}
public function getCategory(): ?string
{
return $this->category;
}
public function setCategory(string $category): InvoiceItem
{
$this->category = $category;
return $this;
}
}

View File

@@ -0,0 +1,17 @@
<?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;
}

View File

@@ -11,52 +11,57 @@ namespace App\Invoice;
use App\Entity\Customer;
use App\Entity\InvoiceTemplate;
use App\Entity\User;
use App\Entity\UserPreference;
use App\Repository\Query\InvoiceQuery;
/**
* InvoiceModel is the ONLY value that a RendererInterface receives for generating the invoice,
* besides the InvoiceDocument which is used as a "template".
*/
class InvoiceModel
final class InvoiceModel
{
/**
* @var Customer|null
*/
protected $customer;
private $customer;
/**
* @var InvoiceQuery
*/
protected $query;
private $query;
/**
* @var InvoiceItemInterface[]
*/
protected $entries = [];
private $entries = [];
/**
* @var InvoiceTemplate
*/
protected $template;
private $template;
/**
* @var CalculatorInterface
*/
protected $calculator;
private $calculator;
/**
* @var NumberGeneratorInterface
*/
protected $generator;
private $generator;
/**
* @var \DateTime
*/
protected $invoiceDate;
private $invoiceDate;
/**
* @var User
*/
private $user;
/**
* @var InvoiceFormatter
*/
private $formatter;
public function __construct()
public function __construct(InvoiceFormatter $formatter)
{
$this->invoiceDate = new \DateTime();
$this->formatter = $formatter;
}
/**
@@ -183,4 +188,242 @@ class InvoiceModel
{
return $this->calculator;
}
/**
* Returns the user who is currently creating the invoice.
*
* @return User|null
*/
public function getUser(): ?User
{
return $this->user;
}
public function setUser(User $user): InvoiceModel
{
$this->user = $user;
return $this;
}
public function getFormatter(): ?InvoiceFormatter
{
return $this->formatter;
}
public function toArray(): array
{
$model = $this;
$customer = $model->getCustomer();
$project = $model->getQuery()->getProject();
$activity = $model->getQuery()->getActivity();
$currency = $model->getCalculator()->getCurrency();
$tax = $model->getCalculator()->getTax();
$total = $model->getCalculator()->getTotal();
$subtotal = $model->getCalculator()->getSubtotal();
$formatter = $model->getFormatter();
$values = [
'invoice.due_date' => $formatter->getFormattedDateTime($model->getDueDate()),
'invoice.date' => $formatter->getFormattedDateTime($model->getInvoiceDate()),
'invoice.number' => $model->getNumberGenerator()->getInvoiceNumber(),
'invoice.currency' => $currency,
'invoice.currency_symbol' => $formatter->getCurrencySymbol($currency),
'invoice.vat' => $model->getCalculator()->getVat(),
'invoice.tax' => $formatter->getFormattedMoney($tax, $currency),
'invoice.tax_nc' => $formatter->getFormattedMoney($tax, null),
'invoice.tax_plain' => $tax,
'invoice.total_time' => $formatter->getFormattedDuration($model->getCalculator()->getTimeWorked()),
'invoice.duration_decimal' => $formatter->getFormattedDecimalDuration($model->getCalculator()->getTimeWorked()),
'invoice.total' => $formatter->getFormattedMoney($total, $currency),
'invoice.total_nc' => $formatter->getFormattedMoney($total, null),
'invoice.total_plain' => $total,
'invoice.subtotal' => $formatter->getFormattedMoney($subtotal, $currency),
'invoice.subtotal_nc' => $formatter->getFormattedMoney($subtotal, null),
'invoice.subtotal_plain' => $subtotal,
'template.name' => $model->getTemplate()->getName(),
'template.company' => $model->getTemplate()->getCompany(),
'template.address' => $model->getTemplate()->getAddress(),
'template.title' => $model->getTemplate()->getTitle(),
'template.payment_terms' => $model->getTemplate()->getPaymentTerms(),
'template.due_days' => $model->getTemplate()->getDueDays(),
'template.vat_id' => $model->getTemplate()->getVatId(),
'template.contact' => $model->getTemplate()->getContact(),
'template.payment_details' => $model->getTemplate()->getPaymentDetails(),
'query.begin' => $formatter->getFormattedDateTime($model->getQuery()->getBegin()),
'query.day' => $model->getQuery()->getBegin()->format('d'),
'query.end' => $formatter->getFormattedDateTime($model->getQuery()->getEnd()),
'query.month' => $formatter->getFormattedMonthName($model->getQuery()->getBegin()),
'query.month_number' => $model->getQuery()->getBegin()->format('m'),
'query.year' => $model->getQuery()->getBegin()->format('Y'),
];
if (null !== $model->getUser()) {
$user = $model->getUser();
/** @var UserPreference $metaField */
foreach ($user->getPreferences() as $metaField) {
$values = array_merge($values, [
'user.meta.' . $metaField->getName() => $metaField->getValue(),
]);
}
$values = array_merge($values, [
'user.name' => $user->getUsername(),
'user.email' => $user->getEmail(),
'user.title' => $user->getTitle(),
'user.alias' => $user->getAlias(),
]);
}
if (null !== $activity) {
$values = array_merge($values, [
'activity.id' => $activity->getId(),
'activity.name' => $activity->getName(),
'activity.comment' => $activity->getComment(),
'activity.fixed_rate' => $formatter->getFormattedMoney($activity->getFixedRate(), $currency),
'activity.fixed_rate_nc' => $formatter->getFormattedMoney($activity->getFixedRate(), null),
'activity.fixed_rate_plain' => $activity->getFixedRate(),
'activity.hourly_rate' => $formatter->getFormattedMoney($activity->getHourlyRate(), $currency),
'activity.hourly_rate_nc' => $formatter->getFormattedMoney($activity->getHourlyRate(), null),
'activity.hourly_rate_plain' => $activity->getHourlyRate(),
]);
foreach ($activity->getVisibleMetaFields() as $metaField) {
$values = array_merge($values, [
'activity.meta.' . $metaField->getName() => $metaField->getValue(),
]);
}
}
if (null !== $project) {
$values = array_merge($values, [
'project.id' => $project->getId(),
'project.name' => $project->getName(),
'project.comment' => $project->getComment(),
'project.order_number' => $project->getOrderNumber(),
'project.order_date' => null !== $project->getOrderDate() ? $formatter->getFormattedDateTime($project->getOrderDate()) : '',
'project.fixed_rate' => $formatter->getFormattedMoney($project->getFixedRate(), $currency),
'project.fixed_rate_nc' => $formatter->getFormattedMoney($project->getFixedRate(), null),
'project.fixed_rate_plain' => $project->getFixedRate(),
'project.hourly_rate' => $formatter->getFormattedMoney($project->getHourlyRate(), $currency),
'project.hourly_rate_nc' => $formatter->getFormattedMoney($project->getHourlyRate(), null),
'project.hourly_rate_plain' => $project->getHourlyRate(),
]);
foreach ($project->getVisibleMetaFields() as $metaField) {
$values = array_merge($values, [
'project.meta.' . $metaField->getName() => $metaField->getValue(),
]);
}
}
if (null !== $customer) {
$values = array_merge($values, [
'customer.id' => $customer->getId(),
'customer.address' => $customer->getAddress(),
'customer.name' => $customer->getName(),
'customer.contact' => $customer->getContact(),
'customer.company' => $customer->getCompany(),
'customer.vat' => $customer->getVatId(),
'customer.number' => $customer->getNumber(),
'customer.country' => $customer->getCountry(),
'customer.homepage' => $customer->getHomepage(),
'customer.comment' => $customer->getComment(),
'customer.fixed_rate' => $formatter->getFormattedMoney($customer->getFixedRate(), $currency),
'customer.fixed_rate_nc' => $formatter->getFormattedMoney($customer->getFixedRate(), null),
'customer.fixed_rate_plain' => $customer->getFixedRate(),
'customer.hourly_rate' => $formatter->getFormattedMoney($customer->getHourlyRate(), $currency),
'customer.hourly_rate_nc' => $formatter->getFormattedMoney($customer->getHourlyRate(), null),
'customer.hourly_rate_plain' => $customer->getHourlyRate(),
]);
foreach ($customer->getVisibleMetaFields() as $metaField) {
$values = array_merge($values, [
'customer.meta.' . $metaField->getName() => $metaField->getValue(),
]);
}
}
return $values;
}
public function itemToArray(InvoiceItem $invoiceItem): array
{
$formatter = $this->getFormatter();
$rate = $invoiceItem->getRate();
$appliedRate = $invoiceItem->getHourlyRate();
$amount = $formatter->getFormattedDuration($invoiceItem->getDuration());
$description = $invoiceItem->getDescription();
if ($invoiceItem->isFixedRate()) {
$appliedRate = $invoiceItem->getFixedRate();
$amount = $invoiceItem->getAmount();
}
if (empty($description)) {
$description = $invoiceItem->getActivity()->getName();
}
$user = $invoiceItem->getUser();
// this should never happen!
if (empty($appliedRate)) {
$appliedRate = 0;
}
$activity = $invoiceItem->getActivity();
$project = $invoiceItem->getProject();
$customer = $project->getCustomer();
$currency = $customer->getCurrency();
$begin = $invoiceItem->getBegin();
$end = $invoiceItem->getEnd();
$values = [
'entry.row' => '',
'entry.description' => $description,
'entry.amount' => $amount,
'entry.type' => $invoiceItem->getType(),
'entry.category' => $invoiceItem->getCategory(),
'entry.rate' => $formatter->getFormattedMoney($appliedRate, $currency),
'entry.rate_nc' => $formatter->getFormattedMoney($appliedRate, null),
'entry.rate_plain' => $appliedRate,
'entry.total' => $formatter->getFormattedMoney($rate, $currency),
'entry.total_nc' => $formatter->getFormattedMoney($rate, null),
'entry.total_plain' => $rate,
'entry.currency' => $currency,
'entry.duration' => $invoiceItem->getDuration(),
'entry.duration_decimal' => $formatter->getFormattedDecimalDuration($invoiceItem->getDuration()),
'entry.duration_minutes' => number_format($invoiceItem->getDuration() / 60),
'entry.begin' => $formatter->getFormattedDateTime($begin),
'entry.begin_time' => $formatter->getFormattedTime($begin),
'entry.begin_timestamp' => $begin->getTimestamp(),
'entry.end' => $formatter->getFormattedDateTime($end),
'entry.end_time' => $formatter->getFormattedTime($end),
'entry.end_timestamp' => $end->getTimestamp(),
'entry.date' => $formatter->getFormattedDateTime($begin),
'entry.user_id' => $user->getId(),
'entry.user_name' => $user->getUsername(),
'entry.user_title' => $user->getTitle(),
'entry.user_alias' => $user->getAlias(),
'entry.activity' => $activity->getName(),
'entry.activity_id' => $activity->getId(),
'entry.project' => $project->getName(),
'entry.project_id' => $project->getId(),
'entry.customer' => $customer->getName(),
'entry.customer_id' => $customer->getId(),
];
foreach ($invoiceItem->getAdditionalFields() as $name => $value) {
$values = array_merge($values, [
'entry.meta.' . $name => $value,
]);
}
return $values;
}
}

View File

@@ -9,93 +9,54 @@
namespace App\Invoice\Renderer;
use App\Twig\DateExtensions;
use App\Twig\Extensions;
use Symfony\Contracts\Translation\TranslatorInterface;
use App\Entity\InvoiceDocument;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
/**
* @internal
*/
abstract class AbstractRenderer
{
use RendererTrait;
/**
* @var DateExtensions
* @return string[]
*/
protected $dateExtension;
abstract protected function getFileExtensions();
/**
* @var Extensions
*/
protected $extension;
/**
* @var TranslatorInterface
*/
protected $translator;
/**
* @param TranslatorInterface $translator
* @param DateExtensions $dateExtension
* @param Extensions $extensions
*/
public function __construct(TranslatorInterface $translator, DateExtensions $dateExtension, Extensions $extensions)
{
$this->translator = $translator;
$this->dateExtension = $dateExtension;
$this->extension = $extensions;
}
/**
* @param \DateTime $date
* @return mixed
*/
protected function getFormattedDateTime(\DateTime $date)
{
return $this->dateExtension->dateShort($date);
}
/**
* @param \DateTime $date
* @return mixed
*/
protected function getFormattedTime(\DateTime $date)
{
return $this->dateExtension->time($date);
}
/**
* @param int $amount
* @param string $currency
* @return string
*/
protected function getFormattedMoney($amount, $currency)
abstract protected function getContentType();
/**
* @param InvoiceDocument $document
* @return bool
*/
public function supports(InvoiceDocument $document): bool
{
return $this->extension->money($amount, $currency);
foreach ($this->getFileExtensions() as $extension) {
if (stripos($document->getFilename(), $extension) !== false) {
return true;
}
}
return false;
}
/**
* @param \DateTime $date
* @return mixed
* @param mixed $file
* @param string $filename
* @return BinaryFileResponse
*/
protected function getFormattedMonthName(\DateTime $date)
protected function getFileResponse($file, $filename)
{
return $this->translator->trans($this->dateExtension->monthName($date));
}
$response = new BinaryFileResponse($file);
$disposition = $response->headers->makeDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $filename);
/**
* @param int $seconds
* @return mixed
*/
protected function getFormattedDuration($seconds)
{
return $this->extension->duration($seconds);
}
$response->headers->set('Content-Type', $this->getContentType());
$response->headers->set('Content-Disposition', $disposition);
$response->deleteFileAfterSend(true);
/**
* @param int $seconds
* @return mixed
*/
protected function getFormattedDecimalDuration($seconds)
{
return $this->extension->durationDecimal($seconds);
return $response;
}
}

View File

@@ -16,6 +16,9 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use Symfony\Component\HttpFoundation\Response;
/**
* @internal
*/
abstract class AbstractSpreadsheetRenderer extends AbstractRenderer
{
/**
@@ -40,7 +43,7 @@ abstract class AbstractSpreadsheetRenderer extends AbstractRenderer
$spreadsheet = IOFactory::load($document->getFilename());
$worksheet = $spreadsheet->getActiveSheet();
$entries = $model->getCalculator()->getEntries();
$replacer = $this->modelToReplacer($model);
$sheetReplacer = $model->toArray();
$invoiceItemCount = count($entries);
if ($invoiceItemCount > 1) {
$this->addTemplateRows($worksheet, $invoiceItemCount);
@@ -57,26 +60,37 @@ abstract class AbstractSpreadsheetRenderer extends AbstractRenderer
$entryRow = 0;
foreach ($worksheet->getRowIterator() as $row) {
$invoiceItem = $entries[$entryRow];
$sheetValues = false;
foreach ($row->getCellIterator() as $cell) {
$value = $cell->getValue();
if (stripos($value, '${entry.') !== false) {
if ($sheetValues === false) {
$sheetValues = $this->invoiceItemToArray($invoiceItem);
}
$searcher = str_replace('${', '', $value);
$searcher = str_replace('}', '', $searcher);
if (isset($sheetValues[$searcher])) {
$cell->setValue($sheetValues[$searcher]);
}
} elseif (stripos($value, '${') !== false) {
$searcher = str_replace('${', '', $value);
$searcher = str_replace('}', '', $searcher);
if (isset($replacer[$searcher])) {
$cell->setValue($replacer[$searcher]);
}
$replacer = null;
if (stripos($value, '${') === false) {
continue;
}
if (stripos($value, '${entry.') !== false) {
if ($sheetValues === false && isset($entries[$entryRow])) {
$sheetValues = $model->itemToArray($entries[$entryRow]);
}
$replacer = $sheetValues;
} elseif (stripos($value, '${') !== false) {
$replacer = $sheetReplacer;
}
if (empty($replacer)) {
continue;
}
// we can have mixed cell content, which makes it much more complicated
foreach ($replacer as $key => $content) {
$searchKey = '${' . $key . '}';
if (stripos($value, $searchKey) === false) {
continue;
}
$value = str_replace($searchKey, $content, $value);
}
$cell->setValue($value);
}
if ($sheetValues !== false && $entryRow < $invoiceItemCount - 1) {
@@ -105,7 +119,7 @@ abstract class AbstractSpreadsheetRenderer extends AbstractRenderer
$value = $cell->getValue();
if (stripos($value, '${entry.') !== false) {
$startRow = $row->getRowIndex();
$worksheet->insertNewRowBefore($row->getRowIndex(), $invoiceItemCount - 1);
$worksheet->insertNewRowBefore($startRow + 1, $invoiceItemCount - 1);
break 2;
}
@@ -123,15 +137,18 @@ abstract class AbstractSpreadsheetRenderer extends AbstractRenderer
throw new \Exception('Invalid invoice document, no template row found.');
}
// fill up all new rows with template values
$templateRow = $invoiceItemCount + $startRow;
$iterator = $worksheet->getRowIterator($templateRow - 1, $templateRow);
// fill up all new rows with template replacer
$templateRow = $startRow;
$iterator = $worksheet->getRowIterator($templateRow, $templateRow + 1);
$templateColumns = [];
foreach ($iterator->current()->getCellIterator() as $cell) {
$tmpRow = $iterator->current();
foreach ($tmpRow->getCellIterator() as $cell) {
$templateColumns[$cell->getColumn()] = $cell->getValue();
}
$iterator = $worksheet->getRowIterator($startRow, $templateRow - 2);
$iterator = $worksheet->getRowIterator($startRow, $startRow + $invoiceItemCount - 1);
foreach ($iterator as $row) {
foreach ($row->getCellIterator() as $cell) {
$cell->setValue($templateColumns[$cell->getColumn()]);

View File

@@ -13,7 +13,7 @@ use App\Invoice\RendererInterface;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
class CsvRenderer extends AbstractSpreadsheetRenderer implements RendererInterface
final class CsvRenderer extends AbstractSpreadsheetRenderer implements RendererInterface
{
/**
* @return string[]

View File

@@ -19,7 +19,7 @@ use PhpOffice\PhpWord\TemplateProcessor;
use Symfony\Component\HttpFoundation\File\Stream;
use Symfony\Component\HttpFoundation\Response;
class DocxRenderer extends AbstractRenderer implements RendererInterface
final class DocxRenderer extends AbstractRenderer implements RendererInterface
{
/**
* @param InvoiceDocument $document
@@ -35,7 +35,7 @@ class DocxRenderer extends AbstractRenderer implements RendererInterface
$xmlEscaper = new Xml();
$template = new TemplateProcessor($document->getFilename());
foreach ($this->modelToReplacer($model) as $search => $replace) {
foreach ($model->toArray() as $search => $replace) {
$replace = $xmlEscaper->escape($replace);
$replace = str_replace(PHP_EOL, '</w:t><w:br /><w:t xml:space="preserve">', $replace);
@@ -45,12 +45,18 @@ class DocxRenderer extends AbstractRenderer implements RendererInterface
try {
$template->cloneRow('entry.description', count($model->getCalculator()->getEntries()));
} catch (OfficeException $ex) {
$template->cloneRow('entry.row', count($model->getCalculator()->getEntries()));
try {
$template->cloneRow('entry.row', count($model->getCalculator()->getEntries()));
} catch (OfficeException $ex) {
@trigger_error(
sprintf('Invoice document (%s) did not contain a clone row, was that on purpose?', $document->getFilename())
);
}
}
$i = 1;
foreach ($model->getCalculator()->getEntries() as $entry) {
$values = $this->invoiceItemToArray($entry);
$values = $model->itemToArray($entry);
foreach ($values as $search => $replace) {
$replace = $xmlEscaper->escape($replace);
$replace = str_replace(PHP_EOL, '</w:t><w:br /><w:t xml:space="preserve">', $replace);

View File

@@ -13,7 +13,7 @@ use App\Invoice\RendererInterface;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
class OdsRenderer extends AbstractSpreadsheetRenderer implements RendererInterface
final class OdsRenderer extends AbstractSpreadsheetRenderer implements RendererInterface
{
/**
* @return string[]

View File

@@ -9,181 +9,28 @@
namespace App\Invoice\Renderer;
use App\Entity\InvoiceDocument;
use App\Invoice\InvoiceItem;
use App\Invoice\InvoiceModel;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
trait RendererTrait
{
/**
* @return string[]
* @var InvoiceModel
*/
abstract protected function getFileExtensions();
/**
* @return string
*/
abstract protected function getContentType();
/**
* @param InvoiceDocument $document
* @return bool
*/
public function supports(InvoiceDocument $document): bool
{
foreach ($this->getFileExtensions() as $extension) {
if (stripos($document->getFilename(), $extension) !== false) {
return true;
}
}
return false;
}
/**
* @param \DateTime $date
* @return mixed
*/
abstract protected function getFormattedDateTime(\DateTime $date);
/**
* @param \DateTime $date
* @return mixed
*/
abstract protected function getFormattedTime(\DateTime $date);
/**
* @param int $amount
* @param string|null $currency
* @return mixed
*/
abstract protected function getFormattedMoney($amount, $currency);
/**
* @param \DateTime $date
* @return mixed
*/
abstract protected function getFormattedMonthName(\DateTime $date);
/**
* @param int $seconds
* @return mixed
*/
abstract protected function getFormattedDuration($seconds);
/**
* @param int $seconds
* @return mixed
*/
abstract protected function getFormattedDecimalDuration($seconds);
private $model;
/**
* @deprecated since 1.6.2 - will be removed with 2.0
* @param InvoiceModel $model
* @return array
*/
protected function modelToReplacer(InvoiceModel $model)
{
$customer = $model->getCustomer();
$project = $model->getQuery()->getProject();
$activity = $model->getQuery()->getActivity();
$currency = $model->getCalculator()->getCurrency();
$tax = $model->getCalculator()->getTax();
$total = $model->getCalculator()->getTotal();
$subtotal = $model->getCalculator()->getSubtotal();
@trigger_error('modelToReplacer() is deprecated and will be removed with 2.0', E_USER_DEPRECATED);
$values = [
'invoice.due_date' => $this->getFormattedDateTime($model->getDueDate()),
'invoice.date' => $this->getFormattedDateTime($model->getInvoiceDate()),
'invoice.number' => $model->getNumberGenerator()->getInvoiceNumber(),
'invoice.currency' => $currency,
'invoice.vat' => $model->getCalculator()->getVat(),
'invoice.tax' => $this->getFormattedMoney($tax, $currency),
'invoice.tax_nc' => $this->getFormattedMoney($tax, null),
'invoice.total_time' => $this->getFormattedDuration($model->getCalculator()->getTimeWorked()),
'invoice.duration_decimal' => $this->getFormattedDecimalDuration($model->getCalculator()->getTimeWorked()),
'invoice.total' => $this->getFormattedMoney($total, $currency),
'invoice.total_nc' => $this->getFormattedMoney($total, null),
'invoice.subtotal' => $this->getFormattedMoney($subtotal, $currency),
'invoice.subtotal_nc' => $this->getFormattedMoney($subtotal, null),
$this->model = $model;
'template.name' => $model->getTemplate()->getName(),
'template.company' => $model->getTemplate()->getCompany(),
'template.address' => $model->getTemplate()->getAddress(),
'template.title' => $model->getTemplate()->getTitle(),
'template.payment_terms' => $model->getTemplate()->getPaymentTerms(),
'template.due_days' => $model->getTemplate()->getDueDays(),
'template.vat_id' => $model->getTemplate()->getVatId(),
'template.contact' => $model->getTemplate()->getContact(),
'template.payment_details' => $model->getTemplate()->getPaymentDetails(),
'query.begin' => $this->getFormattedDateTime($model->getQuery()->getBegin()),
'query.day' => $model->getQuery()->getBegin()->format('d'),
'query.end' => $this->getFormattedDateTime($model->getQuery()->getEnd()),
'query.month' => $this->getFormattedMonthName($model->getQuery()->getBegin()),
'query.month_number' => $model->getQuery()->getBegin()->format('m'),
'query.year' => $model->getQuery()->getBegin()->format('Y'),
];
if (null !== $activity) {
$values = array_merge($values, [
'activity.id' => $activity->getId(),
'activity.name' => $activity->getName(),
'activity.comment' => $activity->getComment(),
'activity.fixed_rate' => $activity->getFixedRate(),
'activity.hourly_rate' => $activity->getHourlyRate(),
]);
foreach ($activity->getVisibleMetaFields() as $metaField) {
$values = array_merge($values, [
'activity.meta.' . $metaField->getName() => $metaField->getValue(),
]);
}
}
if (null !== $project) {
$values = array_merge($values, [
'project.id' => $project->getId(),
'project.name' => $project->getName(),
'project.comment' => $project->getComment(),
'project.order_number' => $project->getOrderNumber(),
'project.order_date' => null !== $project->getOrderDate() ? $this->getFormattedDateTime($project->getOrderDate()) : '',
'project.fixed_rate' => $project->getFixedRate(),
'project.hourly_rate' => $project->getHourlyRate(),
]);
foreach ($project->getVisibleMetaFields() as $metaField) {
$values = array_merge($values, [
'project.meta.' . $metaField->getName() => $metaField->getValue(),
]);
}
}
if (null !== $customer) {
$values = array_merge($values, [
'customer.id' => $customer->getId(),
'customer.address' => $customer->getAddress(),
'customer.name' => $customer->getName(),
'customer.contact' => $customer->getContact(),
'customer.company' => $customer->getCompany(),
'customer.vat' => $customer->getVatId(),
'customer.number' => $customer->getNumber(),
'customer.country' => $customer->getCountry(),
'customer.homepage' => $customer->getHomepage(),
'customer.comment' => $customer->getComment(),
'customer.fixed_rate' => $customer->getFixedRate(),
'customer.hourly_rate' => $customer->getHourlyRate(),
]);
foreach ($customer->getVisibleMetaFields() as $metaField) {
$values = array_merge($values, [
'customer.meta.' . $metaField->getName() => $metaField->getValue(),
]);
}
}
return $values;
return $model->toArray();
}
/**
@@ -193,94 +40,18 @@ trait RendererTrait
{
@trigger_error('timesheetToArray() is deprecated and will be removed with 2.0', E_USER_DEPRECATED);
return $this->invoiceItemToArray($invoiceItem);
}
protected function invoiceItemToArray(InvoiceItem $invoiceItem): array
{
$rate = $invoiceItem->getRate();
$hourlyRate = $invoiceItem->getHourlyRate();
$amount = $this->getFormattedDuration($invoiceItem->getDuration());
$description = $invoiceItem->getDescription();
if ($invoiceItem->isFixedRate()) {
$hourlyRate = $invoiceItem->getFixedRate();
$amount = $invoiceItem->getAmount();
}
if (empty($description)) {
$description = $invoiceItem->getActivity()->getName();
}
$user = $invoiceItem->getUser();
// this should never happen!
if (empty($hourlyRate)) {
$hourlyRate = 0;
}
$activity = $invoiceItem->getActivity();
$project = $invoiceItem->getProject();
$customer = $project->getCustomer();
$currency = $customer->getCurrency();
$begin = $invoiceItem->getBegin();
$end = $invoiceItem->getEnd();
$values = [
'entry.row' => '',
'entry.description' => $description,
'entry.amount' => $amount,
'entry.rate' => $this->getFormattedMoney($hourlyRate, $currency),
'entry.rate_nc' => $this->getFormattedMoney($hourlyRate, null),
'entry.total' => $this->getFormattedMoney($rate, $currency),
'entry.total_nc' => $this->getFormattedMoney($rate, null),
'entry.currency' => $currency,
'entry.duration' => $invoiceItem->getDuration(),
'entry.duration_decimal' => $this->getFormattedDecimalDuration($invoiceItem->getDuration()),
'entry.duration_minutes' => number_format($invoiceItem->getDuration() / 60),
'entry.begin' => $this->getFormattedDateTime($begin),
'entry.begin_time' => $this->getFormattedTime($begin),
'entry.begin_timestamp' => $begin->getTimestamp(),
'entry.end' => $this->getFormattedDateTime($end),
'entry.end_time' => $this->getFormattedTime($end),
'entry.end_timestamp' => $end->getTimestamp(),
'entry.date' => $this->getFormattedDateTime($begin),
'entry.user_id' => $user->getId(),
'entry.user_name' => $user->getUsername(),
'entry.user_title' => $user->getTitle(),
'entry.user_alias' => $user->getAlias(),
'entry.activity' => $activity->getName(),
'entry.activity_id' => $activity->getId(),
'entry.project' => $project->getName(),
'entry.project_id' => $project->getId(),
'entry.customer' => $customer->getName(),
'entry.customer_id' => $customer->getId(),
];
foreach ($invoiceItem->getAdditionalFields() as $name => $value) {
$values = array_merge($values, [
'entry.meta.' . $name => $value,
]);
}
return $values;
return $this->model->itemToArray($invoiceItem);
}
/**
* @param mixed $file
* @param string $filename
* @return BinaryFileResponse
* @deprecated since 1.6.2 - will be removed with 2.0
* @param InvoiceItem $invoiceItem
* @return array
*/
protected function getFileResponse($file, $filename)
protected function invoiceItemToArray(InvoiceItem $invoiceItem): array
{
$response = new BinaryFileResponse($file);
$disposition = $response->headers->makeDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $filename);
@trigger_error('invoiceItemToArray() is deprecated and will be removed with 2.0', E_USER_DEPRECATED);
$response->headers->set('Content-Type', $this->getContentType());
$response->headers->set('Content-Disposition', $disposition);
$response->deleteFileAfterSend(true);
return $response;
return $this->model->itemToArray($invoiceItem);
}
}

View File

@@ -13,7 +13,7 @@ use App\Invoice\RendererInterface;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
class XlsxRenderer extends AbstractSpreadsheetRenderer implements RendererInterface
final class XlsxRenderer extends AbstractSpreadsheetRenderer implements RendererInterface
{
/**
* @return string[]