Next major version 2 with PHP 8.1, Symfony 6, Tabler UI, 2FA ... (#2902)

This commit is contained in:
Kevin Papst
2022-12-31 21:19:55 +01:00
committed by GitHub
parent 95e06746bd
commit 90a0fd8a22
2164 changed files with 83700 additions and 86426 deletions

View File

@@ -9,43 +9,25 @@
namespace App\Invoice\Calculator;
use App\Invoice\CalculatorInterface;
use App\Invoice\InvoiceItem;
use App\Invoice\InvoiceModel;
abstract class AbstractCalculator
{
/**
* @var string
*/
protected $currency;
/**
* @var InvoiceModel
*/
protected $model;
protected InvoiceModel $model;
/**
* @return InvoiceItem[]
*/
abstract public function getEntries();
abstract public function getEntries(): array;
/**
* @return string
*/
abstract public function getId(): string;
/**
* @param InvoiceModel $model
*/
public function setModel(InvoiceModel $model)
public function setModel(InvoiceModel $model): void
{
$this->model = $model;
}
/**
* @return float
*/
public function getSubtotal(): float
{
$amount = 0.00;
@@ -56,17 +38,11 @@ abstract class AbstractCalculator
return round($amount, 2);
}
/**
* @return float
*/
public function getVat(): ?float
public function getVat(): float
{
return $this->model->getTemplate()->getVat();
}
/**
* @return float
*/
public function getTax(): float
{
$vat = $this->getVat();
@@ -79,28 +55,11 @@ abstract class AbstractCalculator
return round($this->getSubtotal() * $percent, 2);
}
/**
* @return float
*/
public function getTotal(): float
{
return $this->getSubtotal() + $this->getTax();
}
/**
* @deprecated since 1.8 will be removed with 2.0
* @return string
*/
public function getCurrency(): string
{
@trigger_error(
sprintf('%s::getCurrency() is deprecated and will be removed with 2.0', CalculatorInterface::class),
E_USER_DEPRECATED
);
return $this->model->getCurrency();
}
/**
* Returns the total amount of worked time in seconds.
*

View File

@@ -9,42 +9,23 @@
namespace App\Invoice\Calculator;
use App\Entity\ExportableItem;
use App\Entity\Timesheet;
use App\Invoice\InvoiceItem;
use App\Invoice\InvoiceItemInterface;
use App\Invoice\InvoiceItemWithAmountInterface;
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
*/
protected function mergeTimesheets(InvoiceItem $invoiceItem, Timesheet $entry)
{
@trigger_error('mergeTimesheets() is deprecated and will be removed with 2.0', E_USER_DEPRECATED);
$this->mergeInvoiceItems($invoiceItem, $entry);
}
/**
* @param InvoiceItem $invoiceItem
* @param InvoiceItemInterface $entry
* @return void
*/
protected function mergeInvoiceItems(InvoiceItem $invoiceItem, InvoiceItemInterface $entry) /* : void */
protected function mergeInvoiceItems(InvoiceItem $invoiceItem, ExportableItem $entry): void
{
$duration = $invoiceItem->getDuration();
if (null !== $entry->getDuration()) {
$duration += $entry->getDuration();
}
$amount = 1;
if ($entry instanceof InvoiceItemWithAmountInterface) {
$amount = $entry->getAmount();
}
$amount = $entry->getAmount();
$type = $entry->getType();
$category = $entry->getCategory();
@@ -62,11 +43,7 @@ abstract class AbstractMergedCalculator extends AbstractCalculator
$invoiceItem->setAmount($invoiceItem->getAmount() + $amount);
$invoiceItem->setUser($entry->getUser());
$invoiceItem->setRate($invoiceItem->getRate() + $entry->getRate());
if (method_exists($entry, 'getInternalRate')) {
$invoiceItem->setInternalRate($invoiceItem->getInternalRate() + $entry->getInternalRate());
} else {
$invoiceItem->setInternalRate($invoiceItem->getInternalRate() + $entry->getRate());
}
$invoiceItem->setInternalRate($invoiceItem->getInternalRate() + ($entry->getInternalRate() ?? 0.00));
$invoiceItem->setDuration($duration);
if (null !== $entry->getFixedRate()) {

View File

@@ -9,18 +9,18 @@
namespace App\Invoice\Calculator;
use App\Entity\ExportableItem;
use App\Invoice\CalculatorInterface;
use App\Invoice\InvoiceItem;
use App\Invoice\InvoiceItemInterface;
/**
* An abstract calculator that sums up the invoice item records.
*/
abstract class AbstractSumInvoiceCalculator extends AbstractMergedCalculator implements CalculatorInterface
{
abstract protected function calculateSumIdentifier(InvoiceItemInterface $invoiceItem): string;
abstract protected function calculateSumIdentifier(ExportableItem $invoiceItem): string;
protected function calculateIdentifier(InvoiceItemInterface $entry): string
protected function calculateIdentifier(ExportableItem $entry): string
{
$prefix = $this->calculateSumIdentifier($entry);
@@ -34,7 +34,7 @@ abstract class AbstractSumInvoiceCalculator extends AbstractMergedCalculator imp
/**
* @return InvoiceItem[]
*/
public function getEntries()
public function getEntries(): array
{
$entries = $this->model->getEntries();
if (empty($entries)) {
@@ -60,14 +60,10 @@ abstract class AbstractSumInvoiceCalculator extends AbstractMergedCalculator imp
/**
* @param InvoiceItem $invoiceItem
* @param InvoiceItemInterface $entry
* @param ExportableItem $entry
* @return void
*/
protected function mergeSumInvoiceItem(InvoiceItem $invoiceItem, InvoiceItemInterface $entry) /* : void */
protected function mergeSumInvoiceItem(InvoiceItem $invoiceItem, ExportableItem $entry): void
{
if (method_exists($this, 'mergeSumTimesheet')) {
@trigger_error('mergeSumTimesheet() is deprecated and will be removed with 2.0 - use mergeSumInvoiceItem() instead', E_USER_DEPRECATED);
$this->mergeSumTimesheet($invoiceItem, $entry);
}
}
}

View File

@@ -9,16 +9,16 @@
namespace App\Invoice\Calculator;
use App\Entity\ExportableItem;
use App\Invoice\CalculatorInterface;
use App\Invoice\InvoiceItem;
use App\Invoice\InvoiceItemInterface;
/**
* A calculator that sums up the invoice item records by activity.
*/
class ActivityInvoiceCalculator extends AbstractSumInvoiceCalculator implements CalculatorInterface
final class ActivityInvoiceCalculator extends AbstractSumInvoiceCalculator implements CalculatorInterface
{
protected function calculateSumIdentifier(InvoiceItemInterface $invoiceItem): string
protected function calculateSumIdentifier(ExportableItem $invoiceItem): string
{
if (null === $invoiceItem->getActivity()) {
return '__NULL__';
@@ -27,7 +27,7 @@ class ActivityInvoiceCalculator extends AbstractSumInvoiceCalculator implements
return (string) $invoiceItem->getActivity()->getId();
}
protected function mergeSumInvoiceItem(InvoiceItem $invoiceItem, InvoiceItemInterface $entry)
protected function mergeSumInvoiceItem(InvoiceItem $invoiceItem, ExportableItem $entry): void
{
if (null === $entry->getActivity()) {
return;
@@ -40,9 +40,6 @@ class ActivityInvoiceCalculator extends AbstractSumInvoiceCalculator implements
}
}
/**
* @return string
*/
public function getId(): string
{
return 'activity';

View File

@@ -9,15 +9,15 @@
namespace App\Invoice\Calculator;
use App\Entity\ExportableItem;
use App\Invoice\CalculatorInterface;
use App\Invoice\InvoiceItemInterface;
/**
* A calculator that sums up the invoice item records for each day.
*/
class DateInvoiceCalculator extends AbstractSumInvoiceCalculator implements CalculatorInterface
final class DateInvoiceCalculator extends AbstractSumInvoiceCalculator implements CalculatorInterface
{
protected function calculateSumIdentifier(InvoiceItemInterface $invoiceItem): string
protected function calculateSumIdentifier(ExportableItem $invoiceItem): string
{
if (null === $invoiceItem->getBegin()) {
throw new \Exception('Cannot handle invoice items without start date');
@@ -26,9 +26,6 @@ class DateInvoiceCalculator extends AbstractSumInvoiceCalculator implements Calc
return $invoiceItem->getBegin()->format('Y-m-d');
}
/**
* @return string
*/
public function getId(): string
{
return 'date';

View File

@@ -14,23 +14,26 @@ use App\Invoice\InvoiceItem;
/**
* Class DefaultCalculator works on all given entries using:
* - the customers currency
* - the customer currency
* - the invoice template vat rate
* - the entries rate
*/
class DefaultCalculator extends AbstractMergedCalculator implements CalculatorInterface
final class DefaultCalculator extends AbstractMergedCalculator implements CalculatorInterface
{
/**
* @return InvoiceItem[]
*/
public function getEntries()
public function getEntries(): array
{
$entries = [];
foreach ($this->model->getEntries() as $entry) {
$item = new InvoiceItem();
$this->mergeInvoiceItems($item, $entry);
foreach ($entry->getVisibleMetaFields() as $field) {
foreach ($entry->getMetaFields() as $field) {
if ($field->getName() === null) {
continue;
}
$item->addAdditionalField($field->getName(), $field->getValue());
}
$entries[] = $item;
@@ -39,9 +42,6 @@ class DefaultCalculator extends AbstractMergedCalculator implements CalculatorIn
return $entries;
}
/**
* @return string
*/
public function getId(): string
{
return 'default';

View File

@@ -9,15 +9,15 @@
namespace App\Invoice\Calculator;
use App\Entity\ExportableItem;
use App\Invoice\CalculatorInterface;
use App\Invoice\InvoiceItemInterface;
/**
* A calculator that sums up the invoice item records by price.
*/
class PriceInvoiceCalculator extends AbstractSumInvoiceCalculator implements CalculatorInterface
final class PriceInvoiceCalculator extends AbstractSumInvoiceCalculator implements CalculatorInterface
{
protected function calculateSumIdentifier(InvoiceItemInterface $invoiceItem): string
protected function calculateSumIdentifier(ExportableItem $invoiceItem): string
{
if (null !== $invoiceItem->getFixedRate()) {
return 'fixed_' . $invoiceItem->getFixedRate();
@@ -26,9 +26,6 @@ class PriceInvoiceCalculator extends AbstractSumInvoiceCalculator implements Cal
return 'hourly_' . $invoiceItem->getHourlyRate();
}
/**
* @return string
*/
public function getId(): string
{
return 'price';

View File

@@ -9,16 +9,16 @@
namespace App\Invoice\Calculator;
use App\Entity\ExportableItem;
use App\Invoice\CalculatorInterface;
use App\Invoice\InvoiceItem;
use App\Invoice\InvoiceItemInterface;
/**
* A calculator that sums up the invoice item records by project.
*/
class ProjectInvoiceCalculator extends AbstractSumInvoiceCalculator implements CalculatorInterface
final class ProjectInvoiceCalculator extends AbstractSumInvoiceCalculator implements CalculatorInterface
{
protected function calculateSumIdentifier(InvoiceItemInterface $invoiceItem): string
protected function calculateSumIdentifier(ExportableItem $invoiceItem): string
{
if (null === $invoiceItem->getProject()->getId()) {
throw new \Exception('Cannot handle un-persisted projects');
@@ -27,7 +27,7 @@ class ProjectInvoiceCalculator extends AbstractSumInvoiceCalculator implements C
return (string) $invoiceItem->getProject()->getId();
}
protected function mergeSumInvoiceItem(InvoiceItem $invoiceItem, InvoiceItemInterface $entry)
protected function mergeSumInvoiceItem(InvoiceItem $invoiceItem, ExportableItem $entry): void
{
if ($entry->getProject()->getInvoiceText() !== null) {
$invoiceItem->setDescription($entry->getProject()->getInvoiceText());
@@ -36,9 +36,6 @@ class ProjectInvoiceCalculator extends AbstractSumInvoiceCalculator implements C
}
}
/**
* @return string
*/
public function getId(): string
{
return 'project';

View File

@@ -16,12 +16,12 @@ use App\Invoice\InvoiceItem;
* A calculator that sums up all invoice item records from the model and returns only one
* entry for a compact invoice version.
*/
class ShortInvoiceCalculator extends AbstractMergedCalculator implements CalculatorInterface
final class ShortInvoiceCalculator extends AbstractMergedCalculator implements CalculatorInterface
{
/**
* @return InvoiceItem[]
*/
public function getEntries()
public function getEntries(): array
{
$entries = $this->model->getEntries();
if (empty($entries)) {
@@ -51,9 +51,6 @@ class ShortInvoiceCalculator extends AbstractMergedCalculator implements Calcula
return [$invoiceItem];
}
/**
* @return string
*/
public function getId(): string
{
return 'short';

View File

@@ -9,15 +9,15 @@
namespace App\Invoice\Calculator;
use App\Entity\ExportableItem;
use App\Invoice\CalculatorInterface;
use App\Invoice\InvoiceItemInterface;
/**
* A calculator that sums up the invoice item records by user.
*/
class UserInvoiceCalculator extends AbstractSumInvoiceCalculator implements CalculatorInterface
final class UserInvoiceCalculator extends AbstractSumInvoiceCalculator implements CalculatorInterface
{
protected function calculateSumIdentifier(InvoiceItemInterface $invoiceItem): string
protected function calculateSumIdentifier(ExportableItem $invoiceItem): string
{
if (null === $invoiceItem->getUser()->getId()) {
throw new \Exception('Cannot handle un-persisted user');
@@ -26,9 +26,6 @@ class UserInvoiceCalculator extends AbstractSumInvoiceCalculator implements Calc
return (string) $invoiceItem->getUser()->getId();
}
/**
* @return string
*/
public function getId(): string
{
return 'user';

View File

@@ -9,22 +9,19 @@
namespace App\Invoice\Calculator;
use App\Entity\ExportableItem;
use App\Invoice\CalculatorInterface;
use App\Invoice\InvoiceItemInterface;
/**
* A calculator that sums up the invoice item records per week.
*/
class WeeklyInvoiceCalculator extends AbstractSumInvoiceCalculator implements CalculatorInterface
final class WeeklyInvoiceCalculator extends AbstractSumInvoiceCalculator implements CalculatorInterface
{
protected function calculateSumIdentifier(InvoiceItemInterface $invoiceItem): string
protected function calculateSumIdentifier(ExportableItem $invoiceItem): string
{
return $invoiceItem->getBegin()->format('W');
}
/**
* @return string
*/
public function getId(): string
{
return 'weekly';

View File

@@ -19,14 +19,14 @@ interface CalculatorInterface
*
* @return InvoiceItem[]
*/
public function getEntries();
public function getEntries(): array;
/**
* Set the invoice model and can be used to fetch the customer.
*
* @param InvoiceModel $model
*/
public function setModel(InvoiceModel $model);
public function setModel(InvoiceModel $model): void;
/**
* Returns the subtotal before taxes.
@@ -49,20 +49,12 @@ interface CalculatorInterface
*/
public function getTotal(): float;
/**
* Returns the currency for the invoices amounts.
*
* @deprecated since 1.8 will be removed with 2.0
* @return string
*/
public function getCurrency(): string;
/**
* Returns the percentage for the value-added tax (VAT) calculation.
*
* @return float
*/
public function getVat(): ?float;
public function getVat(): float;
/**
* Returns the total amount of worked time in seconds.

View File

@@ -9,28 +9,24 @@
namespace App\Invoice;
use App\Configuration\LanguageFormattings;
use App\Configuration\LocaleService;
use App\Utils\LocaleFormatter;
final class DefaultInvoiceFormatter implements InvoiceFormatter
{
private $locale;
private $formats;
/**
* @var LocaleFormatter|null
*/
private $formatter;
public function __construct(LanguageFormattings $formats, string $locale)
public function __construct(private LocaleService $localeService, private string $locale)
{
$this->formats = $formats;
$this->locale = $locale;
}
private function getFormatter(): LocaleFormatter
{
if ($this->formatter === null) {
$this->formatter = new LocaleFormatter($this->formats, $this->locale);
$this->formatter = new LocaleFormatter($this->localeService, $this->locale);
}
return $this->formatter;
@@ -38,12 +34,12 @@ final class DefaultInvoiceFormatter implements InvoiceFormatter
public function getFormattedDateTime(\DateTime $date): string
{
return $this->getFormatter()->dateShort($date);
return (string) $this->getFormatter()->dateShort($date);
}
public function getFormattedTime(\DateTime $date): string
{
return $this->getFormatter()->time($date);
return (string) $this->getFormatter()->time($date);
}
public function getFormattedMonthName(\DateTime $date): string

View File

@@ -21,12 +21,7 @@ trait BudgetHydratorTrait
$budgetOpen = $statistic->getBudgetOpenRelative();
$budgetTimeOpen = $statistic->getTimeBudgetOpenRelative();
if ($model->getTemplate()->isDecimalDuration()) {
$budgetOpenDuration = $formatter->getFormattedDecimalDuration($budgetTimeOpen);
} else {
$budgetOpenDuration = $formatter->getFormattedDuration($budgetTimeOpen);
}
$budgetOpenDuration = $formatter->getFormattedDecimalDuration($budgetTimeOpen);
return [
$prefix . 'budget_open' => $formatter->getFormattedMoney($budgetOpen, $currency),

View File

@@ -13,12 +13,9 @@ use App\Invoice\InvoiceItem;
use App\Invoice\InvoiceItemHydrator;
use App\Invoice\InvoiceModel;
class InvoiceItemDefaultHydrator implements InvoiceItemHydrator
final class InvoiceItemDefaultHydrator implements InvoiceItemHydrator
{
/**
* @var InvoiceModel
*/
private $model;
private InvoiceModel $model;
public function setInvoiceModel(InvoiceModel $model)
{
@@ -32,11 +29,7 @@ class InvoiceItemDefaultHydrator implements InvoiceItemHydrator
$rate = $item->getRate();
$internalRate = $item->getInternalRate();
$appliedRate = $item->getHourlyRate();
if ($this->model->getTemplate()->isDecimalDuration()) {
$amount = $formatter->getFormattedDecimalDuration($item->getDuration());
} else {
$amount = $formatter->getFormattedDuration($item->getDuration());
}
$amount = $formatter->getFormattedDecimalDuration($item->getDuration());
$description = $item->getDescription();
if ($item->isFixedRate()) {
@@ -63,11 +56,11 @@ class InvoiceItemDefaultHydrator implements InvoiceItemHydrator
$values = [
'entry.row' => '',
'entry.description' => $description,
'entry.description' => $description ?? '',
'entry.amount' => $amount,
'entry.type' => $item->getType(),
'entry.tags' => implode(', ', $item->getTags()),
'entry.category' => $item->getCategory(),
'entry.category' => $item->getCategory() ?? '',
'entry.rate' => $formatter->getFormattedMoney($appliedRate, $currency),
'entry.rate_nc' => $formatter->getFormattedMoney($appliedRate, $currency, false),
'entry.rate_plain' => $appliedRate,
@@ -91,9 +84,9 @@ class InvoiceItemDefaultHydrator implements InvoiceItemHydrator
'entry.week' => \intval($begin->format('W')),
'entry.weekyear' => $begin->format('o'),
'entry.user_id' => $user->getId(),
'entry.user_name' => $user->getUsername(),
'entry.user_title' => $user->getTitle(),
'entry.user_alias' => $user->getAlias(),
'entry.user_name' => $user->getUserIdentifier(),
'entry.user_title' => $user->getTitle() ?? '',
'entry.user_alias' => $user->getAlias() ?? '',
];
if (null !== $activity) {

View File

@@ -14,31 +14,39 @@ use App\Entity\Activity;
use App\Invoice\InvoiceModel;
use App\Invoice\InvoiceModelHydrator;
class InvoiceModelActivityHydrator implements InvoiceModelHydrator
final class InvoiceModelActivityHydrator implements InvoiceModelHydrator
{
use BudgetHydratorTrait;
private $activityStatistic;
public function __construct(ActivityStatisticService $activityStatistic)
public function __construct(private ActivityStatisticService $activityStatistic)
{
$this->activityStatistic = $activityStatistic;
}
public function hydrate(InvoiceModel $model): array
{
if (!$model->getQuery()->hasActivities()) {
$activities = [];
foreach ($model->getEntries() as $entry) {
if ($entry->getActivity() === null) {
continue;
}
$key = 'A_' . $entry->getActivity()->getId();
if (!\array_key_exists($key, $activities)) {
$activities[$key] = $entry->getActivity();
}
}
if (\count($activities) === 0) {
return [];
}
$activities = array_values($activities);
$values = [];
$i = 0;
if (\count($model->getQuery()->getActivities()) === 1) {
$values['activity'] = $model->getQuery()->getActivities()[0]->getName();
}
foreach ($model->getQuery()->getActivities() as $activity) {
foreach ($activities as $activity) {
$prefix = '';
if ($i > 0) {
$prefix = $i . '.';
@@ -56,15 +64,17 @@ class InvoiceModelActivityHydrator implements InvoiceModelHydrator
$values = [
$prefix . 'id' => $activity->getId(),
$prefix . 'name' => $activity->getName(),
$prefix . 'comment' => $activity->getComment(),
$prefix . 'name' => $activity->getName() ?? '',
$prefix . 'comment' => $activity->getComment() ?? '',
];
$statistic = $this->activityStatistic->getBudgetStatisticModel($activity, $model->getQuery()->getEnd());
if ($model->getQuery()?->getEnd() !== null) {
$statistic = $this->activityStatistic->getBudgetStatisticModel($activity, $model->getQuery()->getEnd());
$values = array_merge($values, $this->getBudgetValues($prefix, $statistic, $model));
$values = array_merge($values, $this->getBudgetValues($prefix, $statistic, $model));
}
foreach ($activity->getVisibleMetaFields() as $metaField) {
foreach ($activity->getMetaFields() as $metaField) {
$values = array_merge($values, [
$prefix . 'meta.' . $metaField->getName() => $metaField->getValue(),
]);

View File

@@ -13,15 +13,12 @@ use App\Customer\CustomerStatisticService;
use App\Invoice\InvoiceModel;
use App\Invoice\InvoiceModelHydrator;
class InvoiceModelCustomerHydrator implements InvoiceModelHydrator
final class InvoiceModelCustomerHydrator implements InvoiceModelHydrator
{
use BudgetHydratorTrait;
private $customerStatistic;
public function __construct(CustomerStatisticService $customerStatistic)
public function __construct(private CustomerStatisticService $customerStatisticService)
{
$this->customerStatistic = $customerStatistic;
}
public function hydrate(InvoiceModel $model): array
@@ -34,22 +31,23 @@ class InvoiceModelCustomerHydrator implements InvoiceModelHydrator
$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.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.email' => $customer->getEmail(),
'customer.fax' => $customer->getFax(),
'customer.phone' => $customer->getPhone(),
'customer.mobile' => $customer->getMobile(),
'customer.homepage' => $customer->getHomepage() ?? '',
'customer.comment' => $customer->getComment() ?? '',
'customer.email' => $customer->getEmail() ?? '',
'customer.fax' => $customer->getFax() ?? '',
'customer.phone' => $customer->getPhone() ?? '',
'customer.mobile' => $customer->getMobile() ?? '',
'customer.invoice_text' => $customer->getInvoiceText() ?? '',
];
$statistic = $this->customerStatistic->getBudgetStatisticModel($customer, $model->getQuery()->getEnd());
$statistic = $this->customerStatisticService->getBudgetStatisticModel($customer, $model->getQuery()->getEnd());
$values = array_merge($values, $this->getBudgetValues('customer.', $statistic, $model));

View File

@@ -12,7 +12,7 @@ namespace App\Invoice\Hydrator;
use App\Invoice\InvoiceModel;
use App\Invoice\InvoiceModelHydrator;
class InvoiceModelDefaultHydrator implements InvoiceModelHydrator
final class InvoiceModelDefaultHydrator implements InvoiceModelHydrator
{
public function hydrate(InvoiceModel $model): array
{
@@ -45,21 +45,17 @@ class InvoiceModelDefaultHydrator implements InvoiceModelHydrator
'invoice.subtotal_nc' => $formatter->getFormattedMoney($subtotal, $currency, false),
'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.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(),
'template.vat_id' => $model->getTemplate()->getVatId() ?? '',
'template.contact' => $model->getTemplate()->getContact() ?? '',
'template.payment_details' => $model->getTemplate()->getPaymentDetails() ?? '',
'query.begin' => '',
'query.day' => '', // @deprecated
'query.month' => '', // @deprecated
'query.month_number' => '', // @deprecated
'query.year' => '', // @deprecated
'query.begin_day' => '',
'query.begin_month' => '',
'query.begin_month_number' => '',
@@ -73,11 +69,11 @@ class InvoiceModelDefaultHydrator implements InvoiceModelHydrator
if ($begin !== null) {
$values = array_merge($values, [
'query.day' => $begin->format('d'), // @deprecated - but impossible to delete
'query.month' => $formatter->getFormattedMonthName($begin), // @deprecated - but impossible to delete
'query.month_number' => $begin->format('m'), // @deprecated - but impossible to delete
'query.year' => $begin->format('Y'), // @deprecated - but impossible to delete
'query.begin' => $formatter->getFormattedDateTime($begin),
'query.day' => $begin->format('d'), // @deprecated
'query.month' => $formatter->getFormattedMonthName($begin), // @deprecated
'query.month_number' => $begin->format('m'), // @deprecated
'query.year' => $begin->format('Y'), // @deprecated
'query.begin_day' => $begin->format('d'),
'query.begin_month' => $formatter->getFormattedMonthName($begin),
'query.begin_month_number' => $begin->format('m'),

View File

@@ -14,31 +14,39 @@ use App\Invoice\InvoiceModel;
use App\Invoice\InvoiceModelHydrator;
use App\Project\ProjectStatisticService;
class InvoiceModelProjectHydrator implements InvoiceModelHydrator
final class InvoiceModelProjectHydrator implements InvoiceModelHydrator
{
use BudgetHydratorTrait;
private $projectStatistic;
public function __construct(ProjectStatisticService $projectStatistic)
public function __construct(private ProjectStatisticService $projectStatistic)
{
$this->projectStatistic = $projectStatistic;
}
public function hydrate(InvoiceModel $model): array
{
if (!$model->getQuery()->hasProjects()) {
$projects = [];
foreach ($model->getEntries() as $entry) {
if ($entry->getProject() === null) {
continue;
}
$key = 'P_' . $entry->getProject()->getId();
if (!\array_key_exists($key, $projects)) {
$projects[$key] = $entry->getProject();
}
}
if (\count($projects) === 0) {
return [];
}
$projects = array_values($projects);
$values = [];
$i = 0;
if (\count($model->getQuery()->getProjects()) === 1) {
$values['project'] = $model->getQuery()->getProjects()[0]->getName();
}
foreach ($model->getQuery()->getProjects() as $project) {
foreach ($projects as $project) {
$prefix = '';
if ($i > 0) {
$prefix = $i . '.';
@@ -59,8 +67,8 @@ class InvoiceModelProjectHydrator implements InvoiceModelHydrator
$values = [
$prefix . 'id' => $project->getId(),
$prefix . 'name' => $project->getName(),
$prefix . 'comment' => $project->getComment(),
$prefix . 'name' => $project->getName() ?? '',
$prefix . 'comment' => $project->getComment() ?? '',
$prefix . 'order_number' => $project->getOrderNumber(),
$prefix . 'start_date' => null !== $project->getStart() ? $formatter->getFormattedDateTime($project->getStart()) : '',
$prefix . 'end_date' => null !== $project->getEnd() ? $formatter->getFormattedDateTime($project->getEnd()) : '',
@@ -73,11 +81,13 @@ class InvoiceModelProjectHydrator implements InvoiceModelHydrator
$prefix . 'budget_time_minutes' => (int) ($project->getTimeBudget() / 60),
];
$statistic = $this->projectStatistic->getBudgetStatisticModel($project, $model->getQuery()->getEnd());
if ($model->getQuery()?->getEnd() !== null) {
$statistic = $this->projectStatistic->getBudgetStatisticModel($project, $model->getQuery()->getEnd());
$values = array_merge($values, $this->getBudgetValues($prefix, $statistic, $model));
$values = array_merge($values, $this->getBudgetValues($prefix, $statistic, $model));
}
foreach ($project->getVisibleMetaFields() as $metaField) {
foreach ($project->getMetaFields() as $metaField) {
$values = array_merge($values, [
$prefix . 'meta.' . $metaField->getName() => $metaField->getValue(),
]);

View File

@@ -13,7 +13,7 @@ use App\Entity\UserPreference;
use App\Invoice\InvoiceModel;
use App\Invoice\InvoiceModelHydrator;
class InvoiceModelUserHydrator implements InvoiceModelHydrator
final class InvoiceModelUserHydrator implements InvoiceModelHydrator
{
public function hydrate(InvoiceModel $model): array
{
@@ -24,10 +24,10 @@ class InvoiceModelUserHydrator implements InvoiceModelHydrator
}
$values = [
'user.name' => $user->getUsername(),
'user.name' => $user->getUserIdentifier(),
'user.email' => $user->getEmail(),
'user.title' => $user->getTitle(),
'user.alias' => $user->getAlias(),
'user.title' => $user->getTitle() ?? '',
'user.alias' => $user->getAlias() ?? '',
];
/** @var UserPreference $metaField */

View File

@@ -9,15 +9,11 @@
namespace App\Invoice;
use App\Entity\Project;
use App\Utils\FileHelper;
final class InvoiceFilename
{
/**
* @var string
*/
private $filename;
private string $filename;
public function __construct(InvoiceModel $model)
{
@@ -37,10 +33,7 @@ final class InvoiceFilename
if (null !== $model->getQuery()) {
$projects = $model->getQuery()->getProjects();
if (\count($projects) === 1) {
$pName = $projects[0];
if ($pName instanceof Project) {
$filename .= '-' . $this->convert($pName->getName());
}
$filename .= '-' . $this->convert($projects[0]->getName());
}
}
@@ -52,12 +45,12 @@ final class InvoiceFilename
return FileHelper::convertToAsciiFilename($filename);
}
public function getFilename()
public function getFilename(): string
{
return $this->filename;
}
public function __toString()
public function __toString(): string
{
return $this->getFilename();
}

View File

@@ -12,87 +12,47 @@ namespace App\Invoice;
use App\Entity\Activity;
use App\Entity\Project;
use App\Entity\User;
use DateTime;
final class InvoiceItem
{
/**
* @var float
*/
private $fixedRate;
/**
* @var float
*/
private $hourlyRate;
/**
* @var float
*/
private $rate = 0.00;
/**
* @var float
*/
private $rateInternal = 0.00;
/**
* @var float
*/
private $amount = 0;
/**
* @var string
*/
private $description;
/**
* @var int
*/
private $duration = 0;
/**
* @var \DateTime
*/
private $begin;
/**
* @var \DateTime
*/
private $end;
/**
* @var User
*/
private $user;
/**
* @var Activity
*/
private $activity;
/**
* @var Project
*/
private $project;
/**
* @var array
*/
private $additionalFields = [];
/**
* @var string
*/
private $type;
/**
* @var string
*/
private $category;
private ?float $fixedRate = null;
private ?float $hourlyRate = null;
private float $rate = 0.00;
private float $rateInternal = 0.00;
private float $amount = 0.00;
private ?string $description = null;
private int $duration = 0;
private ?DateTime $begin = null;
private ?DateTime $end = null;
private ?User $user = null;
private ?Activity $activity = null;
private ?Project $project = null;
/** @var array<string, mixed> */
private array $additionalFields = [];
private ?string $type = null;
private ?string $category = null;
/**
* @var string[]
*/
private $tags = [];
private array $tags = [];
public function addAdditionalField(string $name, ?string $value): InvoiceItem
public function addAdditionalField(string $name, mixed $value): InvoiceItem
{
$this->additionalFields[$name] = $value;
return $this;
}
/**
* @return array<string, mixed>
*/
public function getAdditionalFields(): array
{
return $this->additionalFields;
}
public function getAdditionalField(string $name, $default = null)
public function getAdditionalField(string $name, mixed $default = null): mixed
{
if (\array_key_exists($name, $this->additionalFields)) {
return $this->additionalFields[$name];
@@ -101,15 +61,6 @@ final class InvoiceItem
return $default;
}
public function getMetaFieldValue(string $field)
{
if (\array_key_exists($field, $this->additionalFields)) {
return $this->additionalFields[$field];
}
return null;
}
public function getActivity(): ?Activity
{
return $this->activity;

View File

@@ -1,63 +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;
use App\Entity\Activity;
use App\Entity\MetaTableTypeInterface;
use App\Entity\Project;
use App\Entity\User;
/**
* @method float|null getInternalRate()
*/
interface InvoiceItemInterface
{
public function getActivity(): ?Activity;
public function getProject(): ?Project;
public function getFixedRate(): ?float;
public function getHourlyRate(): ?float;
public function getRate(): float;
// will be activated with 2.0
// public function getInternalRate(): ?float;
public function getUser(): ?User;
public function getBegin(): ?\DateTime;
public function getEnd(): ?\DateTime;
public function getDuration(): ?int;
public function getDescription(): ?string;
/**
* @return MetaTableTypeInterface[]
*/
public function getVisibleMetaFields(): array;
/**
* A name representation for this type of item.
*
* @return string
*/
public function getType(): string;
/**
* A name representation for the category of this item.
*
* @return string
*/
public function getCategory(): string;
}

View File

@@ -9,19 +9,20 @@
namespace App\Invoice;
use App\Entity\ExportableItem;
use App\Repository\Query\InvoiceQuery;
interface InvoiceItemRepositoryInterface
{
/**
* @param InvoiceItemInterface[] $invoiceItems
* @param ExportableItem[] $invoiceItems
* @return void
*/
public function setExported(array $invoiceItems) /* : void */;
/**
* @param InvoiceQuery $query
* @return InvoiceItemInterface[]
* @return ExportableItem[]
*/
public function getInvoiceItemsForQuery(InvoiceQuery $query): iterable;
}

View File

@@ -1,18 +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;
/**
* Will be removed with 2.0, just here for BC compatibility.
*/
interface InvoiceItemWithAmountInterface
{
public function getAmount(): float;
}

View File

@@ -12,6 +12,7 @@ namespace App\Invoice;
use App\Activity\ActivityStatisticService;
use App\Customer\CustomerStatisticService;
use App\Entity\Customer;
use App\Entity\ExportableItem;
use App\Entity\InvoiceTemplate;
use App\Entity\User;
use App\Invoice\Hydrator\InvoiceItemDefaultHydrator;
@@ -29,58 +30,28 @@ use App\Repository\Query\InvoiceQuery;
*/
final class InvoiceModel
{
private ?Customer $customer = null;
private ?InvoiceQuery $query = null;
/**
* @var Customer|null
* @var ExportableItem[]
*/
private $customer;
/**
* @var InvoiceQuery
*/
private $query;
/**
* @var InvoiceItemInterface[]
*/
private $entries = [];
/**
* @var InvoiceTemplate
*/
private $template;
/**
* @var CalculatorInterface
*/
private $calculator;
/**
* @var NumberGeneratorInterface
*/
private $generator;
/**
* @var \DateTime
*/
private $invoiceDate;
/**
* @var User
*/
private $user;
/**
* @var InvoiceFormatter
*/
private $formatter;
private array $entries = [];
private ?InvoiceTemplate $template = null;
private ?CalculatorInterface $calculator = null;
private ?NumberGeneratorInterface $generator = null;
private \DateTime $invoiceDate;
private ?User $user = null;
private InvoiceFormatter $formatter;
/**
* @var InvoiceModelHydrator[]
*/
private $modelHydrator = [];
private array $modelHydrator = [];
/**
* @var InvoiceItemHydrator[]
*/
private $itemHydrator = [];
/**
* @var string
*/
private $invoiceNumber;
/**
* @var bool
*/
private $hideZeroTax = false;
private array $itemHydrator = [];
private ?string $invoiceNumber = null;
private bool $hideZeroTax = false;
/**
* @internal use InvoiceModelFactory
@@ -97,19 +68,12 @@ final class InvoiceModel
$this->addItemHydrator(new InvoiceItemDefaultHydrator());
}
/**
* @return InvoiceQuery
*/
public function getQuery(): ?InvoiceQuery
{
return $this->query;
}
/**
* @param InvoiceQuery $query
* @return InvoiceModel
*/
public function setQuery(InvoiceQuery $query)
public function setQuery(InvoiceQuery $query): InvoiceModel
{
$this->query = $query;
@@ -119,9 +83,9 @@ final class InvoiceModel
/**
* Returns the raw data from the model.
*
* Do not use this method for rendering the invoice, use getItems() instead.
* Do not use this method for rendering the invoice, use getCalculator()->getEntries() instead.
*
* @return InvoiceItemInterface[]
* @return ExportableItem[]
*/
public function getEntries(): array
{
@@ -129,21 +93,7 @@ final class InvoiceModel
}
/**
* @deprecated since 1.3 - will be removed with 2.0
* @param InvoiceItemInterface[] $entries
* @return InvoiceModel
*/
public function setEntries(array $entries): InvoiceModel
{
@trigger_error('setEntries() is deprecated and will be removed with 2.0', E_USER_DEPRECATED);
$this->entries = $entries;
return $this;
}
/**
* @param InvoiceItemInterface[] $entries
* @param ExportableItem[] $entries
* @return InvoiceModel
*/
public function addEntries(array $entries): InvoiceModel
@@ -186,11 +136,7 @@ final class InvoiceModel
return $this->customer;
}
/**
* @param Customer|null $customer
* @return InvoiceModel
*/
public function setCustomer($customer): InvoiceModel
public function setCustomer(?Customer $customer): InvoiceModel
{
$this->customer = $customer;
@@ -239,14 +185,6 @@ final class InvoiceModel
return $this;
}
/**
* @deprecated since 1.9 - will be removed with 2.0 - use getInvoiceNumber() instead
*/
public function getNumberGenerator(): ?NumberGeneratorInterface
{
return $this->generator;
}
public function setCalculator(CalculatorInterface $calculator): InvoiceModel
{
$this->calculator = $calculator;

View File

@@ -15,15 +15,11 @@ use App\Project\ProjectStatisticService;
final class InvoiceModelFactory
{
private $customerStatisticService;
private $projectStatisticService;
private $activityStatisticService;
public function __construct(CustomerStatisticService $customerStatistic, ProjectStatisticService $projectStatistic, ActivityStatisticService $activityStatistic)
{
$this->customerStatisticService = $customerStatistic;
$this->projectStatisticService = $projectStatistic;
$this->activityStatisticService = $activityStatistic;
public function __construct(
private CustomerStatisticService $customerStatisticService,
private ProjectStatisticService $projectStatisticService,
private ActivityStatisticService $activityStatisticService
) {
}
public function createModel(InvoiceFormatter $formatter): InvoiceModel

View File

@@ -13,40 +13,22 @@ use App\Configuration\SystemConfiguration;
use App\Invoice\InvoiceModel;
use App\Invoice\NumberGeneratorInterface;
use App\Repository\InvoiceRepository;
use App\Utils\NumberGenerator;
final class ConfigurableNumberGenerator implements NumberGeneratorInterface
{
/**
* @var InvoiceModel
*/
private $model;
/**
* @var InvoiceRepository
*/
private $repository;
/**
* @var SystemConfiguration
*/
private $configuration;
private ?InvoiceModel $model = null;
public function __construct(InvoiceRepository $repository, SystemConfiguration $configuration)
public function __construct(private InvoiceRepository $repository, private SystemConfiguration $configuration)
{
$this->repository = $repository;
$this->configuration = $configuration;
}
/**
* @return string
*/
public function getId(): string
{
return 'default';
}
/**
* @param InvoiceModel $model
*/
public function setModel(InvoiceModel $model)
public function setModel(InvoiceModel $model): void
{
$this->model = $model;
}
@@ -57,181 +39,53 @@ final class ConfigurableNumberGenerator implements NumberGeneratorInterface
public function getInvoiceNumber(): string
{
$format = $this->configuration->find('invoice.number_format');
if (empty($format) || !\is_string($format)) {
$format = '{Y}/{cy,3}';
}
$invoiceDate = $this->model->getInvoiceDate();
$loops = 0;
$increaseBy = 0;
do {
$result = $format;
preg_match_all('/{[^}]*?}/', $format, $matches);
foreach ($matches[0] as $part) {
$partialResult = $this->parseReplacer($invoiceDate, $part, $increaseBy);
$result = str_replace($part, $partialResult, $result);
$numberGenerator = new NumberGenerator($format, function (string $originalFormat, string $format, int $increaseBy) use ($invoiceDate): string|int {
if ($this->model === null) {
throw new \InvalidArgumentException('Missing invoice model, cannot calculate invoice number');
}
return match ($format) {
'Y' => $invoiceDate->format('Y'),
'y' => $invoiceDate->format('y'),
'M' => $invoiceDate->format('m'),
'm' => $invoiceDate->format('n'),
'D' => $invoiceDate->format('d'),
'd' => $invoiceDate->format('j'),
'date' => $invoiceDate->format('ymd'),
'cc' => $this->repository->getCounterForCustomerAllTime($this->model->getCustomer()) + $increaseBy,
'ccy' => $this->repository->getCounterForYear($invoiceDate, $this->model->getCustomer()) + $increaseBy,
'ccm' => $this->repository->getCounterForMonth($invoiceDate, $this->model->getCustomer()) + $increaseBy,
'ccd' => $this->repository->getCounterForDay($invoiceDate, $this->model->getCustomer()) + $increaseBy,
'cu' => $this->repository->getCounterForUserAllTime($this->model->getUser()) + $increaseBy,
'cuy' => $this->repository->getCounterForYear($invoiceDate, null, $this->model->getUser()) + $increaseBy,
'cum' => $this->repository->getCounterForMonth($invoiceDate, null, $this->model->getUser()) + $increaseBy,
'cud' => $this->repository->getCounterForDay($invoiceDate, null, $this->model->getUser()) + $increaseBy,
'ustaff' => (string) $this->model->getUser()?->getAccountNumber(),
'uid' => (string) $this->model->getUser()?->getId(),
'c' => $this->repository->getCounterForCustomerAllTime() + $increaseBy,
'cy' => $this->repository->getCounterForYear($invoiceDate) + $increaseBy,
'cm' => $this->repository->getCounterForMonth($invoiceDate) + $increaseBy,
'cd' => $this->repository->getCounterForDay($invoiceDate) + $increaseBy,
'cname' => (string) $this->model->getCustomer()?->getName(),
'cnumber' => (string) $this->model->getCustomer()?->getNumber(),
default => $originalFormat,
};
});
do {
$result = $numberGenerator->getNumber($increaseBy);
$increaseBy++;
} while ($this->repository->hasInvoice($result) && $loops++ < 99);
return (string) $result;
}
private function parseReplacer(\DateTime $invoiceDate, string $originalFormat, int $increaseBy): string
{
$formatterLength = null;
$formatPattern = str_replace(['{', '}'], '', $originalFormat);
$parts = preg_split('/([+\-,])+/', $formatPattern, -1, PREG_SPLIT_DELIM_CAPTURE);
$format = array_shift($parts);
if (\count($parts) % 2 !== 0) {
throw new \InvalidArgumentException('Invalid configuration found');
}
while (null !== ($tmp = array_shift($parts))) {
switch ($tmp) {
case '+':
$local = array_shift($parts);
if (!is_numeric($local)) {
throw new \InvalidArgumentException('Unknown increment found');
}
$increaseBy = $increaseBy + \intval($local);
break;
case '-':
$local = array_shift($parts);
if (!is_numeric($local)) {
throw new \InvalidArgumentException('Unknown decrement found');
}
$increaseBy = $increaseBy - \intval($local);
break;
case ',':
$local = array_shift($parts);
if (!is_numeric($local)) {
throw new \InvalidArgumentException('Unknown format length found');
}
$formatterLength = \intval($local);
if ((string) $formatterLength !== $local) {
throw new \InvalidArgumentException('Unknown format length found');
}
break;
default:
throw new \InvalidArgumentException('Unknown pattern found');
}
}
if ($increaseBy === 0) {
$increaseBy = 1;
}
switch ($format) {
case 'Y':
$partialResult = $invoiceDate->format('Y');
break;
case 'y':
$partialResult = $invoiceDate->format('y');
break;
case 'M':
$partialResult = $invoiceDate->format('m');
break;
case 'm':
$partialResult = $invoiceDate->format('n');
break;
case 'D':
$partialResult = $invoiceDate->format('d');
break;
case 'd':
$partialResult = $invoiceDate->format('j');
break;
case 'date':
$partialResult = $invoiceDate->format('ymd');
break;
// for customer
case 'cc':
$partialResult = $this->repository->getCounterForCustomerAllTime($this->model->getCustomer()) + $increaseBy;
break;
case 'ccy':
$partialResult = $this->repository->getCounterForYear($invoiceDate, $this->model->getCustomer()) + $increaseBy;
break;
case 'ccm':
$partialResult = $this->repository->getCounterForMonth($invoiceDate, $this->model->getCustomer()) + $increaseBy;
break;
case 'ccd':
$partialResult = $this->repository->getCounterForDay($invoiceDate, $this->model->getCustomer()) + $increaseBy;
break;
// for user
case 'cu':
$partialResult = $this->repository->getCounterForUserAllTime($this->model->getUser()) + $increaseBy;
break;
case 'cuy':
$partialResult = $this->repository->getCounterForYear($invoiceDate, null, $this->model->getUser()) + $increaseBy;
break;
case 'cum':
$partialResult = $this->repository->getCounterForMonth($invoiceDate, null, $this->model->getUser()) + $increaseBy;
break;
case 'cud':
$partialResult = $this->repository->getCounterForDay($invoiceDate, null, $this->model->getUser()) + $increaseBy;
break;
case 'ustaff':
$partialResult = $this->model->getUser() !== null ? $this->model->getUser()->getAccountNumber() : '';
break;
case 'uid':
$partialResult = $this->model->getUser() !== null ? (string) $this->model->getUser()->getId() : '';
break;
// across all invoices
case 'c':
$partialResult = $this->repository->getCounterForCustomerAllTime() + $increaseBy;
break;
case 'cy':
$partialResult = $this->repository->getCounterForYear($invoiceDate) + $increaseBy;
break;
case 'cm':
$partialResult = $this->repository->getCounterForMonth($invoiceDate) + $increaseBy;
break;
case 'cd':
$partialResult = $this->repository->getCounterForDay($invoiceDate) + $increaseBy;
break;
case 'cname':
$partialResult = $this->model->getCustomer() !== null ? $this->model->getCustomer()->getName() : '';
break;
case 'cnumber':
$partialResult = $this->model->getCustomer() !== null ? $this->model->getCustomer()->getNumber() : '';
break;
default:
$partialResult = $originalFormat;
}
if (null !== $formatterLength) {
$partialResult = str_pad($partialResult, $formatterLength, '0', STR_PAD_LEFT);
}
return $partialResult;
return $result;
}
}

View File

@@ -19,32 +19,18 @@ use App\Repository\InvoiceRepository;
*/
final class DateNumberGenerator implements NumberGeneratorInterface
{
/**
* @var InvoiceModel
*/
private $model;
/**
* @var InvoiceRepository
*/
private $repository;
private ?InvoiceModel $model = null;
public function __construct(InvoiceRepository $repository)
public function __construct(private InvoiceRepository $repository)
{
$this->repository = $repository;
}
/**
* @return string
*/
public function getId(): string
{
return 'date';
}
/**
* @param InvoiceModel $model
*/
public function setModel(InvoiceModel $model)
public function setModel(InvoiceModel $model): void
{
$this->model = $model;
}

View File

@@ -14,14 +14,8 @@ namespace App\Invoice;
*/
interface NumberGeneratorInterface
{
/**
* @param InvoiceModel $model
*/
public function setModel(InvoiceModel $model);
public function setModel(InvoiceModel $model): void;
/**
* @return string
*/
public function getInvoiceNumber(): string;
/**

View File

@@ -9,9 +9,9 @@
namespace App\Invoice\Renderer;
use App\Entity\InvoiceDocument;
use App\Invoice\InvoiceFilename;
use App\Invoice\InvoiceModel;
use App\Model\InvoiceDocument;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;

View File

@@ -9,8 +9,8 @@
namespace App\Invoice\Renderer;
use App\Entity\InvoiceDocument;
use App\Invoice\InvoiceModel;
use App\Model\InvoiceDocument;
use PhpOffice\PhpSpreadsheet\Cell\Cell;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
use PhpOffice\PhpSpreadsheet\IOFactory;
@@ -67,10 +67,10 @@ abstract class AbstractSpreadsheetRenderer extends AbstractRenderer
$sheetValues = false;
foreach ($row->getCellIterator() as $cell) {
$value = $cell->getValue();
$replacer = null;
if ($value === null) {
continue;
}
$replacer = null;
$firstReplacerPos = stripos($value, '${');
if ($firstReplacerPos === false) {
continue;
@@ -98,7 +98,7 @@ abstract class AbstractSpreadsheetRenderer extends AbstractRenderer
if (stripos($value, $searchKey) === false) {
continue;
}
if (\is_string($content) && $content[0] === '=') {
if (\is_string($content) && str_starts_with($content, '=')) {
$contentLooksLikeFormula = true;
}
$value = str_replace($searchKey, $content, $value);

View File

@@ -9,9 +9,9 @@
namespace App\Invoice\Renderer;
use App\Entity\InvoiceDocument;
use App\Invoice\InvoiceModel;
use App\Invoice\RendererInterface;
use App\Model\InvoiceDocument;
use App\Twig\TwigRendererTrait;
use Twig\Environment;
@@ -22,14 +22,8 @@ abstract class AbstractTwigRenderer implements RendererInterface
{
use TwigRendererTrait;
/**
* @var Environment
*/
private $twig;
public function __construct(Environment $twig)
public function __construct(private Environment $twig)
{
$this->twig = $twig;
}
protected function renderTwigTemplate(InvoiceDocument $document, InvoiceModel $model, array $options = []): string

View File

@@ -16,7 +16,7 @@ use PhpOffice\PhpSpreadsheet\Cell\IValueBinder;
use PhpOffice\PhpSpreadsheet\RichText\RichText;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
class AdvancedValueBinder extends DefaultValueBinder implements IValueBinder
final class AdvancedValueBinder extends DefaultValueBinder implements IValueBinder
{
/**
* Bind value to a cell.
@@ -38,7 +38,7 @@ class AdvancedValueBinder extends DefaultValueBinder implements IValueBinder
if ($dataType === DataType::TYPE_STRING && !$value instanceof RichText) {
// Check for newline character "\n"
if (strpos($value, "\n") !== false) {
if (\is_string($value) && str_contains($value, "\n")) {
$cell->setValueExplicit($value, DataType::TYPE_STRING);
$cell->getWorksheet()->getStyle($cell->getCoordinate())->getAlignment()->setWrapText(true);

View File

@@ -9,9 +9,9 @@
namespace App\Invoice\Renderer;
use App\Entity\InvoiceDocument;
use App\Invoice\InvoiceModel;
use App\Invoice\RendererInterface;
use App\Model\InvoiceDocument;
use PhpOffice\PhpWord\Escaper\Xml;
use PhpOffice\PhpWord\Exception\Exception as OfficeException;
use PhpOffice\PhpWord\Settings;
@@ -21,11 +21,6 @@ use Symfony\Component\HttpFoundation\Response;
final class DocxRenderer extends AbstractRenderer implements RendererInterface
{
/**
* @param InvoiceDocument $document
* @param InvoiceModel $model
* @return Response
*/
public function render(InvoiceDocument $document, InvoiceModel $model): Response
{
Settings::setOutputEscapingEnabled(false);
@@ -46,9 +41,7 @@ final class DocxRenderer extends AbstractRenderer implements RendererInterface
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())
);
@trigger_error('Invoice document did not contain a clone row, was that on purpose?');
}
}
@@ -73,18 +66,12 @@ final class DocxRenderer extends AbstractRenderer implements RendererInterface
return $this->getFileResponse(new Stream($cacheFile), $filename);
}
/**
* @return string[]
*/
protected function getFileExtensions()
protected function getFileExtensions(): array
{
return ['.docx'];
}
/**
* @return string
*/
protected function getContentType()
protected function getContentType(): string
{
return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
}

View File

@@ -1,39 +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\Renderer;
use App\Entity\InvoiceDocument;
use App\Invoice\InvoiceFilename;
use App\Invoice\InvoiceModel;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
final class JsonRenderer extends AbstractTwigRenderer
{
public function supports(InvoiceDocument $document): bool
{
return stripos($document->getFilename(), '.json.twig') !== false;
}
public function render(InvoiceDocument $document, InvoiceModel $model): Response
{
$content = $this->renderTwigTemplate($document, $model);
$filename = (string) new InvoiceFilename($model);
$response = new Response($content);
$disposition = $response->headers->makeDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $filename . '.json');
$response->headers->set('Content-Type', 'application/json');
$response->headers->set('Content-Disposition', $disposition);
return $response;
}
}

View File

@@ -15,28 +15,17 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet;
final class OdsRenderer extends AbstractSpreadsheetRenderer implements RendererInterface
{
/**
* @return string[]
*/
protected function getFileExtensions()
protected function getFileExtensions(): array
{
return ['.ods'];
}
/**
* @return string
*/
protected function getContentType()
protected function getContentType(): string
{
return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
}
/**
* @param Spreadsheet $spreadsheet
* @return string
* @throws \Exception
*/
protected function saveSpreadsheet(Spreadsheet $spreadsheet)
protected function saveSpreadsheet(Spreadsheet $spreadsheet): string
{
$filename = @tempnam(sys_get_temp_dir(), 'kimai-invoice-ods');
if (false === $filename) {

View File

@@ -9,29 +9,23 @@
namespace App\Invoice\Renderer;
use App\Entity\InvoiceDocument;
use App\Export\Base\DispositionInlineInterface;
use App\Export\Base\DispositionInlineTrait;
use App\Export\ExportContext;
use App\Invoice\InvoiceFilename;
use App\Invoice\InvoiceModel;
use App\Utils\HtmlToPdfConverter;
use App\Model\InvoiceDocument;
use App\Pdf\HtmlToPdfConverter;
use App\Pdf\PdfContext;
use App\Pdf\PdfRendererTrait;
use Symfony\Component\HttpFoundation\Response;
use Twig\Environment;
final class PdfRenderer extends AbstractTwigRenderer implements DispositionInlineInterface
{
use DispositionInlineTrait;
use PDFRendererTrait;
/**
* @var HtmlToPdfConverter
*/
private $converter;
public function __construct(Environment $twig, HtmlToPdfConverter $converter)
public function __construct(Environment $twig, private HtmlToPdfConverter $converter)
{
parent::__construct($twig);
$this->converter = $converter;
}
public function supports(InvoiceDocument $document): bool
@@ -43,7 +37,7 @@ final class PdfRenderer extends AbstractTwigRenderer implements DispositionInlin
{
$filename = new InvoiceFilename($model);
$context = new ExportContext();
$context = new PdfContext();
$context->setOption('filename', $filename->getFilename());
$context->setOption('setAutoTopMargin', 'pad');
$context->setOption('setAutoBottomMargin', 'pad');
@@ -53,19 +47,6 @@ final class PdfRenderer extends AbstractTwigRenderer implements DispositionInlin
$content = $this->renderTwigTemplate($document, $model, ['pdfContext' => $context]);
$content = $this->converter->convertToPdf($content, $context->getOptions());
$filename = $context->getOption('filename');
if (empty($filename)) {
$filename = new InvoiceFilename($model);
$filename = $filename->getFilename();
}
$response = new Response($content);
$disposition = $response->headers->makeDisposition($this->getDisposition(), $filename . '.pdf');
$response->headers->set('Content-Type', 'application/pdf');
$response->headers->set('Content-Disposition', $disposition);
return $response;
return $this->createPdfResponse($content, $context);
}
}

View File

@@ -1,57 +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\Renderer;
use App\Invoice\InvoiceItem;
use App\Invoice\InvoiceModel;
trait RendererTrait
{
/**
* @var InvoiceModel
*/
private $model;
/**
* @deprecated since 1.6.2 - will be removed with 2.0
* @param InvoiceModel $model
* @return array
*/
protected function modelToReplacer(InvoiceModel $model)
{
@trigger_error('modelToReplacer() is deprecated and will be removed with 2.0', E_USER_DEPRECATED);
$this->model = $model;
return $model->toArray();
}
/**
* @deprecated since 1.3 - will be removed with 2.0
*/
protected function timesheetToArray(InvoiceItem $invoiceItem): array
{
@trigger_error('timesheetToArray() is deprecated and will be removed with 2.0', E_USER_DEPRECATED);
return $this->model->itemToArray($invoiceItem);
}
/**
* @deprecated since 1.6.2 - will be removed with 2.0
* @param InvoiceItem $invoiceItem
* @return array
*/
protected function invoiceItemToArray(InvoiceItem $invoiceItem): array
{
@trigger_error('invoiceItemToArray() is deprecated and will be removed with 2.0', E_USER_DEPRECATED);
return $this->model->itemToArray($invoiceItem);
}
}

View File

@@ -1,39 +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\Renderer;
use App\Entity\InvoiceDocument;
use App\Invoice\InvoiceFilename;
use App\Invoice\InvoiceModel;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
final class TextRenderer extends AbstractTwigRenderer
{
public function supports(InvoiceDocument $document): bool
{
return stripos($document->getFilename(), '.txt.twig') !== false;
}
public function render(InvoiceDocument $document, InvoiceModel $model): Response
{
$content = $this->renderTwigTemplate($document, $model);
$filename = (string) new InvoiceFilename($model);
$response = new Response($content);
$disposition = $response->headers->makeDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $filename . '.txt');
$response->headers->set('Content-Type', 'text/plain');
$response->headers->set('Content-Disposition', $disposition);
return $response;
}
}

View File

@@ -9,8 +9,8 @@
namespace App\Invoice\Renderer;
use App\Entity\InvoiceDocument;
use App\Invoice\InvoiceModel;
use App\Model\InvoiceDocument;
use Symfony\Component\HttpFoundation\Response;
final class TwigRenderer extends AbstractTwigRenderer

View File

@@ -15,27 +15,16 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet;
final class XlsxRenderer extends AbstractSpreadsheetRenderer implements RendererInterface
{
/**
* @return string[]
*/
protected function getFileExtensions()
protected function getFileExtensions(): array
{
return ['.xlsx', '.xls'];
}
/**
* @return string
*/
protected function getContentType()
protected function getContentType(): string
{
return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
}
/**
* @param Spreadsheet $spreadsheet
* @return string
* @throws \Exception
*/
protected function saveSpreadsheet(Spreadsheet $spreadsheet)
{
$filename = @tempnam(sys_get_temp_dir(), 'kimai-invoice-xlsx');

View File

@@ -1,39 +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\Renderer;
use App\Entity\InvoiceDocument;
use App\Invoice\InvoiceFilename;
use App\Invoice\InvoiceModel;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
final class XmlRenderer extends AbstractTwigRenderer
{
public function supports(InvoiceDocument $document): bool
{
return stripos($document->getFilename(), '.xml.twig') !== false;
}
public function render(InvoiceDocument $document, InvoiceModel $model): Response
{
$content = $this->renderTwigTemplate($document, $model);
$filename = (string) new InvoiceFilename($model);
$response = new Response($content);
$disposition = $response->headers->makeDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $filename . '.xml');
$response->headers->set('Content-Type', 'application/xml');
$response->headers->set('Content-Disposition', $disposition);
return $response;
}
}

View File

@@ -9,7 +9,7 @@
namespace App\Invoice;
use App\Entity\InvoiceDocument;
use App\Model\InvoiceDocument;
use Symfony\Component\HttpFoundation\Response;
interface RendererInterface

View File

@@ -9,20 +9,18 @@
namespace App\Invoice;
use App\Configuration\LanguageFormattings;
use App\Constants;
use App\Entity\Customer;
use App\Configuration\LocaleService;
use App\Entity\ExportableItem;
use App\Entity\Invoice;
use App\Entity\InvoiceDocument;
use App\Event\InvoiceCreatedEvent;
use App\Event\InvoiceDeleteEvent;
use App\Event\InvoicePostRenderEvent;
use App\Event\InvoicePreRenderEvent;
use App\Export\Base\DispositionInlineInterface;
use App\Model\InvoiceDocument;
use App\Repository\InvoiceDocumentRepository;
use App\Repository\InvoiceRepository;
use App\Repository\Query\InvoiceQuery;
use App\Timesheet\DateTimeFactory;
use App\Utils\FileHelper;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\Response;
@@ -36,32 +34,27 @@ final class ServiceInvoice
/**
* @var CalculatorInterface[]
*/
private $calculator = [];
private array $calculator = [];
/**
* @var RendererInterface[]
*/
private $renderer = [];
private array $renderer = [];
/**
* @var NumberGeneratorInterface[]
*/
private $numberGenerator = [];
private array $numberGenerator = [];
/**
* @var array InvoiceItemRepositoryInterface[]
*/
private $invoiceItemRepositories = [];
private $documents;
private $fileHelper;
private $formatter;
private $invoiceRepository;
private $invoiceModelFactory;
private array $invoiceItemRepositories = [];
public function __construct(InvoiceDocumentRepository $repository, FileHelper $fileHelper, InvoiceRepository $invoiceRepository, LanguageFormattings $formatter, InvoiceModelFactory $invoiceModelFactory)
{
$this->documents = $repository;
$this->fileHelper = $fileHelper;
$this->invoiceRepository = $invoiceRepository;
$this->formatter = $formatter;
$this->invoiceModelFactory = $invoiceModelFactory;
public function __construct(
private InvoiceDocumentRepository $documents,
private FileHelper $fileHelper,
private InvoiceRepository $invoiceRepository,
private LocaleService $formatter,
private InvoiceModelFactory $invoiceModelFactory
) {
}
public function addNumberGenerator(NumberGeneratorInterface $generator): ServiceInvoice
@@ -258,24 +251,7 @@ final class ServiceInvoice
/**
* @param InvoiceQuery $query
* @return InvoiceItemInterface[]
* @deprecated since 1.14 and will be removed with 2.0
*/
public function findInvoiceItems(InvoiceQuery $query): array
{
@trigger_error('Using findInvoiceItems() is deprecated since 1.14 and will be removed with 2.0', E_USER_DEPRECATED);
// customer needs to be defined, as we need the currency for the invoice
if (!$query->hasCustomers()) {
return [];
}
return $this->getInvoiceItems($query);
}
/**
* @param InvoiceQuery $query
* @return InvoiceItemInterface[]
* @return ExportableItem[]
*/
public function getInvoiceItems(InvoiceQuery $query): array
{
@@ -288,21 +264,8 @@ final class ServiceInvoice
return $items;
}
private function getDateTimeFactory(InvoiceQuery $query): DateTimeFactory
{
$timezone = date_default_timezone_get();
$sunday = false;
if (null !== ($user = $query->getCurrentUser())) {
$timezone = $user->getTimezone();
$sunday = $user->isFirstDayOfWeekSunday();
}
return new DateTimeFactory(new \DateTimeZone($timezone), $sunday);
}
/**
* @param InvoiceItemInterface[] $entries
* @param ExportableItem[] $entries
*/
private function markEntriesAsExported(array $entries)
{
@@ -311,7 +274,7 @@ final class ServiceInvoice
}
}
public function renderInvoiceWithModel(InvoiceModel $model, EventDispatcherInterface $dispatcher, bool $dispositionInline = false): Response
public function renderInvoice(InvoiceModel $model, EventDispatcherInterface $dispatcher, bool $dispositionInline = false): Response
{
$document = $this->getDocumentByName($model->getTemplate()->getRenderer());
if (null === $document) {
@@ -339,20 +302,13 @@ final class ServiceInvoice
);
}
public function renderInvoice(InvoiceQuery $query, EventDispatcherInterface $dispatcher): Response
{
$model = $this->createModel($query);
return $this->renderInvoiceWithModel($model, $dispatcher);
}
/**
* @param InvoiceModel $model
* @param EventDispatcherInterface $dispatcher
* @return Invoice
* @throws \Exception
*/
public function createInvoiceFromModel(InvoiceModel $model, EventDispatcherInterface $dispatcher): Invoice
public function createInvoice(InvoiceModel $model, EventDispatcherInterface $dispatcher): Invoice
{
$document = $this->getDocumentByName($model->getTemplate()->getRenderer());
if (null === $document) {
@@ -382,12 +338,13 @@ final class ServiceInvoice
$invoice = new Invoice();
$invoice->setModel($model);
$invoice->setFilename($invoiceFilename);
if (!$invoice->getCustomer()->hasInvoiceTemplate()) {
$invoice->getCustomer()->setInvoiceTemplate($model->getTemplate());
}
$this->invoiceRepository->saveInvoice($invoice);
if ($model->getQuery()->isMarkAsExported()) {
$this->markEntriesAsExported($model->getEntries());
}
$this->markEntriesAsExported($model->getEntries());
$dispatcher->dispatch(new InvoiceCreatedEvent($invoice, $model));
return $invoice;
@@ -399,37 +356,6 @@ final class ServiceInvoice
);
}
/**
* @param InvoiceQuery $query
* @param EventDispatcherInterface $dispatcher
* @return Invoice[]
* @throws \Exception
*/
public function createInvoices(InvoiceQuery $query, EventDispatcherInterface $dispatcher): array
{
$invoices = [];
$models = $this->createModels($query);
foreach ($models as $model) {
$invoices[] = $this->createInvoiceFromModel($model, $dispatcher);
}
return $invoices;
}
/**
* @param InvoiceQuery $query
* @param EventDispatcherInterface $dispatcher
* @return Invoice
* @throws \Exception
*/
public function createInvoice(InvoiceQuery $query, EventDispatcherInterface $dispatcher): Invoice
{
$model = $this->createModel($query);
return $this->createInvoiceFromModel($model, $dispatcher);
}
public function deleteInvoice(Invoice $invoice, EventDispatcherInterface $dispatcher)
{
$invoiceDirectory = $this->getInvoicesDirectory();
@@ -461,47 +387,46 @@ final class ServiceInvoice
private function createModelWithoutEntries(InvoiceQuery $query): InvoiceModel
{
$customer = $query->getCustomer();
if ($customer === null) {
throw new \Exception('Cannot create invoice model without customer');
}
$template = $query->getTemplate();
if (!$query->hasCustomers()) {
throw new \Exception('Cannot create invoice model without customer');
if ($query->isAllowTemplateOverwrite() && $customer->hasInvoiceTemplate()) {
$template = $customer->getInvoiceTemplate();
}
if (null === $template) {
throw new \Exception('Cannot create invoice model without template');
}
// prevent that changes on the template will be persisted
$this->invoiceRepository->preventTemplateUpdate($template);
if (null === $template->getLanguage()) {
$template->setLanguage(Constants::DEFAULT_LOCALE);
@trigger_error('Using invoice templates without a language is is deprecated and trigger and will throw an exception with 2.0', E_USER_DEPRECATED);
}
$formatter = new DefaultInvoiceFormatter($this->formatter, $template->getLanguage());
$model = $this->invoiceModelFactory->createModel($formatter);
$model
->setCustomer($customer)
->setTemplate($template)
->setInvoiceDate($this->getDateTimeFactory($query)->createDateTime())
->setQuery($query)
;
if ($query->getInvoiceDate() !== null) {
$model->setInvoiceDate($query->getInvoiceDate());
}
if (null !== $query->getCurrentUser()) {
$model->setUser($query->getCurrentUser());
}
$model->setCustomer($query->getCustomers()[0]);
$generator = $this->getNumberGeneratorByName($query->getTemplate()->getNumberGenerator());
$generator = $this->getNumberGeneratorByName($template->getNumberGenerator());
if (null === $generator) {
throw new \Exception('Unknown number generator: ' . $query->getTemplate()->getNumberGenerator());
throw new \Exception('Unknown number generator: ' . $template->getNumberGenerator());
}
$calculator = $this->getCalculatorByName($query->getTemplate()->getCalculator());
$calculator = $this->getCalculatorByName($template->getCalculator());
if (null === $calculator) {
throw new \Exception('Unknown invoice calculator: ' . $query->getTemplate()->getCalculator());
throw new \Exception('Unknown invoice calculator: ' . $template->getCalculator());
}
$model->setCalculator($calculator);
@@ -510,10 +435,10 @@ final class ServiceInvoice
return $model;
}
private function prepareModelQueryDates(InvoiceModel $model)
private function prepareModelQueryDates(InvoiceModel $model): void
{
$begin = $model->getQuery()->getBegin();
$end = $model->getQuery()->getEnd();
$begin = $model->getQuery()?->getBegin();
$end = $model->getQuery()?->getEnd();
if ($begin !== null && $end !== null) {
return;
@@ -566,6 +491,9 @@ final class ServiceInvoice
foreach ($items as $entry) {
$customer = $entry->getProject()->getCustomer();
if ($customer === null || !$customer->isVisible()) { // generating invoices for hidden customers does not yet work
continue;
}
$id = $customer->getId();
if (!\array_key_exists($id, $customerEntries)) {
$customerEntries[$id] = [
@@ -580,11 +508,9 @@ final class ServiceInvoice
return [];
}
uasort($customerEntries, function ($a, $b) {
$customerA = $a['customer'] ?? null;
$customerB = $b['customer'] ?? null;
$nameA = ($customerA instanceof Customer) ? $customerA->getName() : null;
$nameB = ($customerB instanceof Customer) ? $customerB->getName() : null;
uasort($customerEntries, function ($a, $b): int {
$nameA = $a['customer']->getName();
$nameB = $b['customer']->getName();
if ($nameA === null && $nameB === null) {
$result = 0;
@@ -599,7 +525,7 @@ final class ServiceInvoice
return $result;
});
foreach ($customerEntries as $id => $settings) {
foreach ($customerEntries as $settings) {
$customerQuery = clone $query;
$customerQuery->setCustomers([$settings['customer']]);
$model = $this->createModelWithoutEntries($customerQuery);