From 9cb577bcca1c7770a1028a361d20b9015c90fa2f Mon Sep 17 00:00:00 2001 From: vgreb Date: Wed, 8 Apr 2026 21:24:14 +0200 Subject: [PATCH 1/4] =?UTF-8?q?Refonte=20Tr=C3=A9sorie=20>=20Facture=20-?= =?UTF-8?q?=20Ajout=20/=20Edition?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config/routing/admin_accounting.yml | 4 + composer.lock | 2 +- .../AppBundle/Accounting/Form/InvoiceType.php | 186 ++++++++++++++++++ .../Accounting/InvoicingPaymentStatus.php | 9 + .../AppBundle/Accounting/Model/Invoicing.php | 7 +- .../Model/Repository/InvoicingRepository.php | 13 +- .../Accounting/Invoice/EditInvoiceAction.php | 60 ++++++ .../Accounting/Invoice/ListInvoiceAction.php | 2 +- .../Quotation/AddQuotationAction.php | 2 +- .../Quotation/EditQuotationAction.php | 2 +- .../admin/accounting/invoice/edit.html.twig | 181 +++++++++++++++++ .../admin/accounting/invoice/list.html.twig | 10 +- .../admin/accounting/quotation/list.html.twig | 2 +- templates/admin/accounting/search.html.twig | 2 +- .../Admin/Tresorerie/DevisFactures.feature | 24 ++- 15 files changed, 486 insertions(+), 20 deletions(-) create mode 100644 sources/AppBundle/Accounting/Form/InvoiceType.php create mode 100644 sources/AppBundle/Controller/Admin/Accounting/Invoice/EditInvoiceAction.php create mode 100644 templates/admin/accounting/invoice/edit.html.twig 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/composer.lock b/composer.lock index 7bd095976..e16d18955 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2e8f8a5acf16d7cd9a324efb6861a699", + "content-hash": "8034c8e4a174367274f53e046c9071b1", "packages": [ { "name": "algolia/algoliasearch-client-php", diff --git a/sources/AppBundle/Accounting/Form/InvoiceType.php b/sources/AppBundle/Accounting/Form/InvoiceType.php new file mode 100644 index 000000000..ca3fce954 --- /dev/null +++ b/sources/AppBundle/Accounting/Form/InvoiceType.php @@ -0,0 +1,186 @@ +add('invoiceDate', DateType::class, [ + 'label' => 'Date facture', + 'required' => true, + 'widget' => 'single_text', + ])->add('company', TextType::class, [ + 'label' => 'Société', + 'empty_data' => '', + 'constraints' => [ + new Assert\NotBlank(), + new Assert\Type('string'), + new Assert\Length(max: 50), + ], + ])->add('service', TextType::class, [ + 'label' => 'Service', + 'required' => false, + 'empty_data' => '', + 'constraints' => [ + new Assert\Type('string'), + new Assert\Length(max: 50), + ], + ])->add('address', TextareaType::class, [ + 'label' => 'Adresse', + 'empty_data' => '', + 'constraints' => [ + new Assert\Type('string'), + ], + ])->add('zipcode', TextType::class, [ + 'label' => 'Code postal', + 'empty_data' => '', + 'constraints' => [ + new Assert\NotBlank(), + new Assert\Type('string'), + new Assert\Length(max: 10), + ], + ])->add('city', TextType::class, [ + 'label' => 'Ville', + 'empty_data' => '', + 'constraints' => [ + new Assert\NotBlank(), + new Assert\Type('string'), + 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, + 'empty_data' => '', + 'constraints' => [ + new Assert\Type('string'), + new Assert\Length(max: 50), + ], + ])->add('firstname', TextType::class, [ + 'label' => 'Prénom', + 'required' => false, + 'empty_data' => '', + 'constraints' => [ + new Assert\Type('string'), + new Assert\Length(max: 50), + ], + ])->add('phone', TextType::class, [ + 'label' => 'Tel', + 'required' => false, + 'empty_data' => '', + 'constraints' => [ + new Assert\Type('string'), + new Assert\Length(max: 30), + ], + ])->add('email', EmailType::class, [ + 'label' => 'Email (facture)', + 'required' => true, + 'empty_data' => '', + 'constraints' => [ + new Assert\NotBlank(), + new Assert\Type('string'), + new Assert\Length(max: 100), + ], + ])->add('tvaIntra', TextType::class, [ + 'label' => 'TVA intracommunautaire (facture)', + 'required' => false, + 'empty_data' => '', + 'constraints' => [ + new Assert\Type('string'), + new Assert\Length(max: 20), + ], + ])->add('refClt1', TextType::class, [ + 'label' => 'Référence client', + 'required' => false, + 'empty_data' => '', + 'constraints' => [ + new Assert\Type('string'), + new Assert\Length(max: 50), + ], + ])->add('refClt2', TextType::class, [ + 'label' => 'Référence client 2', + 'required' => false, + 'empty_data' => '', + 'constraints' => [ + new Assert\Type('string'), + new Assert\Length(max: 50), + ], + ])->add('refClt3', TextType::class, [ + 'label' => 'Référence client 3', + 'required' => false, + 'empty_data' => '', + 'constraints' => [ + new Assert\Type('string'), + new Assert\Length(max: 50), + ], + ])->add('observation', TextareaType::class, [ + 'required' => false, + 'empty_data' => '', + '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, + 'delete_empty' => $this->isEmpty(...), + ])->add('quotationNumber', TextType::class, [ + 'label' => 'Numéro de devis', + 'required' => false, + 'attr' => ['readonly' => 'readonly'], + 'constraints' => [ + new Assert\Type('string'), + new Assert\Length(max: 50), + ], + ])->add('invoiceNumber', TextType::class, [ + 'label' => 'Numéro facture', + 'required' => false, + 'attr' => ['readonly' => 'readonly'], + 'constraints' => [ + new Assert\Type('string'), + 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', + ]); + } + + private function isEmpty(?InvoicingDetail $detail = null): bool + { + return null === $detail || (empty($detail->getUnitPrice()) && empty($detail->getQuantity())); + } +} 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..f880f925a --- /dev/null +++ b/sources/AppBundle/Controller/Admin/Accounting/Invoice/EditInvoiceAction.php @@ -0,0 +1,60 @@ +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(); + $keepIds = array_filter(array_map(fn($d) => $d->getId(), $invoice->getDetails())); + $existingIds = $this->invoicingDetailRepository->getRowsIdsPerInvoicingId($invoiceId); + $toDelete = array_diff($existingIds, $keepIds); + if ($toDelete !== []) { + $this->invoicingDetailRepository->removeRowsPerIds($toDelete); + } + 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..e2b403510 --- /dev/null +++ b/templates/admin/accounting/invoice/edit.html.twig @@ -0,0 +1,181 @@ +{% 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/features/Admin/Tresorerie/DevisFactures.feature b/tests/behat/features/Admin/Tresorerie/DevisFactures.feature index 7597e5c0a..818812288 100644 --- a/tests/behat/features/Admin/Tresorerie/DevisFactures.feature +++ b/tests/behat/features/Admin/Tresorerie/DevisFactures.feature @@ -117,11 +117,31 @@ 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" + And I should see "Modifier une facture" + 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é" + # Ajout puis suppression d'une ligne dans la facture (vérifie la suppression en base) + When I follow the button of tooltip "Modifier la ligne ESN dev en folie" and wait until I see "Modifier une facture" + Then I click on link with class "add_item_link" + And I fill in "invoice[details][1][reference]" with "BONUS-001" + And I fill in "invoice[details][1][designation]" with "Ligne bonus" + And I fill in "invoice[details][1][quantity]" with "1" + And I fill in "invoice[details][1][unitPrice]" with "500" + When I press "Modifier" and wait until I see "L'écriture a été modifiée" + Then I should see "12 500,00" + When I follow the button of tooltip "Modifier la ligne ESN dev en folie" and wait until I see "Modifier une facture" + Then I click on link with id "remove_row_1" + When I press "Modifier" and wait until I see "L'écriture a été modifiée" + Then I should see "12 000,00" + When I follow the button of tooltip "Modifier la ligne ESN dev en folie" and wait until I see "Modifier une facture" + Then I should not see "BONUS-001" + When I go to "/admin/" + When I open menu "Trésorerie" + And I follow "Factures" # Envoi de la facture par email Then I follow the button of tooltip "Envoyer la facture 2026-3 par mail" And I should only receive the following emails: From bc7e3af43b751ea289405fc9951962f740f181a5 Mon Sep 17 00:00:00 2001 From: vgreb Date: Thu, 9 Apr 2026 08:43:10 +0200 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20appel=20au=20garbage=20collector=20p?= =?UTF-8?q?our=20limiter=20la=20consommation=20m=C3=A9moire?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- composer.lock | 2 +- .../AppBundle/Accounting/Form/InvoiceType.php | 8 ++------ .../admin/accounting/invoice/edit.html.twig | 6 ------ tests/behat/bootstrap/PdfContext.php | 3 +++ .../Admin/Tresorerie/DevisFactures.feature | 18 ------------------ 5 files changed, 6 insertions(+), 31 deletions(-) diff --git a/composer.lock b/composer.lock index e16d18955..7bd095976 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8034c8e4a174367274f53e046c9071b1", + "content-hash": "2e8f8a5acf16d7cd9a324efb6861a699", "packages": [ { "name": "algolia/algoliasearch-client-php", diff --git a/sources/AppBundle/Accounting/Form/InvoiceType.php b/sources/AppBundle/Accounting/Form/InvoiceType.php index ca3fce954..ac2fc1224 100644 --- a/sources/AppBundle/Accounting/Form/InvoiceType.php +++ b/sources/AppBundle/Accounting/Form/InvoiceType.php @@ -147,7 +147,8 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ])->add('details', CollectionType::class, [ 'entry_type' => InvoicingRowType::class, 'keep_as_list' => true, - 'delete_empty' => $this->isEmpty(...), + 'allow_add' => false, + 'allow_delete' => false, ])->add('quotationNumber', TextType::class, [ 'label' => 'Numéro de devis', 'required' => false, @@ -178,9 +179,4 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'widget' => 'single_text', ]); } - - private function isEmpty(?InvoicingDetail $detail = null): bool - { - return null === $detail || (empty($detail->getUnitPrice()) && empty($detail->getQuantity())); - } } diff --git a/templates/admin/accounting/invoice/edit.html.twig b/templates/admin/accounting/invoice/edit.html.twig index e2b403510..d40a6424d 100644 --- a/templates/admin/accounting/invoice/edit.html.twig +++ b/templates/admin/accounting/invoice/edit.html.twig @@ -151,12 +151,6 @@ {% endfor %} -
-
-
-
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 818812288..79162d6f9 100644 --- a/tests/behat/features/Admin/Tresorerie/DevisFactures.feature +++ b/tests/behat/features/Admin/Tresorerie/DevisFactures.feature @@ -124,24 +124,6 @@ Feature: Administration - Trésorerie - Devis/Facture 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é" - # Ajout puis suppression d'une ligne dans la facture (vérifie la suppression en base) - When I follow the button of tooltip "Modifier la ligne ESN dev en folie" and wait until I see "Modifier une facture" - Then I click on link with class "add_item_link" - And I fill in "invoice[details][1][reference]" with "BONUS-001" - And I fill in "invoice[details][1][designation]" with "Ligne bonus" - And I fill in "invoice[details][1][quantity]" with "1" - And I fill in "invoice[details][1][unitPrice]" with "500" - When I press "Modifier" and wait until I see "L'écriture a été modifiée" - Then I should see "12 500,00" - When I follow the button of tooltip "Modifier la ligne ESN dev en folie" and wait until I see "Modifier une facture" - Then I click on link with id "remove_row_1" - When I press "Modifier" and wait until I see "L'écriture a été modifiée" - Then I should see "12 000,00" - When I follow the button of tooltip "Modifier la ligne ESN dev en folie" and wait until I see "Modifier une facture" - Then I should not see "BONUS-001" - When I go to "/admin/" - When I open menu "Trésorerie" - And I follow "Factures" # Envoi de la facture par email Then I follow the button of tooltip "Envoyer la facture 2026-3 par mail" And I should only receive the following emails: From 10c4fc0e697ce0fb5f9d8a01e0542d2d25bda425 Mon Sep 17 00:00:00 2001 From: vgreb Date: Tue, 14 Apr 2026 22:27:12 +0200 Subject: [PATCH 3/4] Retour PR --- app/config/packages/backoffice_menu.yaml | 1 + .../AppBundle/Accounting/Form/InvoiceType.php | 33 ------------------- 2 files changed, 1 insertion(+), 33 deletions(-) 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/sources/AppBundle/Accounting/Form/InvoiceType.php b/sources/AppBundle/Accounting/Form/InvoiceType.php index ac2fc1224..05a4dcca2 100644 --- a/sources/AppBundle/Accounting/Form/InvoiceType.php +++ b/sources/AppBundle/Accounting/Form/InvoiceType.php @@ -26,44 +26,31 @@ public function buildForm(FormBuilderInterface $builder, array $options): void { $builder->add('invoiceDate', DateType::class, [ 'label' => 'Date facture', - 'required' => true, 'widget' => 'single_text', ])->add('company', TextType::class, [ 'label' => 'Société', - 'empty_data' => '', 'constraints' => [ new Assert\NotBlank(), - new Assert\Type('string'), new Assert\Length(max: 50), ], ])->add('service', TextType::class, [ 'label' => 'Service', 'required' => false, - 'empty_data' => '', 'constraints' => [ - new Assert\Type('string'), new Assert\Length(max: 50), ], ])->add('address', TextareaType::class, [ 'label' => 'Adresse', - 'empty_data' => '', - 'constraints' => [ - new Assert\Type('string'), - ], ])->add('zipcode', TextType::class, [ 'label' => 'Code postal', - 'empty_data' => '', 'constraints' => [ new Assert\NotBlank(), - new Assert\Type('string'), new Assert\Length(max: 10), ], ])->add('city', TextType::class, [ 'label' => 'Ville', - 'empty_data' => '', 'constraints' => [ new Assert\NotBlank(), - new Assert\Type('string'), new Assert\Length(max: 50), ], ])->add('countryId', ChoiceType::class, [ @@ -72,71 +59,53 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ])->add('lastname', TextType::class, [ 'label' => 'Nom', 'required' => false, - 'empty_data' => '', 'constraints' => [ - new Assert\Type('string'), new Assert\Length(max: 50), ], ])->add('firstname', TextType::class, [ 'label' => 'Prénom', 'required' => false, - 'empty_data' => '', 'constraints' => [ - new Assert\Type('string'), new Assert\Length(max: 50), ], ])->add('phone', TextType::class, [ 'label' => 'Tel', 'required' => false, - 'empty_data' => '', 'constraints' => [ - new Assert\Type('string'), new Assert\Length(max: 30), ], ])->add('email', EmailType::class, [ 'label' => 'Email (facture)', - 'required' => true, - 'empty_data' => '', 'constraints' => [ new Assert\NotBlank(), - new Assert\Type('string'), new Assert\Length(max: 100), ], ])->add('tvaIntra', TextType::class, [ 'label' => 'TVA intracommunautaire (facture)', 'required' => false, - 'empty_data' => '', 'constraints' => [ - new Assert\Type('string'), new Assert\Length(max: 20), ], ])->add('refClt1', TextType::class, [ 'label' => 'Référence client', 'required' => false, - 'empty_data' => '', 'constraints' => [ - new Assert\Type('string'), new Assert\Length(max: 50), ], ])->add('refClt2', TextType::class, [ 'label' => 'Référence client 2', 'required' => false, - 'empty_data' => '', 'constraints' => [ - new Assert\Type('string'), new Assert\Length(max: 50), ], ])->add('refClt3', TextType::class, [ 'label' => 'Référence client 3', 'required' => false, - 'empty_data' => '', 'constraints' => [ - new Assert\Type('string'), new Assert\Length(max: 50), ], ])->add('observation', TextareaType::class, [ 'required' => false, - 'empty_data' => '', 'label' => 'Observation', ])->add('currency', EnumType::class, [ 'required' => false, @@ -154,7 +123,6 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'required' => false, 'attr' => ['readonly' => 'readonly'], 'constraints' => [ - new Assert\Type('string'), new Assert\Length(max: 50), ], ])->add('invoiceNumber', TextType::class, [ @@ -162,7 +130,6 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'required' => false, 'attr' => ['readonly' => 'readonly'], 'constraints' => [ - new Assert\Type('string'), new Assert\Length(max: 50), ], ])->add('paymentStatus', EnumType::class, [ From 77c471bfe4f0c257d4e24ad1b2c0835f2193900b Mon Sep 17 00:00:00 2001 From: vgreb Date: Tue, 21 Apr 2026 16:55:46 +0200 Subject: [PATCH 4/4] Retour PR --- .../Admin/Accounting/Invoice/EditInvoiceAction.php | 6 ------ tests/behat/features/Admin/Tresorerie/DevisFactures.feature | 1 - 2 files changed, 7 deletions(-) diff --git a/sources/AppBundle/Controller/Admin/Accounting/Invoice/EditInvoiceAction.php b/sources/AppBundle/Controller/Admin/Accounting/Invoice/EditInvoiceAction.php index f880f925a..a3dda4a3c 100644 --- a/sources/AppBundle/Controller/Admin/Accounting/Invoice/EditInvoiceAction.php +++ b/sources/AppBundle/Controller/Admin/Accounting/Invoice/EditInvoiceAction.php @@ -32,12 +32,6 @@ public function __invoke(Request $request): Response if ($form->isSubmitted() && $form->isValid()) { try { $this->invoicingRepository->startTransaction(); - $keepIds = array_filter(array_map(fn($d) => $d->getId(), $invoice->getDetails())); - $existingIds = $this->invoicingDetailRepository->getRowsIdsPerInvoicingId($invoiceId); - $toDelete = array_diff($existingIds, $keepIds); - if ($toDelete !== []) { - $this->invoicingDetailRepository->removeRowsPerIds($toDelete); - } foreach ($invoice->getDetails() as $detail) { $this->invoicingDetailRepository->save($detail); } diff --git a/tests/behat/features/Admin/Tresorerie/DevisFactures.feature b/tests/behat/features/Admin/Tresorerie/DevisFactures.feature index 79162d6f9..f22785187 100644 --- a/tests/behat/features/Admin/Tresorerie/DevisFactures.feature +++ b/tests/behat/features/Admin/Tresorerie/DevisFactures.feature @@ -117,7 +117,6 @@ 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" - And I should see "Modifier une facture" 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"