diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c1661c3..aa99ad8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Los cambios notables de cada lanzamiento serán documentados en este archivo. - #276 CI: tests unitarios separados de los de integración (SUNAT beta ya no bloquea PRs). - Corregir parámetros implícitamente nullable (deprecación PHP 8.4). - Registrar filtros y funciones Twig del reporte con instancias (compatibilidad con Psalm 5.26). +- #258 Retención/Percepción: agregar `Payment::numero` (número de cuota) usado en `cac:Payment/cbc:ID`, validar un solo pago/cobro por detalle de retención y percepción (XSD SUNAT) y leer número/fecha de pago por nodo en los parsers. ## 5.0.0 - 2023-05-01 - #206 Agregar xml para nueva Guia de Remisión. diff --git a/packages/core/src/Core/Model/Retention/Payment.php b/packages/core/src/Core/Model/Retention/Payment.php index 94b31466..88a3631e 100644 --- a/packages/core/src/Core/Model/Retention/Payment.php +++ b/packages/core/src/Core/Model/Retention/Payment.php @@ -17,6 +17,15 @@ */ class Payment { + /** + * Número de pago (cuota) asignado a la factura. + * + * Si no se especifica, se utiliza la posición del pago dentro del detalle. + * + * @var int|null + */ + private $numero; + /** * Moneda de pago (igual a la moneda del documento de referencia). * @@ -38,6 +47,26 @@ class Payment */ private $fecha; + /** + * @return int|null + */ + public function getNumero(): ?int + { + return $this->numero; + } + + /** + * @param int|null $numero + * + * @return Payment + */ + public function setNumero(?int $numero): Payment + { + $this->numero = $numero; + + return $this; + } + /** * @return string */ diff --git a/packages/validator/src/Validator/Loader/PaymentLoader.php b/packages/validator/src/Validator/Loader/PaymentLoader.php index be3ac2e5..3c75bc1a 100644 --- a/packages/validator/src/Validator/Loader/PaymentLoader.php +++ b/packages/validator/src/Validator/Loader/PaymentLoader.php @@ -18,6 +18,7 @@ class PaymentLoader implements LoaderMetadataInterface { public function load(ClassMetadata $metadata) { + $metadata->addPropertyConstraint('numero', new Assert\GreaterThan(0)); $metadata->addPropertyConstraints('moneda', [ new Assert\NotBlank(), new Assert\Length(['min' => 3, 'max' => 3]), diff --git a/packages/validator/src/Validator/Loader/PerceptionDetailLoader.php b/packages/validator/src/Validator/Loader/PerceptionDetailLoader.php index d837f70b..5d53af42 100644 --- a/packages/validator/src/Validator/Loader/PerceptionDetailLoader.php +++ b/packages/validator/src/Validator/Loader/PerceptionDetailLoader.php @@ -33,6 +33,9 @@ public function load(ClassMetadata $metadata) ]); $metadata->addPropertyConstraints('cobros', [ new Assert\NotBlank(), + // XSD SUNAT: cac:Payment maxOccurs=1 por SUNATPerceptionDocumentReference, + // para varios cobros de una factura agregar un detalle por cada cobro. + new Assert\Count(['max' => 1]), new Assert\Valid(), ]); $metadata->addPropertyConstraint('impCobrar', new Assert\NotNull()); diff --git a/packages/validator/src/Validator/Loader/RetentionDetailLoader.php b/packages/validator/src/Validator/Loader/RetentionDetailLoader.php index 0f623d45..dd584321 100644 --- a/packages/validator/src/Validator/Loader/RetentionDetailLoader.php +++ b/packages/validator/src/Validator/Loader/RetentionDetailLoader.php @@ -35,6 +35,9 @@ public function load(ClassMetadata $metadata) ]); $metadata->addPropertyConstraints('pagos', [ new Assert\NotBlank(), + // XSD SUNAT: cac:Payment maxOccurs=1 por SUNATRetentionDocumentReference, + // para varios pagos de una factura agregar un detalle por cada pago. + new Assert\Count(['max' => 1]), new Assert\Valid(), ]); $metadata->addPropertyConstraint('impPagar', new Assert\NotNull()); diff --git a/packages/validator/tests/Validator/CePerceptionValidatorTest.php b/packages/validator/tests/Validator/CePerceptionValidatorTest.php index 2e25d8d1..d9115898 100644 --- a/packages/validator/tests/Validator/CePerceptionValidatorTest.php +++ b/packages/validator/tests/Validator/CePerceptionValidatorTest.php @@ -52,6 +52,32 @@ public function testValidatePerceptionInvalidIndExcepcional() $this->assertEquals('indExcepcional', $errors->get(0)->getPropertyPath()); } + public function testValidatePerceptionMultiplesCobrosPorDetalle() + { + $perception = $this->getPerception(); + $detail = $perception->getDetails()[0]; + $cobro = (new Payment()) + ->setNumero(2) + ->setMoneda('PEN') + ->setFecha(new DateTime()) + ->setImporte(100); + $detail->setCobros(array_merge($detail->getCobros(), [$cobro])); + + $errors = $this->getValidator()->validate($perception); + + $this->assertEquals(1, $errors->count()); + } + + public function testValidatePerceptionNumeroCobroInvalido() + { + $perception = $this->getPerception(); + $perception->getDetails()[0]->getCobros()[0]->setNumero(0); + + $errors = $this->getValidator()->validate($perception); + + $this->assertEquals(1, $errors->count()); + } + /** * @return Perception */ @@ -97,7 +123,8 @@ private function getPerception() private function getExtras() { $pay = new Payment(); - $pay->setMoneda('PEN') + $pay->setNumero(1) + ->setMoneda('PEN') ->setFecha(new DateTime()) ->setImporte(100); diff --git a/packages/validator/tests/Validator/CeRetentionValidatorTest.php b/packages/validator/tests/Validator/CeRetentionValidatorTest.php index e7fdb6ba..b8259053 100644 --- a/packages/validator/tests/Validator/CeRetentionValidatorTest.php +++ b/packages/validator/tests/Validator/CeRetentionValidatorTest.php @@ -30,6 +30,32 @@ public function testValidateRetention() $this->assertEquals(0,$errors->count()); } + public function testValidateRetentionMultiplePagosPorDetalle() + { + $retention = $this->getRetention(); + $detail = $retention->getDetails()[0]; + $pay = (new Payment()) + ->setNumero(2) + ->setMoneda('PEN') + ->setFecha(new \DateTime()) + ->setImporte(100); + $detail->setPagos(array_merge($detail->getPagos(), [$pay])); + + $errors = $this->getValidator()->validate($retention); + + $this->assertEquals(1, $errors->count()); + } + + public function testValidateRetentionNumeroPagoInvalido() + { + $retention = $this->getRetention(); + $retention->getDetails()[0]->getPagos()[0]->setNumero(0); + + $errors = $this->getValidator()->validate($retention); + + $this->assertEquals(1, $errors->count()); + } + /** * @return Retention */ @@ -77,7 +103,8 @@ private function getRetention() private function getExtras() { $pay = new Payment(); - $pay->setMoneda('PEN') + $pay->setNumero(1) + ->setMoneda('PEN') ->setFecha(new \DateTime()) ->setImporte(100); diff --git a/packages/xml-parser/src/Xml/Parser/PerceptionParser.php b/packages/xml-parser/src/Xml/Parser/PerceptionParser.php index 85c40a16..35faa471 100644 --- a/packages/xml-parser/src/Xml/Parser/PerceptionParser.php +++ b/packages/xml-parser/src/Xml/Parser/PerceptionParser.php @@ -172,10 +172,12 @@ private function getPayments(DOMNode $node) $pays = $xml->getNodes('cac:Payment', $node); foreach ($pays as $pay) { $temp = $xml->getNode('cbc:PaidAmount', $pay); + $numero = $xml->getValue('cbc:ID', $pay); $payment = new Payment(); - $payment->setMoneda($temp->getAttribute('currencyID')) + $payment->setNumero($numero === null || $numero === '' ? null : (int)$numero) + ->setMoneda($temp->getAttribute('currencyID')) ->setImporte((float)$temp->nodeValue) - ->setFecha(new DateTime($xml->getValue('cbc:PaidDate'))); + ->setFecha(new DateTime($xml->getValue('cbc:PaidDate', $pay))); yield $payment; } diff --git a/packages/xml-parser/src/Xml/Parser/RetentionParser.php b/packages/xml-parser/src/Xml/Parser/RetentionParser.php index a853830b..42dee4c6 100644 --- a/packages/xml-parser/src/Xml/Parser/RetentionParser.php +++ b/packages/xml-parser/src/Xml/Parser/RetentionParser.php @@ -167,10 +167,12 @@ private function getPayments(DOMNode $node) $pays = $xml->getNodes('cac:Payment', $node); foreach ($pays as $pay) { $temp = $xml->getNode('cbc:PaidAmount', $pay); + $numero = $xml->getValue('cbc:ID', $pay); $payment = new Payment(); - $payment->setMoneda($temp->getAttribute('currencyID')) + $payment->setNumero($numero === null || $numero === '' ? null : (int)$numero) + ->setMoneda($temp->getAttribute('currencyID')) ->setImporte((float)$temp->nodeValue) - ->setFecha(new DateTime($xml->getValue('cbc:PaidDate'))); + ->setFecha(new DateTime($xml->getValue('cbc:PaidDate', $pay))); yield $payment; } diff --git a/packages/xml-parser/tests/Xml/Parser/PerceptionParserTest.php b/packages/xml-parser/tests/Xml/Parser/PerceptionParserTest.php index e9488df9..6ae0c53a 100644 --- a/packages/xml-parser/tests/Xml/Parser/PerceptionParserTest.php +++ b/packages/xml-parser/tests/Xml/Parser/PerceptionParserTest.php @@ -37,6 +37,10 @@ public function testParseDoc($filename) $this->assertNotEmpty($detail->getNumDoc()); $this->assertNotNull($detail->getFechaEmision()); $this->assertGreaterThan(0, count($detail->getCobros())); + foreach ($detail->getCobros() as $cobro) { + $this->assertGreaterThan(0, $cobro->getNumero()); + $this->assertNotNull($cobro->getFecha()); + } $this->assertTrue(is_float($detail->getImpTotal())); $this->assertTrue(is_float($detail->getImpPercibido())); $this->assertTrue(is_float($detail->getImpCobrar())); diff --git a/packages/xml-parser/tests/Xml/Parser/RetentionParserTest.php b/packages/xml-parser/tests/Xml/Parser/RetentionParserTest.php index 0c5d9b39..7aacf8f0 100644 --- a/packages/xml-parser/tests/Xml/Parser/RetentionParserTest.php +++ b/packages/xml-parser/tests/Xml/Parser/RetentionParserTest.php @@ -37,6 +37,10 @@ public function testParseDoc($filename) $this->assertNotEmpty($detail->getNumDoc()); $this->assertNotNull($detail->getFechaEmision()); $this->assertTrue(is_array($detail->getPagos())); + foreach ($detail->getPagos() as $pago) { + $this->assertGreaterThan(0, $pago->getNumero()); + $this->assertNotNull($pago->getFecha()); + } $this->assertTrue(is_float($detail->getImpTotal())); $this->assertTrue(is_float($detail->getImpRetenido())); $this->assertTrue(is_float($detail->getImpPagar())); diff --git a/packages/xml/src/Xml/Templates/perception.xml.twig b/packages/xml/src/Xml/Templates/perception.xml.twig index eab37778..5152e639 100644 --- a/packages/xml/src/Xml/Templates/perception.xml.twig +++ b/packages/xml/src/Xml/Templates/perception.xml.twig @@ -76,7 +76,7 @@ {% if det.cobros %} {% for cob in det.cobros %} - {{ loop.index }} + {{ cob.numero|default(loop.index) }} {{ cob.importe|n_format }} {{ cob.fecha|date('Y-m-d') }} diff --git a/packages/xml/src/Xml/Templates/retention.xml.twig b/packages/xml/src/Xml/Templates/retention.xml.twig index e3a78b34..e6ad7695 100644 --- a/packages/xml/src/Xml/Templates/retention.xml.twig +++ b/packages/xml/src/Xml/Templates/retention.xml.twig @@ -73,7 +73,7 @@ {% if det.pagos %} {% for pay in det.pagos %} - {{ loop.index }} + {{ pay.numero|default(loop.index) }} {{ pay.importe|n_format }} {{ pay.fecha|date('Y-m-d') }}