From a5e540f5087a88215e9dbef668b014bde1f7cf83 Mon Sep 17 00:00:00 2001 From: Gustavo Atencio Mauricio <53370174+gustavoam-asdf@users.noreply.github.com> Date: Wed, 9 Sep 2026 19:00:04 -0500 Subject: [PATCH] feat(core): support global discount total on credit and debit notes SUNAT's XML guide for the electronic credit note defines a total for global discounts, but there is no way to set it with Greenter today. Field 25 of the guide, "Total de Descuentos": > A traves de este elemento se debe indicar el valor total de los descuentos > realizados de ser el caso. Su proposito es permitir consignar en el > comprobante de pago la sumatoria de los descuentos de cada linea > (descuentos por item). > > Ubicacion: //CreditNote/cac:LegalMonetaryTotal/cbc:AllowanceTotalAmount The sibling field is already there: BaseSale has `sumOtrosCargos` and both note templates render `cbc:ChargeTotalAmount`. Only the discount twin was missing, so a note cannot report discounts at all. - `sumOtrosDescuentos` moves from Invoice to BaseSale, right next to `sumOtrosCargos`. Invoice keeps its public API by inheritance and drops the duplicated property; the setter now returns `self` with a `@return $this` docblock, exactly like `setSumOtrosCargos` already did. - `notacr2.1` and `notadb2.1` render `cbc:AllowanceTotalAmount` before `cbc:ChargeTotalAmount`, which is the order `MonetaryTotalType` requires. - Two tests, one per note type, asserting the node and validating the document against the UBL schema. Related to #232: this covers the global discount half of that report. The debit note interest reason mentioned there is not addressed here. Co-Authored-By: Claude Opus 5 (1M context) --- .../core/src/Core/Model/Sale/BaseSale.php | 25 ++++++++++++++++ packages/core/src/Core/Model/Sale/Invoice.php | 25 ---------------- .../xml/src/Xml/Templates/notacr2.1.xml.twig | 3 ++ .../xml/src/Xml/Templates/notadb2.1.xml.twig | 3 ++ .../Xml/Builder/v21/FeNoteBuilderTest.php | 29 +++++++++++++++++++ 5 files changed, 60 insertions(+), 25 deletions(-) diff --git a/packages/core/src/Core/Model/Sale/BaseSale.php b/packages/core/src/Core/Model/Sale/BaseSale.php index 17d3bc19..f8937cc1 100644 --- a/packages/core/src/Core/Model/Sale/BaseSale.php +++ b/packages/core/src/Core/Model/Sale/BaseSale.php @@ -60,6 +60,11 @@ class BaseSale implements DocumentInterface */ protected $tipoMoneda; + /** + * @var float + */ + protected $sumOtrosDescuentos; + /** * @var float */ @@ -351,6 +356,26 @@ public function setTipoMoneda(?string $tipoMoneda): self return $this; } + /** + * @return float + */ + public function getSumOtrosDescuentos(): ?float + { + return $this->sumOtrosDescuentos; + } + + /** + * @param float $sumOtrosDescuentos + * + * @return $this + */ + public function setSumOtrosDescuentos(?float $sumOtrosDescuentos): self + { + $this->sumOtrosDescuentos = $sumOtrosDescuentos; + + return $this; + } + /** * @return float */ diff --git a/packages/core/src/Core/Model/Sale/Invoice.php b/packages/core/src/Core/Model/Sale/Invoice.php index c6feb474..2ffced88 100644 --- a/packages/core/src/Core/Model/Sale/Invoice.php +++ b/packages/core/src/Core/Model/Sale/Invoice.php @@ -43,11 +43,6 @@ class Invoice extends BaseSale */ private $mtoDescuentos; - /** - * @var float - */ - private $sumOtrosDescuentos; - /** * @var Charge[] */ @@ -195,26 +190,6 @@ public function setMtoDescuentos(?float $mtoDescuentos): Invoice return $this; } - /** - * @return float - */ - public function getSumOtrosDescuentos(): ?float - { - return $this->sumOtrosDescuentos; - } - - /** - * @param float $sumOtrosDescuentos - * - * @return Invoice - */ - public function setSumOtrosDescuentos(?float $sumOtrosDescuentos): Invoice - { - $this->sumOtrosDescuentos = $sumOtrosDescuentos; - - return $this; - } - /** * @return Charge[] */ diff --git a/packages/xml/src/Xml/Templates/notacr2.1.xml.twig b/packages/xml/src/Xml/Templates/notacr2.1.xml.twig index 7492a54f..8e4a35d7 100644 --- a/packages/xml/src/Xml/Templates/notacr2.1.xml.twig +++ b/packages/xml/src/Xml/Templates/notacr2.1.xml.twig @@ -278,6 +278,9 @@ {% endif %} + {% if doc.sumOtrosDescuentos is not null %} + {{ doc.sumOtrosDescuentos|n_format }} + {% endif %} {% if doc.sumOtrosCargos is not null %} {{ doc.sumOtrosCargos|n_format }} {% endif %} diff --git a/packages/xml/src/Xml/Templates/notadb2.1.xml.twig b/packages/xml/src/Xml/Templates/notadb2.1.xml.twig index ae0eb113..37f2ff84 100644 --- a/packages/xml/src/Xml/Templates/notadb2.1.xml.twig +++ b/packages/xml/src/Xml/Templates/notadb2.1.xml.twig @@ -265,6 +265,9 @@ {% if doc.subTotal is not null %} {{ doc.subTotal|n_format }} {% endif %} + {% if doc.sumOtrosDescuentos is not null %} + {{ doc.sumOtrosDescuentos|n_format }} + {% endif %} {% if doc.sumOtrosCargos is not null %} {{ doc.sumOtrosCargos|n_format }} {% endif %} diff --git a/packages/xml/tests/Xml/Builder/v21/FeNoteBuilderTest.php b/packages/xml/tests/Xml/Builder/v21/FeNoteBuilderTest.php index 10b5579d..05c219db 100644 --- a/packages/xml/tests/Xml/Builder/v21/FeNoteBuilderTest.php +++ b/packages/xml/tests/Xml/Builder/v21/FeNoteBuilderTest.php @@ -51,4 +51,33 @@ public function testCreateXmlDebitNote() $this->assertNotEmpty($xml); $this->assertSchema($xml); } + + /** + * @dataProvider noteTypeProvider + */ + public function testCreateXmlNoteWithGlobalDiscount(string $tipoDoc) + { + /**@var $note Note*/ + $note = $this->createDocument(NoteStore::class); + $note->setTipoDoc($tipoDoc); + $note->setUblVersion('2.1'); + $note->setSumOtrosDescuentos(20.50); + + $xml = $this->build($note); + + $this->assertNotEmpty($xml); + $this->assertSchema($xml); + $this->assertStringContainsString( + '20.50', + $xml + ); + } + + public function noteTypeProvider(): array + { + return [ + 'credit note' => ['07'], + 'debit note' => ['08'], + ]; + } } \ No newline at end of file