diff --git a/development/ElementsMaker/specifications/ComercioExterior20.json b/development/ElementsMaker/specifications/ComercioExterior20.json new file mode 100644 index 0000000..791a60e --- /dev/null +++ b/development/ElementsMaker/specifications/ComercioExterior20.json @@ -0,0 +1,34 @@ +{ + "php-namespace": "CfdiUtils\\Elements\\Cce20", + "prefix": "cce20", + "xml-namespace": "http://www.sat.gob.mx/ComercioExterior20", + "xml-schemalocation": "http://www.sat.gob.mx/sitio_internet/cfd/ComercioExterior20/ComercioExterior20.xsd", + "version-attribute": "Version", + "version-value": "2.0", + "root-element": "ComercioExterior", + "structure": { + "Emisor": { + "Domicilio": {} + }, + "Propietario": { + "multiple": true + }, + "Receptor": { + "Domicilio": {} + }, + "Destinatario": { + "multiple": true, + "Domicilio": { + "multiple": true + } + }, + "Mercancias": { + "Mercancia": { + "multiple": true, + "DescripcionesEspecificas": { + "multiple": true + } + } + } + } +} diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 091c296..1af1047 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -32,6 +32,18 @@ - Merge methods from `\CfdiUtils\Nodes\NodeHasValueInterface` into `\CfdiUtils\Nodes\NodeInterface`. - Remove deprecated constant `CfdiUtils\Retenciones\Retenciones::RET_NAMESPACE`. +## Version 2.26.0 2024-01-10 + +Add `CfdiUtils\Elements\Cce30` *Elements* to work with "Complemento de Comercio Exterior 3.0". + +Extract logic to move SAT definitions to root element to a helper in order to use it on other creators: + +- Add `SatNsDefinitionsMover` helper. +- Change `CfdiCreatorTrait::moveSatDefinitionsToComprobante` to use helper. +- Add `RetencionesCreatorTrait::moveSatDefinitionsToRetenciones`. +- Document samples using `$creator->moveSatDefinitionsToRetenciones()`. +- Fix tests sample files with sat definitions on root element. + ## Unreleased 2024-01-08 - Fix continuous integration code style. diff --git a/docs/crear/cfdi-de-retenciones-e-informacion-de-pagos.md b/docs/crear/cfdi-de-retenciones-e-informacion-de-pagos.md index e88a2d9..43d8714 100644 --- a/docs/crear/cfdi-de-retenciones-e-informacion-de-pagos.md +++ b/docs/crear/cfdi-de-retenciones-e-informacion-de-pagos.md @@ -66,6 +66,9 @@ $retenciones->addComplemento($dividendos); $creator->putCertificado(new \CfdiUtils\Certificado\Certificado('archivo.cer')); $creator->addSello('file://archivo.key.pem', 'la contraseña'); +// método de ayuda para mover las declaraciones de espacios de nombre al nodo raíz +$creator->moveSatDefinitionsToRetenciones(); + // Asserts contendrá el resultado de la validación $asserts = $creator->validate(); @@ -132,6 +135,9 @@ $retenciones->addComplemento($dividendos); $creator->putCertificado(new \CfdiUtils\Certificado\Certificado('archivo.cer')); $creator->addSello('file://archivo.key.pem', 'la contraseña'); +// método de ayuda para mover las declaraciones de espacios de nombre al nodo raíz +$creator->moveSatDefinitionsToRetenciones(); + // Asserts contendrá el resultado de la validación $asserts = $creator->validate(); diff --git a/src/CfdiUtils/CfdiCreatorTrait.php b/src/CfdiUtils/CfdiCreatorTrait.php index 1de70db..c84492c 100644 --- a/src/CfdiUtils/CfdiCreatorTrait.php +++ b/src/CfdiUtils/CfdiCreatorTrait.php @@ -8,11 +8,11 @@ use CfdiUtils\Certificado\Certificado; use CfdiUtils\Certificado\CertificadoPropertyTrait; use CfdiUtils\Nodes\NodeInterface; -use CfdiUtils\Nodes\NodeNsDefinitionsMover; use CfdiUtils\Nodes\XmlNodeUtils; use CfdiUtils\PemPrivateKey\PemPrivateKey; use CfdiUtils\SumasConceptos\SumasConceptos; use CfdiUtils\SumasConceptos\SumasConceptosWriter; +use CfdiUtils\Utils\SatNsDefinitionsMover; use CfdiUtils\Validate\Asserts; use CfdiUtils\Validate\Hydrater; use CfdiUtils\Validate\MultiValidator; @@ -75,15 +75,10 @@ public function asXml(): string return XmlNodeUtils::nodeToXmlString($this->comprobante, true); } - public function moveSatDefinitionsToComprobante() + public function moveSatDefinitionsToComprobante(): void { - $nodeNsDefinitionsMover = new NodeNsDefinitionsMover(); - $nodeNsDefinitionsMover->setNamespaceFilter( - function (string $namespaceUri): bool { - return ('http://www.sat.gob.mx/' === (substr($namespaceUri, 0, 22) ?: '')); - } - ); - $nodeNsDefinitionsMover->process($this->comprobante); + $mover = new SatNsDefinitionsMover(); + $mover->move($this->comprobante); } public function saveXml(string $filename): bool diff --git a/src/CfdiUtils/Elements/Cce20/ComercioExterior.php b/src/CfdiUtils/Elements/Cce20/ComercioExterior.php new file mode 100644 index 0000000..405723c --- /dev/null +++ b/src/CfdiUtils/Elements/Cce20/ComercioExterior.php @@ -0,0 +1,100 @@ + 'http://www.sat.gob.mx/ComercioExterior20', + 'xsi:schemaLocation' => 'http://www.sat.gob.mx/ComercioExterior20' + . ' http://www.sat.gob.mx/sitio_internet/cfd/ComercioExterior20/ComercioExterior20.xsd', + 'Version' => '2.0', + ]; + } + + public function getEmisor(): Emisor + { + return $this->helperGetOrAdd(new Emisor()); + } + + public function addEmisor(array $attributes = []): Emisor + { + $subject = $this->getEmisor(); + $subject->addAttributes($attributes); + return $subject; + } + + public function addPropietario(array $attributes = []): Propietario + { + $subject = new Propietario($attributes); + $this->addChild($subject); + return $subject; + } + + public function multiPropietario(array ...$elementAttributes): self + { + foreach ($elementAttributes as $attributes) { + $this->addPropietario($attributes); + } + return $this; + } + + public function getReceptor(): Receptor + { + return $this->helperGetOrAdd(new Receptor()); + } + + public function addReceptor(array $attributes = []): Receptor + { + $subject = $this->getReceptor(); + $subject->addAttributes($attributes); + return $subject; + } + + public function addDestinatario(array $attributes = []): Destinatario + { + $subject = new Destinatario($attributes); + $this->addChild($subject); + return $subject; + } + + public function multiDestinatario(array ...$elementAttributes): self + { + foreach ($elementAttributes as $attributes) { + $this->addDestinatario($attributes); + } + return $this; + } + + public function getMercancias(): Mercancias + { + return $this->helperGetOrAdd(new Mercancias()); + } + + public function addMercancias(array $attributes = []): Mercancias + { + $subject = $this->getMercancias(); + $subject->addAttributes($attributes); + return $subject; + } +} diff --git a/src/CfdiUtils/Elements/Cce20/DescripcionesEspecificas.php b/src/CfdiUtils/Elements/Cce20/DescripcionesEspecificas.php new file mode 100644 index 0000000..1c4a0c9 --- /dev/null +++ b/src/CfdiUtils/Elements/Cce20/DescripcionesEspecificas.php @@ -0,0 +1,13 @@ +addChild($subject); + return $subject; + } + + public function multiDomicilio(array ...$elementAttributes): self + { + foreach ($elementAttributes as $attributes) { + $this->addDomicilio($attributes); + } + return $this; + } +} diff --git a/src/CfdiUtils/Elements/Cce20/Domicilio.php b/src/CfdiUtils/Elements/Cce20/Domicilio.php new file mode 100644 index 0000000..19d2ab5 --- /dev/null +++ b/src/CfdiUtils/Elements/Cce20/Domicilio.php @@ -0,0 +1,13 @@ +helperGetOrAdd(new Domicilio()); + } + + public function addDomicilio(array $attributes = []): Domicilio + { + $subject = $this->getDomicilio(); + $subject->addAttributes($attributes); + return $subject; + } +} diff --git a/src/CfdiUtils/Elements/Cce20/Mercancia.php b/src/CfdiUtils/Elements/Cce20/Mercancia.php new file mode 100644 index 0000000..59ca9fd --- /dev/null +++ b/src/CfdiUtils/Elements/Cce20/Mercancia.php @@ -0,0 +1,28 @@ +addChild($subject); + return $subject; + } + + public function multiDescripcionesEspecificas(array ...$elementAttributes): self + { + foreach ($elementAttributes as $attributes) { + $this->addDescripcionesEspecificas($attributes); + } + return $this; + } +} diff --git a/src/CfdiUtils/Elements/Cce20/Mercancias.php b/src/CfdiUtils/Elements/Cce20/Mercancias.php new file mode 100644 index 0000000..d0ff9d7 --- /dev/null +++ b/src/CfdiUtils/Elements/Cce20/Mercancias.php @@ -0,0 +1,28 @@ +addChild($subject); + return $subject; + } + + public function multiMercancia(array ...$elementAttributes): self + { + foreach ($elementAttributes as $attributes) { + $this->addMercancia($attributes); + } + return $this; + } +} diff --git a/src/CfdiUtils/Elements/Cce20/Propietario.php b/src/CfdiUtils/Elements/Cce20/Propietario.php new file mode 100644 index 0000000..3087876 --- /dev/null +++ b/src/CfdiUtils/Elements/Cce20/Propietario.php @@ -0,0 +1,13 @@ +helperGetOrAdd(new Domicilio()); + } + + public function addDomicilio(array $attributes = []): Domicilio + { + $subject = $this->getDomicilio(); + $subject->addAttributes($attributes); + return $subject; + } +} diff --git a/src/CfdiUtils/Retenciones/RetencionesCreatorTrait.php b/src/CfdiUtils/Retenciones/RetencionesCreatorTrait.php index 590a7aa..0c74aa5 100644 --- a/src/CfdiUtils/Retenciones/RetencionesCreatorTrait.php +++ b/src/CfdiUtils/Retenciones/RetencionesCreatorTrait.php @@ -10,6 +10,7 @@ use CfdiUtils\Elements\Common\AbstractElement; use CfdiUtils\Nodes\XmlNodeUtils; use CfdiUtils\PemPrivateKey\PemPrivateKey; +use CfdiUtils\Utils\SatNsDefinitionsMover; use CfdiUtils\Validate\Asserts; use CfdiUtils\Validate\Xml\XmlFollowSchema; use CfdiUtils\XmlResolver\XmlResolver; @@ -74,4 +75,10 @@ public function asXml(): string { return XmlNodeUtils::nodeToXmlString($this->retenciones, true); } + + public function moveSatDefinitionsToRetenciones(): void + { + $mover = new SatNsDefinitionsMover(); + $mover->move($this->retenciones); + } } diff --git a/src/CfdiUtils/Utils/SatNsDefinitionsMover.php b/src/CfdiUtils/Utils/SatNsDefinitionsMover.php new file mode 100644 index 0000000..2b4676a --- /dev/null +++ b/src/CfdiUtils/Utils/SatNsDefinitionsMover.php @@ -0,0 +1,20 @@ +setNamespaceFilter( + function (string $namespaceUri): bool { + return ('http://www.sat.gob.mx/' === (substr($namespaceUri, 0, 22) ?: '')); + } + ); + $nodeNsDefinitionsMover->process($root); + } +} diff --git a/tests/CfdiUtilsTests/Elements/Cce20/ComercioExteriorTest.php b/tests/CfdiUtilsTests/Elements/Cce20/ComercioExteriorTest.php new file mode 100644 index 0000000..4c79a39 --- /dev/null +++ b/tests/CfdiUtilsTests/Elements/Cce20/ComercioExteriorTest.php @@ -0,0 +1,94 @@ +assertElementHasName($element, 'cce20:ComercioExterior'); + $this->assertElementHasOrder($element, [ + 'cce20:Emisor', + 'cce20:Propietario', + 'cce20:Receptor', + 'cce20:Destinatario', + 'cce20:Mercancias', + ]); + $this->assertElementHasFixedAttributes($element, [ + 'xmlns:cce20' => 'http://www.sat.gob.mx/ComercioExterior20', + 'xsi:schemaLocation' => 'http://www.sat.gob.mx/ComercioExterior20' + . ' http://www.sat.gob.mx/sitio_internet/cfd/ComercioExterior20/ComercioExterior20.xsd', + 'Version' => '2.0', + ]); + $this->assertElementHasChildSingle($element, Emisor::class); + $this->assertElementHasChildMultiple($element, Propietario::class); + $this->assertElementHasChildSingle($element, Receptor::class); + $this->assertElementHasChildMultiple($element, Destinatario::class); + $this->assertElementHasChildSingle($element, Mercancias::class); + } + + public function testEmisor(): void + { + $element = new Emisor(); + $this->assertElementHasName($element, 'cce20:Emisor'); + $this->assertElementHasChildSingle($element, Domicilio::class); + } + + public function testDomicilio(): void + { + $element = new Domicilio(); + $this->assertElementHasName($element, 'cce20:Domicilio'); + } + + public function testPropietario(): void + { + $element = new Propietario(); + $this->assertElementHasName($element, 'cce20:Propietario'); + } + + public function testReceptor(): void + { + $element = new Receptor(); + $this->assertElementHasName($element, 'cce20:Receptor'); + $this->assertElementHasChildSingle($element, Domicilio::class); + } + + public function testDestinatario(): void + { + $element = new Destinatario(); + $this->assertElementHasName($element, 'cce20:Destinatario'); + $this->assertElementHasChildMultiple($element, Domicilio::class); + } + + public function testMercancias(): void + { + $element = new Mercancias(); + $this->assertElementHasName($element, 'cce20:Mercancias'); + $this->assertElementHasChildMultiple($element, Mercancia::class); + } + + public function testMercancia(): void + { + $element = new Mercancia(); + $this->assertElementHasName($element, 'cce20:Mercancia'); + $this->assertElementHasChildMultiple($element, DescripcionesEspecificas::class); + } + + public function testDescripcionesEspecificas(): void + { + $element = new DescripcionesEspecificas(); + $this->assertElementHasName($element, 'cce20:DescripcionesEspecificas'); + } +} diff --git a/tests/CfdiUtilsTests/Retenciones/RetencionesCreator10Test.php b/tests/CfdiUtilsTests/Retenciones/RetencionesCreator10Test.php index 30ec71c..28f01ea 100644 --- a/tests/CfdiUtilsTests/Retenciones/RetencionesCreator10Test.php +++ b/tests/CfdiUtilsTests/Retenciones/RetencionesCreator10Test.php @@ -72,6 +72,9 @@ public function testCreatePreCfdiWithAllCorrectValues() $creator->putCertificado($certificado); $creator->addSello('file://' . $pemFile, $passPhrase); + // move sat definitions + $creator->moveSatDefinitionsToRetenciones(); + // validate $asserts = $creator->validate(); $this->assertGreaterThanOrEqual(1, $asserts->count()); diff --git a/tests/CfdiUtilsTests/Retenciones/RetencionesCreator20Test.php b/tests/CfdiUtilsTests/Retenciones/RetencionesCreator20Test.php index 9da3f30..2f4b122 100644 --- a/tests/CfdiUtilsTests/Retenciones/RetencionesCreator20Test.php +++ b/tests/CfdiUtilsTests/Retenciones/RetencionesCreator20Test.php @@ -82,6 +82,9 @@ public function testCreatePreCfdiWithAllCorrectValues() $creator->putCertificado($certificado); $creator->addSello('file://' . $pemFile, $passPhrase); + // move sat definitions + $creator->moveSatDefinitionsToRetenciones(); + // validate $asserts = $creator->validate(); $this->assertGreaterThanOrEqual(1, $asserts->count()); diff --git a/tests/assets/retenciones/retenciones10.xml b/tests/assets/retenciones/retenciones10.xml index 0e53fe9..f907bf9 100644 --- a/tests/assets/retenciones/retenciones10.xml +++ b/tests/assets/retenciones/retenciones10.xml @@ -1,5 +1,5 @@ - + @@ -9,7 +9,7 @@ - + diff --git a/tests/assets/retenciones/retenciones20.xml b/tests/assets/retenciones/retenciones20.xml index 13328f5..13daaf4 100644 --- a/tests/assets/retenciones/retenciones20.xml +++ b/tests/assets/retenciones/retenciones20.xml @@ -1,5 +1,5 @@ - + @@ -10,7 +10,7 @@ - +