diff --git a/app/config/packages/backoffice_menu.yaml b/app/config/packages/backoffice_menu.yaml index e12d71d71..24ee95f83 100644 --- a/app/config/packages/backoffice_menu.yaml +++ b/app/config/packages/backoffice_menu.yaml @@ -236,6 +236,7 @@ parameters: niveau: 'ROLE_ADMIN' extra_routes: - admin_accounting_invoices_list + - admin_accounting_invoices_edit compta_journal: nom: 'Journal' url: '/admin/accounting/journal/list' diff --git a/app/config/routing/admin_accounting.yml b/app/config/routing/admin_accounting.yml index 181bae4fd..c4d615ea4 100644 --- a/app/config/routing/admin_accounting.yml +++ b/app/config/routing/admin_accounting.yml @@ -34,6 +34,10 @@ admin_accounting_invoices_list: path: /invoices/list defaults: {_controller: AppBundle\Controller\Admin\Accounting\Invoice\ListInvoiceAction} +admin_accounting_invoices_edit: + path: /invoices/edit + defaults: {_controller: AppBundle\Controller\Admin\Accounting\Invoice\EditInvoiceAction} + admin_accounting_invoices_download: path: /invoices/download defaults: {_controller: AppBundle\Controller\Admin\Accounting\Invoice\DownloadInvoiceAction} diff --git a/sources/AppBundle/Accounting/Form/InvoiceType.php b/sources/AppBundle/Accounting/Form/InvoiceType.php new file mode 100644 index 000000000..05a4dcca2 --- /dev/null +++ b/sources/AppBundle/Accounting/Form/InvoiceType.php @@ -0,0 +1,149 @@ +add('invoiceDate', DateType::class, [ + 'label' => 'Date facture', + 'widget' => 'single_text', + ])->add('company', TextType::class, [ + 'label' => 'Société', + 'constraints' => [ + new Assert\NotBlank(), + new Assert\Length(max: 50), + ], + ])->add('service', TextType::class, [ + 'label' => 'Service', + 'required' => false, + 'constraints' => [ + new Assert\Length(max: 50), + ], + ])->add('address', TextareaType::class, [ + 'label' => 'Adresse', + ])->add('zipcode', TextType::class, [ + 'label' => 'Code postal', + 'constraints' => [ + new Assert\NotBlank(), + new Assert\Length(max: 10), + ], + ])->add('city', TextType::class, [ + 'label' => 'Ville', + 'constraints' => [ + new Assert\NotBlank(), + new Assert\Length(max: 50), + ], + ])->add('countryId', ChoiceType::class, [ + 'label' => 'Pays', + 'choices' => array_flip($this->pays->obtenirPays()), + ])->add('lastname', TextType::class, [ + 'label' => 'Nom', + 'required' => false, + 'constraints' => [ + new Assert\Length(max: 50), + ], + ])->add('firstname', TextType::class, [ + 'label' => 'Prénom', + 'required' => false, + 'constraints' => [ + new Assert\Length(max: 50), + ], + ])->add('phone', TextType::class, [ + 'label' => 'Tel', + 'required' => false, + 'constraints' => [ + new Assert\Length(max: 30), + ], + ])->add('email', EmailType::class, [ + 'label' => 'Email (facture)', + 'constraints' => [ + new Assert\NotBlank(), + new Assert\Length(max: 100), + ], + ])->add('tvaIntra', TextType::class, [ + 'label' => 'TVA intracommunautaire (facture)', + 'required' => false, + 'constraints' => [ + new Assert\Length(max: 20), + ], + ])->add('refClt1', TextType::class, [ + 'label' => 'Référence client', + 'required' => false, + 'constraints' => [ + new Assert\Length(max: 50), + ], + ])->add('refClt2', TextType::class, [ + 'label' => 'Référence client 2', + 'required' => false, + 'constraints' => [ + new Assert\Length(max: 50), + ], + ])->add('refClt3', TextType::class, [ + 'label' => 'Référence client 3', + 'required' => false, + 'constraints' => [ + new Assert\Length(max: 50), + ], + ])->add('observation', TextareaType::class, [ + 'required' => false, + 'label' => 'Observation', + ])->add('currency', EnumType::class, [ + 'required' => false, + 'class' => InvoicingCurrency::class, + 'attr' => ['size' => count(InvoicingCurrency::cases())], + 'label' => 'Monnaie de la facture', + 'placeholder' => false, + ])->add('details', CollectionType::class, [ + 'entry_type' => InvoicingRowType::class, + 'keep_as_list' => true, + 'allow_add' => false, + 'allow_delete' => false, + ])->add('quotationNumber', TextType::class, [ + 'label' => 'Numéro de devis', + 'required' => false, + 'attr' => ['readonly' => 'readonly'], + 'constraints' => [ + new Assert\Length(max: 50), + ], + ])->add('invoiceNumber', TextType::class, [ + 'label' => 'Numéro facture', + 'required' => false, + 'attr' => ['readonly' => 'readonly'], + 'constraints' => [ + new Assert\Length(max: 50), + ], + ])->add('paymentStatus', EnumType::class, [ + 'required' => false, + 'class' => InvoicingPaymentStatus::class, + 'attr' => ['size' => count(InvoicingPaymentStatus::cases())], + 'label' => 'État paiement', + 'placeholder' => false, + 'choice_label' => fn(InvoicingPaymentStatus $choice, string $key, mixed $value): string => $choice->label(), + ]) + ->add('paymentDate', DateType::class, [ + 'label' => 'Date de paiement', + 'required' => false, + 'widget' => 'single_text', + ]); + } +} diff --git a/sources/AppBundle/Accounting/InvoicingPaymentStatus.php b/sources/AppBundle/Accounting/InvoicingPaymentStatus.php index 8674d7ae9..48a607ccb 100644 --- a/sources/AppBundle/Accounting/InvoicingPaymentStatus.php +++ b/sources/AppBundle/Accounting/InvoicingPaymentStatus.php @@ -9,4 +9,13 @@ enum InvoicingPaymentStatus: int case Waiting = 0; case Payed = 1; case Cancelled = 2; + + public function label(): string + { + return match ($this) { + self::Waiting => 'En attente de paiement', + self::Payed => 'Payé', + self::Cancelled => 'Annulé', + }; + } } diff --git a/sources/AppBundle/Accounting/Model/Invoicing.php b/sources/AppBundle/Accounting/Model/Invoicing.php index 38b519048..2914f8a1f 100644 --- a/sources/AppBundle/Accounting/Model/Invoicing.php +++ b/sources/AppBundle/Accounting/Model/Invoicing.php @@ -6,6 +6,7 @@ use Afup\Site\Utils\Utils; use AppBundle\Accounting\InvoicingCurrency; +use AppBundle\Accounting\InvoicingPaymentStatus; use CCMBenchmark\Ting\Entity\NotifyProperty; use CCMBenchmark\Ting\Entity\NotifyPropertyInterface; use DateTime; @@ -34,7 +35,7 @@ class Invoicing implements NotifyPropertyInterface private string $lastname = ''; private string $firstname = ''; private string $phone = ''; - private int $paymentStatus = 0; + private InvoicingPaymentStatus $paymentStatus = InvoicingPaymentStatus::Waiting; private ?DateTime $paymentDate = null; private ?InvoicingCurrency $currency = null; /** @var InvoicingDetail[] */ @@ -301,12 +302,12 @@ public function setPhone(string $phone): self return $this; } - public function getPaymentStatus(): int + public function getPaymentStatus(): InvoicingPaymentStatus { return $this->paymentStatus; } - public function setPaymentStatus(int $paymentStatus): self + public function setPaymentStatus(InvoicingPaymentStatus $paymentStatus): self { $this->propertyChanged('paymentStatus', $this->paymentStatus, $paymentStatus); $this->paymentStatus = $paymentStatus; diff --git a/sources/AppBundle/Accounting/Model/Repository/InvoicingRepository.php b/sources/AppBundle/Accounting/Model/Repository/InvoicingRepository.php index 2f32109cc..78d2855e3 100644 --- a/sources/AppBundle/Accounting/Model/Repository/InvoicingRepository.php +++ b/sources/AppBundle/Accounting/Model/Repository/InvoicingRepository.php @@ -4,6 +4,7 @@ namespace AppBundle\Accounting\Model\Repository; +use AppBundle\Accounting\InvoicingPaymentStatus; use CCMBenchmark\Ting\Repository\Hydrator\AggregateFrom; use CCMBenchmark\Ting\Repository\Hydrator\AggregateTo; use CCMBenchmark\Ting\Repository\Hydrator\RelationMany; @@ -25,21 +26,21 @@ */ class InvoicingRepository extends Repository implements MetadataInitializer { - public function getQuotationById(int $periodId): ?Invoicing + public function getById(int $id): ?Invoicing { /** @var Select $builder */ $builder = $this->getQueryBuilder(self::QUERY_SELECT); $builder->cols(['acf.*', 'acfd.*']) ->from('afup_compta_facture acf') ->leftJoin('afup_compta_facture_details acfd', 'acfd.idafup_compta_facture = acf.id') - ->where('acf.id = :periodId'); + ->where('acf.id = :id'); $hydrator = new HydratorRelational(); $hydrator->addRelation(new RelationMany(new AggregateFrom('acfd'), new AggregateTo('acf'), 'setDetails')); $hydrator->callableFinalizeAggregate(fn(array $row) => $row['acf']); $collection = $this->getQuery($builder->getStatement()) - ->setParams(['periodId' => $periodId]) + ->setParams(['id' => $id]) ->query($this->getCollection($hydrator)); if ($collection->count() === 0) { @@ -240,7 +241,11 @@ public static function initMetadata(SerializerFactoryInterface $serializerFactor ->addField([ 'columnName' => 'etat_paiement', 'fieldName' => 'paymentStatus', - 'type' => 'int', + 'type' => 'enum', + 'serializer' => BackedEnum::class, + 'serializer_options' => [ + 'unserialize' => ['enum' => InvoicingPaymentStatus::class], + ], ]) ->addField([ 'columnName' => 'date_paiement', diff --git a/sources/AppBundle/Controller/Admin/Accounting/Invoice/EditInvoiceAction.php b/sources/AppBundle/Controller/Admin/Accounting/Invoice/EditInvoiceAction.php new file mode 100644 index 000000000..a3dda4a3c --- /dev/null +++ b/sources/AppBundle/Controller/Admin/Accounting/Invoice/EditInvoiceAction.php @@ -0,0 +1,54 @@ +query->getInt('invoiceId'); + $invoice = $this->invoicingRepository->getById($invoiceId); + if (!$invoice instanceof Invoicing) { + throw $this->createNotFoundException("Cette facture n'existe pas"); + } + + $form = $this->createForm(InvoiceType::class, $invoice); + $form->handleRequest($request); + if ($form->isSubmitted() && $form->isValid()) { + try { + $this->invoicingRepository->startTransaction(); + foreach ($invoice->getDetails() as $detail) { + $this->invoicingDetailRepository->save($detail); + } + $this->invoicingRepository->save($invoice); + $this->invoicingRepository->commit(); + $this->addFlash('success', 'L\'écriture a été modifiée'); + return $this->redirectToRoute('admin_accounting_invoices_list'); + } catch (\Exception $e) { + $this->invoicingRepository->rollback(); + $this->addFlash('error', 'L\'écriture n\'a pas pu être enregistrée'); + } + } + + return $this->render('admin/accounting/invoice/edit.html.twig', [ + 'invoice' => $invoice, + 'form' => $form->createView(), + 'submitLabel' => 'Modifier', + ]); + } +} diff --git a/sources/AppBundle/Controller/Admin/Accounting/Invoice/ListInvoiceAction.php b/sources/AppBundle/Controller/Admin/Accounting/Invoice/ListInvoiceAction.php index 469b9f29f..484557269 100644 --- a/sources/AppBundle/Controller/Admin/Accounting/Invoice/ListInvoiceAction.php +++ b/sources/AppBundle/Controller/Admin/Accounting/Invoice/ListInvoiceAction.php @@ -38,7 +38,7 @@ public function __invoke(Request $request): Response /** @var Invoicing $invoice */ foreach ($invoices as $invoice) { - if ($invoice->getPaymentStatus() === InvoicingPaymentStatus::Cancelled->value) { + if ($invoice->getPaymentStatus() === InvoicingPaymentStatus::Cancelled) { continue; } diff --git a/sources/AppBundle/Controller/Admin/Accounting/Quotation/AddQuotationAction.php b/sources/AppBundle/Controller/Admin/Accounting/Quotation/AddQuotationAction.php index 469bdb9ad..bfe8f8e0b 100644 --- a/sources/AppBundle/Controller/Admin/Accounting/Quotation/AddQuotationAction.php +++ b/sources/AppBundle/Controller/Admin/Accounting/Quotation/AddQuotationAction.php @@ -54,7 +54,7 @@ public function __invoke(Request $request): Response private function init(int $quotationId): Invoicing { - $baseQuotation = $this->invoicingRepository->getQuotationById($quotationId); + $baseQuotation = $this->invoicingRepository->getById($quotationId); if (!$baseQuotation instanceof Invoicing) { $quotation = new Invoicing(); $quotation->setQuotationDate(new \DateTime()); diff --git a/sources/AppBundle/Controller/Admin/Accounting/Quotation/EditQuotationAction.php b/sources/AppBundle/Controller/Admin/Accounting/Quotation/EditQuotationAction.php index 4daa9e0b7..01506d042 100644 --- a/sources/AppBundle/Controller/Admin/Accounting/Quotation/EditQuotationAction.php +++ b/sources/AppBundle/Controller/Admin/Accounting/Quotation/EditQuotationAction.php @@ -21,7 +21,7 @@ public function __construct( public function __invoke(Request $request): Response { $quotationId = $request->query->getInt('quotationId'); - $quotation = $this->invoicingRepository->getQuotationById($quotationId); + $quotation = $this->invoicingRepository->getById($quotationId); if ($quotation === null) { throw $this->createNotFoundException("Ce devis n'existe pas"); } diff --git a/templates/admin/accounting/invoice/edit.html.twig b/templates/admin/accounting/invoice/edit.html.twig new file mode 100644 index 000000000..d40a6424d --- /dev/null +++ b/templates/admin/accounting/invoice/edit.html.twig @@ -0,0 +1,175 @@ +{% extends 'admin/base_with_header.html.twig' %} + +{% block content %} +

Modifier une facture

+ + + + {% form_theme form 'form_theme_admin.html.twig' %} + + {{ form_start(form) }} +
+

Détail facture

+
+
+
+ {{ form_row(form.invoiceDate) }} +
+
+
+ +
+

Facturation

+
+
+
+
+
+
+
+ Ces informations concernent la personne ou la société qui sera facturée

+
+
+ {{ form_row(form.company) }} + {{ form_row(form.service) }} + {{ form_row(form.address) }} + {{ form_row(form.zipcode) }} + {{ form_row(form.city) }} + {{ form_row(form.countryId) }} +
+
+
+ +
+

Contact

+
+
+
+ {{ form_row(form.lastname) }} + {{ form_row(form.firstname) }} + {{ form_row(form.phone) }} + {{ form_row(form.email) }} + {{ form_row(form.tvaIntra) }} +
+
+
+ +
+

Réservé à l'administration

+
+
+
+
+
+
+
+ Numéro généré automatiquement et affiché en automatique

+
+
+ {{ form_row(form.quotationNumber) }} + {{ form_row(form.invoiceNumber) }} +
+
+
+ +
+

Référence client

+
+
+
+
+
+
+
+ Possible d'avoir plusieurs références à mettre (obligation client)

+
+
+ {{ form_row(form.refClt1) }} + {{ form_row(form.refClt2) }} + {{ form_row(form.refClt3) }} +
+
+
+ +
+

Observation

+
+
+
+
+
+
+
+ Ces informations seront écrites à la fin du document

+
+
+ {{ form_row(form.observation) }} +
+
+
+ +
+

Paiement

+
+
+
+ {{ form_row(form.currency) }} + {{ form_row(form.paymentStatus) }} + {{ form_row(form.paymentDate) }} +
+
+
+ +
+
+
+

Contenu

+
+
+
+ {% for detail in form.details %} +
+
+
+
+
Ligne {{ loop.index }}
+
+
+ {{ form_row(detail.reference) }} +
+
+
Rappel : sponsoring 20%, place supplémentaire 10%.
+
+ {{ form_row(detail.tva) }} + {{ form_row(detail.designation) }} + {{ form_row(detail.quantity) }} + {{ form_row(detail.unitPrice) }} +
+ {% endfor %} +
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+

+ * indique un champ obligatoire +

+
+ + {{ form_end(form) }} + +{% endblock %} diff --git a/templates/admin/accounting/invoice/list.html.twig b/templates/admin/accounting/invoice/list.html.twig index ca823e971..2c831cedd 100644 --- a/templates/admin/accounting/invoice/list.html.twig +++ b/templates/admin/accounting/invoice/list.html.twig @@ -40,15 +40,15 @@ {% for line in lines %} - {{ line.quotationDate|format_date('short') }} + {{ line.invoiceDate|format_date('short') }} {{ line.company }} {{ line.city }} {{ line.invoiceNumber }} {{ line.refClt1 }} - {% if line.paymentStatus == 2 %} + {% if line.paymentStatus == enum('AppBundle\\Accounting\\InvoicingPaymentStatus').Cancelled %} Annulé - {% elseif line.paymentStatus == 1 %} + {% elseif line.paymentStatus == enum('AppBundle\\Accounting\\InvoicingPaymentStatus').Payed %} Payé {% else %} En attente @@ -56,12 +56,12 @@ {{ line.price|number_format(2, ',', ' ') }} - - + {% else %} - En attente de paiement {% endif %} - Voir diff --git a/tests/behat/bootstrap/PdfContext.php b/tests/behat/bootstrap/PdfContext.php index f5bf3b4f7..4e87eca06 100644 --- a/tests/behat/bootstrap/PdfContext.php +++ b/tests/behat/bootstrap/PdfContext.php @@ -33,6 +33,9 @@ public function iParseThePdfContent(): void foreach ($pages as $i => $page) { $this->pdfPages[++$i] = $page->getText(); } + + unset($pdf, $parser, $pages, $pageContent); + gc_collect_cycles(); } #[Then('The page :page of the PDF should contain :content')] diff --git a/tests/behat/features/Admin/Tresorerie/DevisFactures.feature b/tests/behat/features/Admin/Tresorerie/DevisFactures.feature index 7597e5c0a..f22785187 100644 --- a/tests/behat/features/Admin/Tresorerie/DevisFactures.feature +++ b/tests/behat/features/Admin/Tresorerie/DevisFactures.feature @@ -117,8 +117,9 @@ Feature: Administration - Trésorerie - Devis/Facture And I should see a yellow label "En attente" # Modification de la facture When I follow the button of tooltip "Modifier la ligne ESN dev en folie" and wait until I see "Modifier une facture" - Then I fill in "ville" with "Paris Cedex 7" - Then I select "1" from "etat_paiement" + Then I fill in "invoice[city]" with "Paris Cedex 7" + And I select "1" from "invoice[paymentStatus]" + And I fill in "invoice[paymentDate]" with "2026-12-31" When I press "Modifier" and wait until I see "L'écriture a été modifiée" And I should see "Paris Cedex 7" And I should see "Payé"