diff --git a/.env b/.env index 2a8a2a38..88e541be 100644 --- a/.env +++ b/.env @@ -92,3 +92,7 @@ INVOICE_ENTRY_ACCOUNTS='{ "label": "Define INVOICE_ENTRY_ACCOUNTS in .env.local" } }' + +# If true, project billing will generate one invoice per issue per client. +# Otherwise, all issues will be added to a single invoice per client. +INVOICE_ONE_INVOICE_PER_ISSUE=false diff --git a/CHANGELOG.md b/CHANGELOG.md index 8520b553..90e62f96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] * [PR-130](https://github.com/itk-dev/economics/pull/130) - 1635: Reduced product invoice entries to a single entry + Reduced product invoice entries to a single entry + Added option to generate one invoice per issue * NOTE: APP_DEFAULT_PLANNING_DATA_PROVIDER has been changed to APP_DEFAULT_DATA_PROVIDER. This has to be changed when releasing. * [PR-117](https://github.com/itk-dev/economics/pull/117) 1211: Added hour report diff --git a/config/packages/twig.yaml b/config/packages/twig.yaml index 3c9c24eb..46d61c6b 100644 --- a/config/packages/twig.yaml +++ b/config/packages/twig.yaml @@ -4,7 +4,7 @@ twig: format: 'd.m.Y' globals: view_controller: '@App\Controller\ViewController' - invoice_entry_helper: '@App\Service\InvoiceEntryHelper' + invoice_helper: '@App\Service\InvoiceHelper' when@test: twig: diff --git a/config/services.yaml b/config/services.yaml index 1cbf1048..e1d726ca 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -54,7 +54,8 @@ services: issue_product_type_options: quantity_scale: '%env(int:PRODUCT_QUANTITY_SCALE)%' - App\Service\InvoiceEntryHelper: + App\Service\InvoiceHelper: arguments: $options: accounts: '%env(json:INVOICE_ENTRY_ACCOUNTS)%' + one_invoice_per_issue: '%env(bool:INVOICE_ONE_INVOICE_PER_ISSUE)%' diff --git a/src/Controller/InvoiceController.php b/src/Controller/InvoiceController.php index ad586aa7..e086d860 100644 --- a/src/Controller/InvoiceController.php +++ b/src/Controller/InvoiceController.php @@ -18,7 +18,7 @@ use App\Repository\InvoiceRepository; use App\Service\BillingService; use App\Service\ClientHelper; -use App\Service\InvoiceEntryHelper; +use App\Service\InvoiceHelper; use App\Service\ViewService; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -38,7 +38,7 @@ public function __construct( private readonly BillingService $billingService, private readonly TranslatorInterface $translator, private readonly ViewService $viewService, - private readonly InvoiceEntryHelper $invoiceEntryHelper, + private readonly InvoiceHelper $invoiceEntryHelper, ) { } diff --git a/src/Controller/InvoiceEntryController.php b/src/Controller/InvoiceEntryController.php index 5323bf96..693062c4 100644 --- a/src/Controller/InvoiceEntryController.php +++ b/src/Controller/InvoiceEntryController.php @@ -11,7 +11,7 @@ use App\Repository\InvoiceEntryRepository; use App\Service\BillingService; use App\Service\ClientHelper; -use App\Service\InvoiceEntryHelper; +use App\Service\InvoiceHelper; use App\Service\ViewService; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; @@ -28,7 +28,7 @@ public function __construct( private readonly TranslatorInterface $translator, private readonly ViewService $viewService, private readonly ClientHelper $clientHelper, - private readonly InvoiceEntryHelper $invoiceEntryHelper, + private readonly InvoiceHelper $invoiceHelper, ) { } @@ -93,7 +93,7 @@ public function edit(Request $request, Invoice $invoice, InvoiceEntry $invoiceEn $options['disabled'] = true; } - $accounts = $this->invoiceEntryHelper->getAccountOptions($invoiceEntry->getAccount()); + $accounts = $this->invoiceHelper->getAccountOptions($invoiceEntry->getAccount()); if (!empty($accounts)) { $options['invoice_entry_accounts'] = $accounts; } diff --git a/src/Service/InvoiceEntryHelper.php b/src/Service/InvoiceHelper.php similarity index 92% rename from src/Service/InvoiceEntryHelper.php rename to src/Service/InvoiceHelper.php index 34f57e66..37b6c0e8 100644 --- a/src/Service/InvoiceEntryHelper.php +++ b/src/Service/InvoiceHelper.php @@ -7,7 +7,7 @@ use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; -class InvoiceEntryHelper +class InvoiceHelper { private readonly array $options; @@ -17,6 +17,11 @@ public function __construct( $this->options = $this->resolveOptions($options); } + public function getOneInvoicePerIssue() + { + return $this->options['one_invoice_per_issue']; + } + /** * Get all configured accounts. * @@ -99,7 +104,7 @@ public function getAccountInvoiceEntryPrefix(string $account): ?string /** * Decide if an invoice entry is editable. */ - public function isEditable(InvoiceEntry $entry): bool + public function isEntryEditable(InvoiceEntry $entry): bool { return null === $entry->getInvoice()?->getProjectBilling() || count($this->getAccountOptions(null)) > 1; @@ -140,7 +145,8 @@ private function resolveOptions(array $options): array 'invoice_entry_prefix' => null, ]) ->setAllowedTypes('default', 'bool') - ->setAllowedTypes('product', 'bool'); + ->setAllowedTypes('product', 'bool') + ->setAllowedTypes('invoice_entry_prefix', ['null', 'string']); }) ->setAllowedValues('accounts', function (array $values) { if (empty($values)) { @@ -180,6 +186,9 @@ private function resolveOptions(array $options): array return $values; }) + ->setDefault('one_invoice_per_issue', false) + ->setAllowedTypes('one_invoice_per_issue', 'bool') + ->resolve($options); } } diff --git a/src/Service/ProjectBillingService.php b/src/Service/ProjectBillingService.php index 272b3abd..88a6d0fd 100644 --- a/src/Service/ProjectBillingService.php +++ b/src/Service/ProjectBillingService.php @@ -33,7 +33,7 @@ public function __construct( private readonly ClientHelper $clientHelper, private readonly EntityManagerInterface $entityManager, private readonly TranslatorInterface $translator, - private readonly InvoiceEntryHelper $invoiceEntryHelper, + private readonly InvoiceHelper $invoiceEntryHelper, ) { } @@ -167,17 +167,21 @@ public function createProjectBilling(int $projectBillingId): void continue; } - $clientId = $client->getId(); + $invoiceKey = $client->getId(); - if (null !== $clientId) { - if (!isset($invoices[$clientId])) { - $invoices[$clientId] = [ + if (null !== $invoiceKey) { + if ($this->invoiceEntryHelper->getOneInvoicePerIssue()) { + $invoiceKey .= '|||'.$issue->getId(); + } + + if (!isset($invoices[$invoiceKey])) { + $invoices[$invoiceKey] = [ 'client' => $client, 'issues' => [], ]; } - $invoices[$clientId]['issues'][] = $issue; + $invoices[$invoiceKey]['issues'][] = $issue; } } @@ -202,12 +206,24 @@ public function createProjectBilling(int $projectBillingId): void /** @var Client $client */ $client = $invoiceArray['client']; + $invoiceName = sprintf('%s: %s (%s - %s)', + $project->getName() ?? '', + $client->getName() ?? '', + $periodStart->format('d/m/Y'), + $periodEnd->format('d/m/Y') + ); + if ($this->invoiceEntryHelper->getOneInvoicePerIssue()) { + // We know that we have at least one issue. + $issue = reset($invoiceArray['issues']); + $invoiceName = $issue->getName() . ': ' . $invoiceName; + } + $invoice = new Invoice(); $invoice->setRecorded(false); $invoice->setProject($projectBilling->getProject()); $invoice->setProjectBilling($projectBilling); $invoice->setDescription($projectBilling->getDescription()); - $invoice->setName($project->getName().': '.$client->getName().' ('.$periodStart->format('d/m/Y').' - '.$periodEnd->format('d/m/Y').')'); + $invoice->setName($invoiceName); $invoice->setPeriodFrom($periodStart); $invoice->setPeriodTo($periodEnd); $invoice->setClient($client); diff --git a/templates/invoice_entry/edit.html.twig b/templates/invoice_entry/edit.html.twig index c732c023..1973f6ee 100644 --- a/templates/invoice_entry/edit.html.twig +++ b/templates/invoice_entry/edit.html.twig @@ -9,7 +9,7 @@
{{ form_widget(form) }} - {% if not invoice.recorded and invoice_entry_helper.isEditable(invoice_entry) %} + {% if not invoice.recorded and invoice_helper.isEntryEditable(invoice_entry) %}
diff --git a/templates/invoices/edit.html.twig b/templates/invoices/edit.html.twig index d2b4eec4..a0a21a00 100644 --- a/templates/invoices/edit.html.twig +++ b/templates/invoices/edit.html.twig @@ -113,7 +113,7 @@ } %} {% for index, invoice_entry in invoice.invoiceEntries %} - {{ invoice_entry_helper.getAccountLabel(invoice_entry.account) }} + {{ invoice_helper.getAccountLabel(invoice_entry.account) }} {{ invoice_entry.materialNumber.value ?? '' }} {{ invoice_entry.product }} {{ invoice_entry.amount }}