support multiple invoice repositories (#1084)

This commit is contained in:
Kevin Papst
2019-09-04 22:50:53 +02:00
committed by GitHub
parent 3f27ab1e44
commit 3ea05cb705
29 changed files with 323 additions and 144 deletions

View File

@@ -13,6 +13,7 @@ Perform EACH version specific task between your version and the new one, otherwi
### Possible BC breaks ### Possible BC breaks
- Refactored toolbars and search, plugins needs to be checked - Refactored toolbars and search, plugins needs to be checked
- Invoices now supports multiple repositories, some method signatures had to be changed (eg. `calculateSumIdentifier()`)
## [1.2](https://github.com/kevinpapst/kimai2/releases/tag/1.2) ## [1.2](https://github.com/kevinpapst/kimai2/releases/tag/1.2)

View File

@@ -5,7 +5,7 @@
"build/runtime.4ee6be68.js", "build/runtime.4ee6be68.js",
"build/0.a87622f3.js", "build/0.a87622f3.js",
"build/1.c1bee41f.js", "build/1.c1bee41f.js",
"build/app.dacf60f7.js" "build/app.75bdc057.js"
], ],
"css": [ "css": [
"build/app.eb4ed947.css" "build/app.eb4ed947.css"
@@ -35,7 +35,7 @@
"build/runtime.4ee6be68.js": "sha384-xNNrNinl64G3nCUrIskgSjU0mUXXCB9lj6XCSInBTwxSKXk8uTMafnLHtdWdIGtd", "build/runtime.4ee6be68.js": "sha384-xNNrNinl64G3nCUrIskgSjU0mUXXCB9lj6XCSInBTwxSKXk8uTMafnLHtdWdIGtd",
"build/0.a87622f3.js": "sha384-ncT/BKhCsqH6jhxwdsSG95m1ei7ZZjeZtzH1262h+OPUU80TSFFE3dt+abcHHMok", "build/0.a87622f3.js": "sha384-ncT/BKhCsqH6jhxwdsSG95m1ei7ZZjeZtzH1262h+OPUU80TSFFE3dt+abcHHMok",
"build/1.c1bee41f.js": "sha384-7UVWcP6Hefp2k/CrtGSITKXx4dSZqtvpAiU8WX7dClETkzMewrUjoCRtVXQ5j3KI", "build/1.c1bee41f.js": "sha384-7UVWcP6Hefp2k/CrtGSITKXx4dSZqtvpAiU8WX7dClETkzMewrUjoCRtVXQ5j3KI",
"build/app.dacf60f7.js": "sha384-a93v/mUzZDopA6To/YJEjOAxgJ6T4H6gmXnGf0ja6Bq4M7ZVDQqqKMX7/9hIhRPM", "build/app.75bdc057.js": "sha384-a93v/mUzZDopA6To/YJEjOAxgJ6T4H6gmXnGf0ja6Bq4M7ZVDQqqKMX7/9hIhRPM",
"build/app.eb4ed947.css": "sha384-TQ5nEns/+JBxjuHlqGXkiS1qt9Zdzb7tREhc4DPunQkTib/iuv8MfwELt8OPhF10", "build/app.eb4ed947.css": "sha384-TQ5nEns/+JBxjuHlqGXkiS1qt9Zdzb7tREhc4DPunQkTib/iuv8MfwELt8OPhF10",
"build/2.7be60d8d.js": "sha384-txR0QG+838LKYtPQ99Gx4OU7WmgN9J3joZEyGwIskSz74EN1T4/IBVnmNaKiFN1q", "build/2.7be60d8d.js": "sha384-txR0QG+838LKYtPQ99Gx4OU7WmgN9J3joZEyGwIskSz74EN1T4/IBVnmNaKiFN1q",
"build/chart.0af3f813.js": "sha384-I57c9DtU3AOG2kzKqIZkIu0hi1aGYHRZ5QG4LKC9+9slzJnAMttPGXoL2cQG3m6y", "build/chart.0af3f813.js": "sha384-I57c9DtU3AOG2kzKqIZkIu0hi1aGYHRZ5QG4LKC9+9slzJnAMttPGXoL2cQG3m6y",

View File

@@ -3,7 +3,7 @@
"build/1.c1bee41f.js": "build/1.c1bee41f.js", "build/1.c1bee41f.js": "build/1.c1bee41f.js",
"build/2.7be60d8d.js": "build/2.7be60d8d.js", "build/2.7be60d8d.js": "build/2.7be60d8d.js",
"build/app.css": "build/app.eb4ed947.css", "build/app.css": "build/app.eb4ed947.css",
"build/app.js": "build/app.dacf60f7.js", "build/app.js": "build/app.75bdc057.js",
"build/calendar.css": "build/calendar.b0551848.css", "build/calendar.css": "build/calendar.b0551848.css",
"build/calendar.js": "build/calendar.5839778f.js", "build/calendar.js": "build/calendar.5839778f.js",
"build/chart.js": "build/chart.0af3f813.js", "build/chart.js": "build/chart.0af3f813.js",

View File

@@ -10,15 +10,14 @@
namespace App\Controller; namespace App\Controller;
use App\Entity\InvoiceTemplate; use App\Entity\InvoiceTemplate;
use App\Entity\Timesheet;
use App\Form\InvoiceTemplateForm; use App\Form\InvoiceTemplateForm;
use App\Form\Toolbar\InvoiceToolbarForm; use App\Form\Toolbar\InvoiceToolbarForm;
use App\Invoice\InvoiceItemInterface;
use App\Invoice\InvoiceModel; use App\Invoice\InvoiceModel;
use App\Invoice\ServiceInvoice; use App\Invoice\ServiceInvoice;
use App\Repository\InvoiceTemplateRepository; use App\Repository\InvoiceTemplateRepository;
use App\Repository\Query\BaseQuery; use App\Repository\Query\BaseQuery;
use App\Repository\Query\InvoiceQuery; use App\Repository\Query\InvoiceQuery;
use App\Repository\TimesheetRepository;
use App\Timesheet\UserDateTimeFactory; use App\Timesheet\UserDateTimeFactory;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormInterface;
@@ -43,10 +42,6 @@ class InvoiceController extends AbstractController
* @var InvoiceTemplateRepository * @var InvoiceTemplateRepository
*/ */
protected $invoiceRepository; protected $invoiceRepository;
/**
* @var TimesheetRepository
*/
protected $timesheetRepository;
/** /**
* @var UserDateTimeFactory * @var UserDateTimeFactory
*/ */
@@ -62,12 +57,8 @@ class InvoiceController extends AbstractController
/** /**
* @Route(path="/", name="invoice", methods={"GET", "POST"}) * @Route(path="/", name="invoice", methods={"GET", "POST"})
* @Security("is_granted('view_invoice')") * @Security("is_granted('view_invoice')")
*
* @param Request $request
* @return \Symfony\Component\HttpFoundation\Response
* @throws \Exception
*/ */
public function indexAction(Request $request, TimesheetRepository $repository) public function indexAction(Request $request): Response
{ {
if (!$this->invoiceRepository->hasTemplate()) { if (!$this->invoiceRepository->hasTemplate()) {
if ($this->isGranted('manage_invoice_template')) { if ($this->isGranted('manage_invoice_template')) {
@@ -90,7 +81,7 @@ class InvoiceController extends AbstractController
/** @var SubmitButton $createButton */ /** @var SubmitButton $createButton */
$createButton = $form->get('create'); $createButton = $form->get('create');
if ($createButton->isClicked()) { if ($createButton->isClicked()) {
return $this->renderInvoice($query, $repository); return $this->renderInvoice($query);
} }
/** @var SubmitButton $previewButton */ /** @var SubmitButton $previewButton */
@@ -98,12 +89,15 @@ class InvoiceController extends AbstractController
if ($previewButton->isClicked()) { if ($previewButton->isClicked()) {
$showPreview = true; $showPreview = true;
$query->setPageSize($maxItemsPreview); $query->setPageSize($maxItemsPreview);
$entries = $this->getEntries($query, $repository); $entries = $this->getPreviewEntries($query);
} }
} }
} }
$model = $this->prepareModel($query, $entries); $model = $this->prepareModel($query);
if (!empty($entries)) {
$model->addEntries($entries);
}
return $this->render('invoice/index.html.twig', [ return $this->render('invoice/index.html.twig', [
'model' => $model, 'model' => $model,
@@ -129,10 +123,13 @@ class InvoiceController extends AbstractController
return $query; return $query;
} }
protected function renderInvoice(InvoiceQuery $query, TimesheetRepository $repository) protected function renderInvoice(InvoiceQuery $query)
{ {
$entries = $this->getEntries($query, $repository); $entries = $this->getEntries($query);
$model = $this->prepareModel($query, $entries); $model = $this->prepareModel($query);
foreach ($entries as $repo => $items) {
$model->addEntries($items);
}
$document = $this->service->getDocumentByName($model->getTemplate()->getRenderer()); $document = $this->service->getDocumentByName($model->getTemplate()->getRenderer());
if (null === $document) { if (null === $document) {
@@ -143,7 +140,7 @@ class InvoiceController extends AbstractController
if ($renderer->supports($document)) { if ($renderer->supports($document)) {
$response = $renderer->render($document, $model); $response = $renderer->render($document, $model);
if ($query->isMarkAsExported()) { if ($query->isMarkAsExported()) {
$repository->setExported($entries); $this->markEntriesAsExported($entries);
} }
return $response; return $response;
@@ -158,11 +155,26 @@ class InvoiceController extends AbstractController
} }
/** /**
* @param InvoiceQuery $query * @param InvoiceItemInterface[] $entries
* @param TimesheetRepository $repository
* @return Timesheet[]
*/ */
protected function getEntries(InvoiceQuery $query, TimesheetRepository $repository): iterable private function markEntriesAsExported(iterable $entries)
{
$repositories = $this->service->getInvoiceItemRepositories();
foreach ($entries as $repo => $items) {
foreach ($repositories as $repository) {
if (get_class($repository) === $repo) {
$repository->setExported($items);
}
}
}
}
/**
* @param InvoiceQuery $query
* @return InvoiceItemInterface[]
*/
protected function getEntries(InvoiceQuery $query): array
{ {
// customer needs to be defined, as we need the currency for the invoice // customer needs to be defined, as we need the currency for the invoice
if (null === $query->getCustomer()) { if (null === $query->getCustomer()) {
@@ -178,21 +190,38 @@ class InvoiceController extends AbstractController
$query->getBegin()->setTime(0, 0, 0); $query->getBegin()->setTime(0, 0, 0);
$query->getEnd()->setTime(23, 59, 59); $query->getEnd()->setTime(23, 59, 59);
return $repository->getTimesheetsForQuery($query); $repositories = $this->service->getInvoiceItemRepositories();
$items = [];
foreach ($repositories as $repository) {
$items[get_class($repository)] = $repository->getInvoiceItemsForQuery($query);
}
return $items;
}
protected function getPreviewEntries(InvoiceQuery $query): array
{
$entries = [];
$temp = $this->getEntries($query);
foreach ($temp as $repo => $items) {
$entries = array_merge($entries, $items);
}
return array_slice($entries, 0, $query->getPageSize());
} }
/** /**
* @param InvoiceQuery $query * @param InvoiceQuery $query
* @param Timesheet[] $entries
* @return InvoiceModel * @return InvoiceModel
* @throws \Exception * @throws \Exception
*/ */
protected function prepareModel(InvoiceQuery $query, array $entries): InvoiceModel protected function prepareModel(InvoiceQuery $query): InvoiceModel
{ {
$model = new InvoiceModel(); $model = new InvoiceModel();
$model $model
->setQuery($query) ->setQuery($query)
->setEntries($entries)
->setCustomer($query->getCustomer()) ->setCustomer($query->getCustomer())
; ;

View File

@@ -47,5 +47,10 @@ class InvoiceServiceCompilerPass implements CompilerPassInterface
foreach ($taggedCalculator as $id => $tags) { foreach ($taggedCalculator as $id => $tags) {
$definition->addMethodCall('addCalculator', [new Reference($id)]); $definition->addMethodCall('addCalculator', [new Reference($id)]);
} }
$taggedRepository = $container->findTaggedServiceIds(Kernel::TAG_INVOICE_REPOSITORY);
foreach ($taggedRepository as $id => $tags) {
$definition->addMethodCall('addInvoiceItemRepository', [new Reference($id)]);
}
} }
} }

View File

@@ -9,6 +9,7 @@
namespace App\Entity; namespace App\Entity;
use App\Invoice\InvoiceItemInterface;
use DateTime; use DateTime;
use DateTimeZone; use DateTimeZone;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
@@ -38,7 +39,7 @@ use Symfony\Component\Validator\Constraints as Assert;
* columns={"start_time","end_time"} => IDX_4F60C6B1502DF58741561401 => ??? * columns={"start_time","end_time"} => IDX_4F60C6B1502DF58741561401 => ???
* columns={"start_time","end_time","user"} => IDX_4F60C6B1502DF587415614018D93D649 => ??? * columns={"start_time","end_time","user"} => IDX_4F60C6B1502DF587415614018D93D649 => ???
*/ */
class Timesheet implements EntityWithMetaFields class Timesheet implements EntityWithMetaFields, InvoiceItemInterface
{ {
/** /**
* @var int * @var int
@@ -260,7 +261,7 @@ class Timesheet implements EntityWithMetaFields
* *
* @return int * @return int
*/ */
public function getDuration() public function getDuration(): int
{ {
return $this->duration; return $this->duration;
} }
@@ -340,10 +341,7 @@ class Timesheet implements EntityWithMetaFields
return $this; return $this;
} }
/** public function getRate(): float
* @return float
*/
public function getRate()
{ {
return $this->rate; return $this->rate;
} }

View File

@@ -11,10 +11,21 @@ namespace App\Invoice\Calculator;
use App\Entity\Timesheet; use App\Entity\Timesheet;
use App\Invoice\InvoiceItem; use App\Invoice\InvoiceItem;
use App\Invoice\InvoiceItemInterface;
abstract class AbstractMergedCalculator extends AbstractCalculator abstract class AbstractMergedCalculator extends AbstractCalculator
{ {
/**
* @deprecated since 1.3 - will be removed with 2.0
*/
protected function mergeTimesheets(InvoiceItem $invoiceItem, Timesheet $entry) 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);
}
protected function mergeInvoiceItems(InvoiceItem $invoiceItem, InvoiceItemInterface $entry)
{ {
$invoiceItem->setAmount($invoiceItem->getAmount() + 1); $invoiceItem->setAmount($invoiceItem->getAmount() + 1);
$invoiceItem->setUser($entry->getUser()); $invoiceItem->setUser($entry->getUser());

View File

@@ -9,16 +9,16 @@
namespace App\Invoice\Calculator; namespace App\Invoice\Calculator;
use App\Entity\Timesheet;
use App\Invoice\CalculatorInterface; use App\Invoice\CalculatorInterface;
use App\Invoice\InvoiceItem; use App\Invoice\InvoiceItem;
use App\Invoice\InvoiceItemInterface;
/** /**
* An abstract calculator that sums up the timesheet records. * An abstract calculator that sums up the invoice item records.
*/ */
abstract class AbstractSumInvoiceCalculator extends AbstractMergedCalculator implements CalculatorInterface abstract class AbstractSumInvoiceCalculator extends AbstractMergedCalculator implements CalculatorInterface
{ {
abstract protected function calculateSumIdentifier(Timesheet $timesheet): string; abstract protected function calculateSumIdentifier(InvoiceItemInterface $invoiceItem): string;
/** /**
* @return InvoiceItem[] * @return InvoiceItem[]
@@ -45,15 +45,15 @@ abstract class AbstractSumInvoiceCalculator extends AbstractMergedCalculator imp
if (!isset($invoiceItems[$id])) { if (!isset($invoiceItems[$id])) {
$invoiceItems[$id] = new InvoiceItem(); $invoiceItems[$id] = new InvoiceItem();
} }
$timesheet = $invoiceItems[$id]; $invoiceItem = $invoiceItems[$id];
$this->mergeTimesheets($timesheet, $entry); $this->mergeInvoiceItems($invoiceItem, $entry);
$this->mergeSumTimesheet($timesheet, $entry); $this->mergeSumTimesheet($invoiceItem, $entry);
} }
return array_values($invoiceItems); return array_values($invoiceItems);
} }
protected function mergeSumTimesheet(InvoiceItem $invoiceItem, Timesheet $entry) protected function mergeSumTimesheet(InvoiceItem $invoiceItem, InvoiceItemInterface $entry)
{ {
// allows to set values per calculator after merging the timesheet // allows to set values per calculator after merging the timesheet
} }

View File

@@ -9,25 +9,25 @@
namespace App\Invoice\Calculator; namespace App\Invoice\Calculator;
use App\Entity\Timesheet;
use App\Invoice\CalculatorInterface; use App\Invoice\CalculatorInterface;
use App\Invoice\InvoiceItem; use App\Invoice\InvoiceItem;
use App\Invoice\InvoiceItemInterface;
/** /**
* A calculator that sums up the timesheet records by activity. * A calculator that sums up the invoice item records by activity.
*/ */
class ActivityInvoiceCalculator extends AbstractSumInvoiceCalculator implements CalculatorInterface class ActivityInvoiceCalculator extends AbstractSumInvoiceCalculator implements CalculatorInterface
{ {
protected function calculateSumIdentifier(Timesheet $timesheet): string protected function calculateSumIdentifier(InvoiceItemInterface $invoiceItem): string
{ {
if (null === $timesheet->getActivity()->getId()) { if (null === $invoiceItem->getActivity()->getId()) {
throw new \Exception('Cannot handle un-persisted activities'); throw new \Exception('Cannot handle un-persisted activities');
} }
return (string) $timesheet->getActivity()->getId(); return (string) $invoiceItem->getActivity()->getId();
} }
protected function mergeSumTimesheet(InvoiceItem $invoiceItem, Timesheet $entry) protected function mergeSumTimesheet(InvoiceItem $invoiceItem, InvoiceItemInterface $entry)
{ {
$invoiceItem->setActivity($entry->getActivity()); $invoiceItem->setActivity($entry->getActivity());
$invoiceItem->setDescription($entry->getActivity()->getName()); $invoiceItem->setDescription($entry->getActivity()->getName());

View File

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

View File

@@ -29,7 +29,7 @@ class DefaultCalculator extends AbstractMergedCalculator implements CalculatorIn
foreach ($this->model->getEntries() as $entry) { foreach ($this->model->getEntries() as $entry) {
$item = new InvoiceItem(); $item = new InvoiceItem();
$this->mergeTimesheets($item, $entry); $this->mergeInvoiceItems($item, $entry);
foreach ($entry->getVisibleMetaFields() as $field) { foreach ($entry->getVisibleMetaFields() as $field) {
$item->addAdditionalField($field->getName(), $field->getValue()); $item->addAdditionalField($field->getName(), $field->getValue());
} }

View File

@@ -9,25 +9,25 @@
namespace App\Invoice\Calculator; namespace App\Invoice\Calculator;
use App\Entity\Timesheet;
use App\Invoice\CalculatorInterface; use App\Invoice\CalculatorInterface;
use App\Invoice\InvoiceItem; use App\Invoice\InvoiceItem;
use App\Invoice\InvoiceItemInterface;
/** /**
* A calculator that sums up the timesheet records by project. * A calculator that sums up the invoice item records by project.
*/ */
class ProjectInvoiceCalculator extends AbstractSumInvoiceCalculator implements CalculatorInterface class ProjectInvoiceCalculator extends AbstractSumInvoiceCalculator implements CalculatorInterface
{ {
protected function calculateSumIdentifier(Timesheet $timesheet): string protected function calculateSumIdentifier(InvoiceItemInterface $invoiceItem): string
{ {
if (null === $timesheet->getProject()->getId()) { if (null === $invoiceItem->getProject()->getId()) {
throw new \Exception('Cannot handle un-persisted projects'); throw new \Exception('Cannot handle un-persisted projects');
} }
return (string) $timesheet->getProject()->getId(); return (string) $invoiceItem->getProject()->getId();
} }
protected function mergeSumTimesheet(InvoiceItem $invoiceItem, Timesheet $entry) protected function mergeSumTimesheet(InvoiceItem $invoiceItem, InvoiceItemInterface $entry)
{ {
$invoiceItem->setProject($entry->getProject()); $invoiceItem->setProject($entry->getProject());
$invoiceItem->setDescription($entry->getProject()->getName()); $invoiceItem->setDescription($entry->getProject()->getName());

View File

@@ -9,12 +9,11 @@
namespace App\Invoice\Calculator; namespace App\Invoice\Calculator;
use App\Entity\Timesheet;
use App\Invoice\CalculatorInterface; use App\Invoice\CalculatorInterface;
use App\Invoice\InvoiceItem; use App\Invoice\InvoiceItem;
/** /**
* A calculator that sums up all timesheet records from the model and returns only one * A calculator that sums up all invoice item records from the model and returns only one
* entry for a compact invoice version. * entry for a compact invoice version.
*/ */
class ShortInvoiceCalculator extends AbstractMergedCalculator implements CalculatorInterface class ShortInvoiceCalculator extends AbstractMergedCalculator implements CalculatorInterface
@@ -40,7 +39,7 @@ class ShortInvoiceCalculator extends AbstractMergedCalculator implements Calcula
if (!in_array($key, $keys)) { if (!in_array($key, $keys)) {
$keys[] = $key; $keys[] = $key;
} }
$this->mergeTimesheets($invoiceItem, $entry); $this->mergeInvoiceItems($invoiceItem, $entry);
} }
if (count($keys) > 1) { if (count($keys) > 1) {

View File

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

View File

@@ -0,0 +1,43 @@
<?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;
interface InvoiceItemInterface
{
public function getActivity(): ?Activity;
public function getProject(): ?Project;
public function getFixedRate(): ?float;
public function getHourlyRate(): ?float;
public function getRate(): 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;
}

View File

@@ -0,0 +1,26 @@
<?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\Repository\Query\InvoiceQuery;
interface InvoiceItemRepositoryInterface
{
/**
* @param InvoiceItemInterface[] $invoiceItems
*/
public function setExported(array $invoiceItems);
/**
* @param InvoiceQuery $query
* @return InvoiceItemInterface[]
*/
public function getInvoiceItemsForQuery(InvoiceQuery $query): iterable;
}

View File

@@ -11,7 +11,6 @@ namespace App\Invoice;
use App\Entity\Customer; use App\Entity\Customer;
use App\Entity\InvoiceTemplate; use App\Entity\InvoiceTemplate;
use App\Entity\Timesheet;
use App\Repository\Query\InvoiceQuery; use App\Repository\Query\InvoiceQuery;
/** /**
@@ -31,7 +30,7 @@ class InvoiceModel
protected $query; protected $query;
/** /**
* @var Timesheet[] * @var InvoiceItemInterface[]
*/ */
protected $entries = []; protected $entries = [];
@@ -82,7 +81,7 @@ class InvoiceModel
/** /**
* Do not use this method for rendering the invoice, use InvoiceModel::getCalculator()->getEntries() instead. * Do not use this method for rendering the invoice, use InvoiceModel::getCalculator()->getEntries() instead.
* *
* @return Timesheet[] * @return InvoiceItemInterface[]
*/ */
public function getEntries(): array public function getEntries(): array
{ {
@@ -90,16 +89,30 @@ class InvoiceModel
} }
/** /**
* @param Timesheet[] $entries * @deprecated since 1.3 - will be removed with 2.0
* @param InvoiceItemInterface[] $entries
* @return InvoiceModel * @return InvoiceModel
*/ */
public function setEntries(array $entries): 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; $this->entries = $entries;
return $this; return $this;
} }
/**
* @param InvoiceItemInterface[] $entries
* @return InvoiceModel
*/
public function addEntries(array $entries): InvoiceModel
{
$this->entries = array_merge($this->entries, $entries);
return $this;
}
public function getTemplate(): ?InvoiceTemplate public function getTemplate(): ?InvoiceTemplate
{ {
return $this->template; return $this->template;

View File

@@ -41,9 +41,9 @@ abstract class AbstractSpreadsheetRenderer extends AbstractRenderer
$worksheet = $spreadsheet->getActiveSheet(); $worksheet = $spreadsheet->getActiveSheet();
$entries = $model->getCalculator()->getEntries(); $entries = $model->getCalculator()->getEntries();
$replacer = $this->modelToReplacer($model); $replacer = $this->modelToReplacer($model);
$timesheetAmount = count($entries); $invoiceItemCount = count($entries);
if ($timesheetAmount > 1) { if ($invoiceItemCount > 1) {
$this->addTemplateRows($worksheet, $timesheetAmount); $this->addTemplateRows($worksheet, $invoiceItemCount);
} }
$worksheet->setTitle($model->getTemplate()->getTitle()); $worksheet->setTitle($model->getTemplate()->getTitle());
@@ -51,13 +51,13 @@ abstract class AbstractSpreadsheetRenderer extends AbstractRenderer
$entryRow = 0; $entryRow = 0;
foreach ($worksheet->getRowIterator() as $row) { foreach ($worksheet->getRowIterator() as $row) {
$timesheet = $entries[$entryRow]; $invoiceItem = $entries[$entryRow];
$sheetValues = false; $sheetValues = false;
foreach ($row->getCellIterator() as $cell) { foreach ($row->getCellIterator() as $cell) {
$value = $cell->getValue(); $value = $cell->getValue();
if (stripos($value, '${entry.') !== false) { if (stripos($value, '${entry.') !== false) {
if ($sheetValues === false) { if ($sheetValues === false) {
$sheetValues = $this->timesheetToArray($timesheet); $sheetValues = $this->invoiceItemToArray($invoiceItem);
} }
$searcher = str_replace('${', '', $value); $searcher = str_replace('${', '', $value);
$searcher = str_replace('}', '', $searcher); $searcher = str_replace('}', '', $searcher);
@@ -73,7 +73,7 @@ abstract class AbstractSpreadsheetRenderer extends AbstractRenderer
} }
} }
if ($sheetValues !== false && $entryRow < $timesheetAmount - 1) { if ($sheetValues !== false && $entryRow < $invoiceItemCount - 1) {
$entryRow++; $entryRow++;
} }
} }
@@ -85,9 +85,10 @@ abstract class AbstractSpreadsheetRenderer extends AbstractRenderer
/** /**
* @param Worksheet $worksheet * @param Worksheet $worksheet
* @param int $timesheets * @param int $invoiceItemCount
* @throws \PhpOffice\PhpSpreadsheet\Exception
*/ */
protected function addTemplateRows(Worksheet $worksheet, int $timesheets) protected function addTemplateRows(Worksheet $worksheet, int $invoiceItemCount)
{ {
$startRow = null; $startRow = null;
$rowCounter = 0; $rowCounter = 0;
@@ -98,7 +99,7 @@ abstract class AbstractSpreadsheetRenderer extends AbstractRenderer
$value = $cell->getValue(); $value = $cell->getValue();
if (stripos($value, '${entry.') !== false) { if (stripos($value, '${entry.') !== false) {
$startRow = $row->getRowIndex(); $startRow = $row->getRowIndex();
$worksheet->insertNewRowBefore($row->getRowIndex(), $timesheets - 1); $worksheet->insertNewRowBefore($row->getRowIndex(), $invoiceItemCount - 1);
break 2; break 2;
} }
@@ -117,7 +118,7 @@ abstract class AbstractSpreadsheetRenderer extends AbstractRenderer
} }
// fill up all new rows with template values // fill up all new rows with template values
$templateRow = $timesheets + $startRow; $templateRow = $invoiceItemCount + $startRow;
$iterator = $worksheet->getRowIterator($templateRow - 1, $templateRow); $iterator = $worksheet->getRowIterator($templateRow - 1, $templateRow);
$templateColumns = []; $templateColumns = [];
foreach ($iterator->current()->getCellIterator() as $cell) { foreach ($iterator->current()->getCellIterator() as $cell) {

View File

@@ -50,7 +50,7 @@ class DocxRenderer extends AbstractRenderer implements RendererInterface
$i = 1; $i = 1;
foreach ($model->getCalculator()->getEntries() as $entry) { foreach ($model->getCalculator()->getEntries() as $entry) {
$values = $this->timesheetToArray($entry); $values = $this->invoiceItemToArray($entry);
foreach ($values as $search => $replace) { foreach ($values as $search => $replace) {
$replace = $xmlEscaper->escape($replace); $replace = $xmlEscaper->escape($replace);
$replace = str_replace(PHP_EOL, '</w:t><w:br /><w:t xml:space="preserve">', $replace); $replace = str_replace(PHP_EOL, '</w:t><w:br /><w:t xml:space="preserve">', $replace);

View File

@@ -176,7 +176,17 @@ trait RendererTrait
return $values; return $values;
} }
/**
* @deprecated since 1.3 - will be removed with 2.0
*/
protected function timesheetToArray(InvoiceItem $invoiceItem): array protected function timesheetToArray(InvoiceItem $invoiceItem): array
{
@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(); $rate = $invoiceItem->getRate();
$hourlyRate = $invoiceItem->getHourlyRate(); $hourlyRate = $invoiceItem->getHourlyRate();

View File

@@ -13,43 +13,37 @@ use App\Entity\InvoiceDocument;
use App\Repository\InvoiceDocumentRepository; use App\Repository\InvoiceDocumentRepository;
/** /**
* A service to manage invoice dependencies. * Service to manage invoice dependencies.
*/ */
class ServiceInvoice final class ServiceInvoice
{ {
/** /**
* @var CalculatorInterface[] * @var CalculatorInterface[]
*/ */
protected $calculator = []; private $calculator = [];
/** /**
* @var RendererInterface[] * @var RendererInterface[]
*/ */
protected $renderer = []; private $renderer = [];
/** /**
* @var NumberGeneratorInterface[] * @var NumberGeneratorInterface[]
*/ */
protected $numberGenerator = []; private $numberGenerator = [];
/**
* @var array InvoiceItemRepositoryInterface[]
*/
private $invoiceItemRepositories = [];
/** /**
* @var InvoiceDocumentRepository * @var InvoiceDocumentRepository
*/ */
protected $documents; private $documents;
/**
* @param InvoiceDocumentRepository $repository
*/
public function __construct(InvoiceDocumentRepository $repository) public function __construct(InvoiceDocumentRepository $repository)
{ {
$this->documents = $repository; $this->documents = $repository;
} }
/** public function addNumberGenerator(NumberGeneratorInterface $generator): ServiceInvoice
* @param NumberGeneratorInterface $generator
* @return $this
*/
public function addNumberGenerator(NumberGeneratorInterface $generator)
{ {
$this->numberGenerator[] = $generator; $this->numberGenerator[] = $generator;
@@ -59,16 +53,12 @@ class ServiceInvoice
/** /**
* @return NumberGeneratorInterface[] * @return NumberGeneratorInterface[]
*/ */
public function getNumberGenerator() public function getNumberGenerator(): array
{ {
return $this->numberGenerator; return $this->numberGenerator;
} }
/** public function getNumberGeneratorByName(string $name): ?NumberGeneratorInterface
* @param string $name
* @return NumberGeneratorInterface|null
*/
public function getNumberGeneratorByName(string $name)
{ {
foreach ($this->getNumberGenerator() as $generator) { foreach ($this->getNumberGenerator() as $generator) {
if ($generator->getId() === $name) { if ($generator->getId() === $name) {
@@ -79,11 +69,7 @@ class ServiceInvoice
return null; return null;
} }
/** public function addCalculator(CalculatorInterface $calculator): ServiceInvoice
* @param CalculatorInterface $calculator
* @return $this
*/
public function addCalculator(CalculatorInterface $calculator)
{ {
$this->calculator[] = $calculator; $this->calculator[] = $calculator;
@@ -93,16 +79,12 @@ class ServiceInvoice
/** /**
* @return CalculatorInterface[] * @return CalculatorInterface[]
*/ */
public function getCalculator() public function getCalculator(): array
{ {
return $this->calculator; return $this->calculator;
} }
/** public function getCalculatorByName(string $name): ?CalculatorInterface
* @param string $name
* @return CalculatorInterface|null
*/
public function getCalculatorByName(string $name)
{ {
foreach ($this->getCalculator() as $calculator) { foreach ($this->getCalculator() as $calculator) {
if ($calculator->getId() === $name) { if ($calculator->getId() === $name) {
@@ -113,11 +95,7 @@ class ServiceInvoice
return null; return null;
} }
/** public function getDocumentByName(string $name): ?InvoiceDocument
* @param string $name
* @return InvoiceDocument|null
*/
public function getDocumentByName(string $name)
{ {
return $this->documents->findByName($name); return $this->documents->findByName($name);
} }
@@ -127,16 +105,12 @@ class ServiceInvoice
* *
* @return InvoiceDocument[] * @return InvoiceDocument[]
*/ */
public function getDocuments() public function getDocuments(): array
{ {
return $this->documents->findAll(); return $this->documents->findAll();
} }
/** public function addRenderer(RendererInterface $renderer): ServiceInvoice
* @param RendererInterface $renderer
* @return $this
*/
public function addRenderer(RendererInterface $renderer)
{ {
$this->renderer[] = $renderer; $this->renderer[] = $renderer;
@@ -148,8 +122,23 @@ class ServiceInvoice
* *
* @return RendererInterface[] * @return RendererInterface[]
*/ */
public function getRenderer() public function getRenderer(): array
{ {
return $this->renderer; return $this->renderer;
} }
/**
* @return InvoiceItemRepositoryInterface[]
*/
public function getInvoiceItemRepositories(): array
{
return $this->invoiceItemRepositories;
}
public function addInvoiceItemRepository(InvoiceItemRepositoryInterface $invoiceItemRepository): ServiceInvoice
{
$this->invoiceItemRepositories[] = $invoiceItemRepository;
return $this;
}
} }

View File

@@ -16,6 +16,7 @@ use App\DependencyInjection\Compiler\TwigContextCompilerPass;
use App\DependencyInjection\Compiler\WidgetCompilerPass; use App\DependencyInjection\Compiler\WidgetCompilerPass;
use App\Export\RendererInterface as ExportRendererInterface; use App\Export\RendererInterface as ExportRendererInterface;
use App\Invoice\CalculatorInterface as InvoiceCalculator; use App\Invoice\CalculatorInterface as InvoiceCalculator;
use App\Invoice\InvoiceItemRepositoryInterface;
use App\Invoice\NumberGeneratorInterface; use App\Invoice\NumberGeneratorInterface;
use App\Invoice\RendererInterface as InvoiceRendererInterface; use App\Invoice\RendererInterface as InvoiceRendererInterface;
use App\Ldap\FormLoginLdapFactory; use App\Ldap\FormLoginLdapFactory;
@@ -46,6 +47,7 @@ class Kernel extends BaseKernel
public const TAG_INVOICE_RENDERER = 'invoice.renderer'; public const TAG_INVOICE_RENDERER = 'invoice.renderer';
public const TAG_INVOICE_NUMBER_GENERATOR = 'invoice.number_generator'; public const TAG_INVOICE_NUMBER_GENERATOR = 'invoice.number_generator';
public const TAG_INVOICE_CALCULATOR = 'invoice.calculator'; public const TAG_INVOICE_CALCULATOR = 'invoice.calculator';
public const TAG_INVOICE_REPOSITORY = 'invoice.repository';
public const TAG_TIMESHEET_CALCULATOR = 'timesheet.calculator'; public const TAG_TIMESHEET_CALCULATOR = 'timesheet.calculator';
public function getCacheDir() public function getCacheDir()
@@ -65,6 +67,7 @@ class Kernel extends BaseKernel
$container->registerForAutoconfiguration(InvoiceRendererInterface::class)->addTag(self::TAG_INVOICE_RENDERER); $container->registerForAutoconfiguration(InvoiceRendererInterface::class)->addTag(self::TAG_INVOICE_RENDERER);
$container->registerForAutoconfiguration(NumberGeneratorInterface::class)->addTag(self::TAG_INVOICE_NUMBER_GENERATOR); $container->registerForAutoconfiguration(NumberGeneratorInterface::class)->addTag(self::TAG_INVOICE_NUMBER_GENERATOR);
$container->registerForAutoconfiguration(InvoiceCalculator::class)->addTag(self::TAG_INVOICE_CALCULATOR); $container->registerForAutoconfiguration(InvoiceCalculator::class)->addTag(self::TAG_INVOICE_CALCULATOR);
$container->registerForAutoconfiguration(InvoiceItemRepositoryInterface::class)->addTag(self::TAG_INVOICE_REPOSITORY);
$container->registerForAutoconfiguration(PluginInterface::class)->addTag(self::TAG_PLUGIN); $container->registerForAutoconfiguration(PluginInterface::class)->addTag(self::TAG_PLUGIN);
$container->registerForAutoconfiguration(WidgetRendererInterface::class)->addTag(self::TAG_WIDGET_RENDERER); $container->registerForAutoconfiguration(WidgetRendererInterface::class)->addTag(self::TAG_WIDGET_RENDERER);
$container->registerForAutoconfiguration(WidgetInterface::class)->addTag(self::TAG_WIDGET); $container->registerForAutoconfiguration(WidgetInterface::class)->addTag(self::TAG_WIDGET);

View File

@@ -41,9 +41,9 @@ class ActivityQuery extends ProjectQuery
/** /**
* @param bool $globalsOnly * @param bool $globalsOnly
* @return ActivityQuery * @return self
*/ */
public function setGlobalsOnly($globalsOnly): ActivityQuery public function setGlobalsOnly($globalsOnly): self
{ {
$this->globalsOnly = (bool) $globalsOnly; $this->globalsOnly = (bool) $globalsOnly;
@@ -60,9 +60,9 @@ class ActivityQuery extends ProjectQuery
/** /**
* @param Project|int|null $project * @param Project|int|null $project
* @return ActivityQuery * @return self
*/ */
public function setProject($project = null): ActivityQuery public function setProject($project = null): self
{ {
$this->project = $project; $this->project = $project;

View File

@@ -83,7 +83,7 @@ class BaseQuery
/** /**
* @param User $user * @param User $user
* @return $this * @return self
*/ */
public function setCurrentUser(User $user) public function setCurrentUser(User $user)
{ {
@@ -102,7 +102,7 @@ class BaseQuery
/** /**
* @param int $page * @param int $page
* @return $this * @return self
*/ */
public function setPage($page) public function setPage($page)
{ {
@@ -118,7 +118,7 @@ class BaseQuery
/** /**
* @param int $pageSize * @param int $pageSize
* @return $this * @return self
*/ */
public function setPageSize($pageSize) public function setPageSize($pageSize)
{ {
@@ -138,7 +138,7 @@ class BaseQuery
* You need to validate carefully if this value is used from a user-input. * You need to validate carefully if this value is used from a user-input.
* *
* @param string $orderBy * @param string $orderBy
* @return $this * @return self
*/ */
public function setOrderBy($orderBy) public function setOrderBy($orderBy)
{ {
@@ -154,7 +154,7 @@ class BaseQuery
/** /**
* @param string $order * @param string $order
* @return $this * @return self
*/ */
public function setOrder($order) public function setOrder($order)
{ {
@@ -205,7 +205,7 @@ class BaseQuery
/** /**
* @param SearchTerm|null $searchTerm * @param SearchTerm|null $searchTerm
* @return BaseQuery * @return self
*/ */
public function setSearchTerm(?SearchTerm $searchTerm) public function setSearchTerm(?SearchTerm $searchTerm)
{ {

View File

@@ -0,0 +1,51 @@
<?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\Repository;
use App\Entity\Timesheet;
use App\Invoice\InvoiceItemInterface;
use App\Invoice\InvoiceItemRepositoryInterface;
use App\Repository\Query\InvoiceQuery;
final class TimesheetInvoiceItemRepository implements InvoiceItemRepositoryInterface
{
/**
* @var TimesheetRepository
*/
private $repository;
public function __construct(TimesheetRepository $repository)
{
$this->repository = $repository;
}
/**
* @param InvoiceQuery $query
* @return InvoiceItemInterface[]
*/
public function getInvoiceItemsForQuery(InvoiceQuery $query): iterable
{
return $this->repository->getTimesheetsForQuery($query);
}
/**
* @param InvoiceItemInterface[] $invoiceItems
*/
public function setExported(array $invoiceItems)
{
foreach ($invoiceItems as $item) {
if (!$item instanceof Timesheet) {
throw new \InvalidArgumentException('TimesheetInvoiceItemRepository only supports Timesheet entities');
}
}
$this->repository->setExported($invoiceItems);
}
}

View File

@@ -609,9 +609,9 @@ class TimesheetRepository extends EntityRepository
} }
if ($query->getExported() === TimesheetQuery::STATE_EXPORTED) { if ($query->getExported() === TimesheetQuery::STATE_EXPORTED) {
$qb->andWhere('t.exported = :exported')->setParameter('exported', true); $qb->andWhere('t.exported = :exported')->setParameter('exported', true, \PDO::PARAM_BOOL);
} elseif ($query->getExported() === TimesheetQuery::STATE_NOT_EXPORTED) { } elseif ($query->getExported() === TimesheetQuery::STATE_NOT_EXPORTED) {
$qb->andWhere('t.exported = :exported')->setParameter('exported', false); $qb->andWhere('t.exported = :exported')->setParameter('exported', false, \PDO::PARAM_BOOL);
} }
if (null !== $query->getActivity()) { if (null !== $query->getActivity()) {

View File

@@ -19,7 +19,7 @@
</trans-unit> </trans-unit>
<trans-unit id="both"> <trans-unit id="both">
<source>both</source> <source>both</source>
<target></target> <target>Beides</target>
</trans-unit> </trans-unit>
<trans-unit id="This is a mandatory field"> <trans-unit id="This is a mandatory field">
<source>This is a mandatory field</source> <source>This is a mandatory field</source>

View File

@@ -19,7 +19,7 @@
</trans-unit> </trans-unit>
<trans-unit id="both"> <trans-unit id="both">
<source>both</source> <source>both</source>
<target></target> <target>Both</target>
</trans-unit> </trans-unit>
<trans-unit id="This is a mandatory field"> <trans-unit id="This is a mandatory field">
<source>This is a mandatory field</source> <source>This is a mandatory field</source>