diff --git a/.gitattributes b/.gitattributes index b7335593..d8a38b83 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,11 +4,11 @@ # Do not put this files on a distribution package (by .gitignore) /vendor export-ignore /build export-ignore -/wiki export-ignore /composer.lock export-ignore +/node_modules export-ignore +/package-lock.json export-ignore # Do not put this files on a distribution package -/docs/ export-ignore /tests/ export-ignore /.gitattributes export-ignore /.gitignore export-ignore @@ -18,3 +18,6 @@ /.travis.yml export-ignore /phpcs.xml.dist export-ignore /phpunit.xml.dist export-ignore +/mkdocs.yml export-ignore +/.markdownlint.json export-ignore + diff --git a/.gitignore b/.gitignore index b7b30019..6ac272bb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ # do not include this files on git /vendor /build -/wiki /composer.lock +/node_modules +/package-lock.json + diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 00000000..fec68810 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,11 @@ +{ + "line-length": false, + "no-inline-html": true, + "blank_lines": { + "maximum": 2 + }, + "no-trailing-punctuation": { + "punctuation": ".,;:" + } +} + diff --git a/.travis.yml b/.travis.yml index 8826f117..7f383ca5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,9 @@ php: - 7.1 - 7.2 +before_install: + - nvm install 8 + # This triggers builds to run on the new TravisCI infrastructure. # See: http://docs.travis-ci.com/user/workers/container-based-infrastructure/ sudo: false @@ -20,6 +23,8 @@ env: before_script: - travis_retry composer install --no-interaction --prefer-dist + - travis_retry npm install + - travis_retry pip install --user mkdocs - phpenv config-rm xdebug.ini script: @@ -34,6 +39,8 @@ script: vendor/bin/phpunit fi - vendor/bin/phpstan.phar --no-progress analyse --level max src/ tests/ + - node node_modules/markdownlint-cli/markdownlint.js *.md docs/ + - ~/.local/bin/mkdocs build --strict --site-dir build/docs after_script: # upload test covegare to scrutinizer diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 2e19ed88..2992f411 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -22,8 +22,8 @@ include: Examples of unacceptable behavior by participants include: -* The use of sexualized language or imagery and unwelcome sexual attention or -advances +* The use of sexualized language or imagery and unwelcome sexual attention + or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic diff --git a/README.md b/README.md index 0b82ed1f..1a536d9d 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ and is written in **spanish language** since is the language of the intented aud **Atención: este proyecto se migrará a `phpcfdi/cfdiutils`, aun no hay fecha planeada** -## Main features: +## Main features - Create CFDI version 3.3 based on a friendly extendable non-xml objects (`nodes`) - Read CFDI version 3.2 and 3.3 @@ -30,18 +30,19 @@ and is written in **spanish language** since is the language of the intented aud if not then the document was modified after signature. - Validate the "Complemento de recepción de pagos" - Helper objects to deal with: - - `Cadena de origen` generation - - Extract information from CER files or `Certificado` attribute - - Calculate `Comprobante` sums based on the list of `Conceptos` - - Retrieve the CFDI version information + - `Cadena de origen` generation + - Extract information from CER files or `Certificado` attribute + - Calculate `Comprobante` sums based on the list of `Conceptos` + - Retrieve the CFDI version information - Keep a local copy of the tree of XSD and XSLT file dependences from SAT - Keep a local copy of certificates to avoid download them each time -- Check the SAT WebService to get the status of a CDI ('Activo', 'Cancelado' & 'No encontrado') +- Check the SAT WebService to get the status of a CDI ('Activo', 'Cancelado' & 'No encontrado') ## Installation Use [composer](https://getcomposer.org/), so please run + ```shell composer require eclipxe/cfdiutils ``` @@ -49,17 +50,12 @@ composer require eclipxe/cfdiutils ## Major versions -- Version 1.x **deprecated** was deprecated time ago, that version didn't do much anyway -- Version 2.x **current** has a lot of features and helper objects -- Version 3.x **future** will be released with the following backward compatibility breaks: - - Rename `\CfdiUtils\CadenaOrigen\CadenaOrigenBuilder` to `\CfdiUtils\CadenaOrigen\DOMBuilder` - - Rename `\CfdiUtils\CadenaOrigen\DefaultLocations` to `\CfdiUtils\CadenaOrigen\CfdiDefaultLocations` - - Remove `\CfdiUtils\CadenaOrigen\CadenaOrigenLocations` - - Remove `\CfdiUtils\PemPrivateKey\PemPrivateKey::isOpened` to `\CfdiUtils\PemPrivateKey\PemPrivateKey::isOpen` - - Remove `static` methods from `\CfdiUtils\CfdiVersion`, create an instance of the class - - Remove `static` methods from `\CfdiUtils\TimbreFiscalDigital\TfdVersion`, create an instance of the class - -It could be possible that version 3 will be migrated to a different project under the group [PhpCfdi] +- Version 1.x **deprecated** was deprecated time ago, that version didn't do much anyway. +- Version 2.x **current** has a lot of features and helper objects. +- Version 3.x **future** will be released with backward compatibility breaks. + - See [docs/CHANGELOG.md](docs/CHANGELOG.md) for backward compatibility breaks. + - It may change to PHP 7.1 + - It could be possible to migrate to phpcfdi/cfiutils under [phpCfdi](https://github.com/phpCfdi) organization ## PHP Support diff --git a/composer.json b/composer.json index 1ffdfc7f..eaf126de 100644 --- a/composer.json +++ b/composer.json @@ -57,7 +57,8 @@ "scripts": { "build": [ "@fix-style", - "@qa" + "@qa", + "@docs" ], "check-style": [ "vendor/bin/php-cs-fixer fix --dry-run --verbose", @@ -71,6 +72,10 @@ "vendor/bin/phpstan.phar analyse --no-progress --level max src/ tests/", "@test" ], + "docs": [ + "node_modules/markdownlint-cli/markdownlint.js *.md docs/", + "mkdocs build --strict --site-dir build/docs" + ], "test": [ "vendor/bin/phplint", "vendor/bin/phpunit" diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 47c63feb..501027ba 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,23 +1,51 @@ -# Backward compatibility breaks (not released yet), plan for version 3.0 +# CfdiUtils Changelog file + +## Backward compatibility breaks (not released yet), plan for version 3.0 + - Remove deprecated classes: - - `\CfdiUtils\CadenaOrigen\CadenaOrigenBuilder` - - `\CfdiUtils\CadenaOrigen\DefaultLocations` - - `\CfdiUtils\CadenaOrigen\CadenaOrigenLocations` + - `\CfdiUtils\CadenaOrigen\CadenaOrigenBuilder` + - `\CfdiUtils\CadenaOrigen\DefaultLocations` + - `\CfdiUtils\CadenaOrigen\CadenaOrigenLocations` - Remove `\CfdiUtils\PemPrivateKey\PemPrivateKey::isOpened` to `\CfdiUtils\PemPrivateKey\PemPrivateKey::isOpen` -- Remove `static` methods from `\CfdiUtils\CfdiVersion`, instead create an instance of the class -- Remove `static` methods from `\CfdiUtils\TimbreFiscalDigital\TfdVersion`, instead create an instance of the class - +- Remove `static` methods from `\CfdiUtils\CfdiVersion`, instead create an instance of the class +- Remove `static` methods from `\CfdiUtils\TimbreFiscalDigital\TfdVersion`, instead create an instance of the class +- Remove `trigger_error` on `\CfdiUtils\Elements\Cfdi33\Comprobante::getCfdiRelacionados` when called with arguments. + + +## Version 2.6.0 2018-07-06 - bugfixes, quickreader & welcome readthedocs & mkdocs + +- Create `QuickReader`, utility for easy navigate and extract information from a CFDI +- Fix `Rfc` to don't throw an exception if checksum fails, SAT is not following its own standard +- Add `Rfc::checkSum()` and `Rfc::checkSumMatch()` to know if the Rfc is following the checksum +- Fix tests that expect Rfc checksum failure +- Fix tests comments on `testDescuentoNotSetIfAllConceptosDoesNotHaveDescuento` +- Fix `CfdiUtils\Elements\Cfdi33\Comprobante::getCfdiRelacionados` to don't receive a parameter. + - For backwards compatibility when it receive a parameter do the same thing but trigger a E_USER_NOTICE + - Create an special test case `ComprobanteGetCfdiRelacionadosTest` that catched the E_USER_NOTICE error +- Add `CfdiUtils\Elements\Cfdi33\Comprobante::addCfdiRelacionados(array $attributes)` +- Add `CfdiUtils\Elements\Cfdi33\Comprobante::multiCfdiRelacionado(array $attributes)` +- Add tests to assert that `Comprobante/Impuestos/(Traslados/Traslado|Retenciones/Retencion)@Impuesto` is rounded +- Minor fix at docblocks for packed arguments +- Change all documentation to move from GitHub Wiki to ReadTheDocs + - More documentation pages & a lot of fixes + - Add `.markdownlint.json` to run with `markdownlint-cli` (`node`), add to travis build process + - Add `mkdocs.yml` to run with `mkdocs` (`python`), add to travis build process + - Fix markdown files according to markdownlint + - Add `composer docs` and append to general `composer build` + + +## Version 2.5.1 2018-06-26 -# Version 2.5.1 2018-06-26 - Fix edge case for validations `SELLO03` and `SELLO04` on `SelloDigitalCertificado`. In some cases, the authority does not require a certificate from emisor and uses one certificate for its own. Therefore, the `Rfc` and `Nombre` of the `Emisor` does not match with the certificate. This produces a false error. To avoid this issue, the validation of `Rfc` and `Nombre` matching with the certificate data must not perform when: - * The `cfdi:Comprobante@NoCertificado` is the same as the `tfd:TimbreFiscalDigital@NoCertificadoSAT` - * The "complemento" `registrofiscal:CFDIRegistroFiscal` exists + - The `cfdi:Comprobante@NoCertificado` is the same as the `tfd:TimbreFiscalDigital@NoCertificadoSAT` + - The "complemento" `registrofiscal:CFDIRegistroFiscal` exists -# Version 2.5.0 2018-05-24 +## Version 2.5.0 2018-05-24 + - Add validations for `http://www.sat.gob.mx/Pagos` at namespace `\CfdiUtils\Validate\Cfdi33\RecepcionPagos` This is a big change that includes more than 50 validators that work in cascade. It implements almost all of the validations from the SAT "Matriz de errores". @@ -25,17 +53,18 @@ - Remove non existent validators discovery `Cfdi33/Timbre` - Move logic of version discovery to a new class, change `CfdiVersion` and `TfdVersion` to implement this logic - Deprecate `static` methods from `\CfdiUtils\CfdiVersion`, instead create an instance of the class -- Deprecate `static` methods from `\CfdiUtils\TimbreFiscalDigital\TfdVersion`, instead create an instance of the class +- Deprecate `static` methods from `\CfdiUtils\TimbreFiscalDigital\TfdVersion`, instead create an instance of the class - Fix deprecation notices existent docblocks - Update deprecation notice to README - Replace TODO with a more explained version -# Version 2.4.6 2018-05-24 +## Version 2.4.6 2018-05-24 + - Fix validation of TIPOCOMP06, it was not checking correctly. - Fix bug in validators that does not respect when the resolver does not have local path: - - `CfdiUtils\Validate\Cfdi33\Standard\TimbreFiscalDigitalSello` - - `CfdiUtils\Validate\Cfdi33\Xml\XmlFollowSchema` + - `CfdiUtils\Validate\Cfdi33\Standard\TimbreFiscalDigitalSello` + - `CfdiUtils\Validate\Cfdi33\Xml\XmlFollowSchema` - Fix bug when removing a `schemaLocation` attribute in `CfdiUtils\Cleaner\Cleaner` - Refactor `CfdiUtils\ConsultaCfdiSat\WebService::request` and move the SOAP call to a protected method, this allow better testing of the class by mocking the call @@ -49,14 +78,15 @@ - Remove `CfdiUtils\Elements\Pagos10\Pago::multiImpuestos`, it should never exists and must not have any use case. - Improve testing on: - - `CfdiUtils\Elements\Pagos10\Pagos` - - `CfdiUtils\Validate\Cfdi33\Standard\ConceptoImpuestos` + - `CfdiUtils\Elements\Pagos10\Pagos` + - `CfdiUtils\Validate\Cfdi33\Standard\ConceptoImpuestos` - Improve docblocks and fix typos in several files - Add new parameter to development script `tests/validate.php`: `--no-cache` that tell resolver to not use local cache. - Improve travis disabling xdebug always and only use it in phpunit code coverage -# Version 2.4.5 2018-05-12 +## Version 2.4.5 2018-05-12 + - Fix: change xml namespace prefix `pagos10` to `pago10` - Refactor `CfdiUtils\Certificado\SerialNumber::baseConvert` - Add `CfdiUtils\Certificado\SerialNumber::asDecimal()` @@ -68,19 +98,21 @@ - Add util `\CfdiUtils\Utils\Rfc`, help to work with strict RFC validations - Add `\CfdiUtils\Validate\Cfdi33\Standard\ReceptorRfc` to validate the RFC of the CFDI receiver - Add `\CfdiUtils\Validate\Cfdi33\Standard\EmisorRfc` to validate the RFC of the CFDI emitter - - Fix `CfdiUtilsTests\CfdiValidator33Test::testValidateWithCorrectData` since used RFC is not valid - - Fix `CfdiUtilsTests\CreateComprobanteCaseTest::testCreateCfdiUsingComprobanteElement` since used RFC is not valid + - Fix `CfdiUtilsTests\CfdiValidator33Test::testValidateWithCorrectData` since used RFC is not valid + - Fix `CfdiUtilsTests\CreateComprobanteCaseTest::testCreateCfdiUsingComprobanteElement` since used RFC is not valid - Add docblocks to `CfdiUtils\Cfdi` - Building: - - Add .phplint.yml to export-ignore (standard line) - - Travis-CI: Declare `FULL_BUILD_PHP_VERSION` for easy understanding + - Add .phplint.yml to export-ignore (standard line) + - Travis-CI: Declare `FULL_BUILD_PHP_VERSION` for easy understanding - Add more dependences: `ext-dom`, `ext-xsl`, `ext-simplexml`, `ext-mbstring` -# Version 2.4.4 2018-05-11 +## Version 2.4.4 2018-05-11 + - FIX: Unable to load a PEM file using filename on windows (Closes #33) - Do not use bcmath function to convert from decimal to hexadecimal the serial number of a certificate -# Version 2.4.3 2018-04-26 +## Version 2.4.3 2018-04-26 + - FIX: The attribute `cfdi:Comprobante@Descuento` must not be deleted if any attribute `cfdi:Comprobante/cfdi:Conceptos/cfdi:Concepto@Descuento` exists. (Closes: #50) - FIX: When validating a CFDI, the validator `CfdiUtils\Validate\Cfdi33\Standard\SelloDigitalCertificado` @@ -88,7 +120,8 @@ - Add a **development** script `tests/validate.php` to validate existing files. WARNING: This can change at any time! Do not depend on this file or its results! -# Version 2.4.2 2018-04-23 +## Version 2.4.2 2018-04-23 + - Fix `\CfdiUtils\Nodes\XmlNodeExporter::export`, it was not appending root element to xml document. - Allow `\CfdiUtils\Nodes\XmlNodeUtils::nodeToXmlString` to export including xml header ``. Default behavior is to not include xml header, it remains unchanged. @@ -96,16 +129,17 @@ - By default, `\DOMDocument` objects are created with version 1.0 and encoding UTF-8. - Add tests to validate previous changes. -# Version 2.4.1 2018-04-11 +## Version 2.4.1 2018-04-11 + - Fix `\CfdiUtils\Certificado\Certificado` when reading serial number. - Use `serialNumberHex` if available, if not then use `serialNumber` and convert to hex. - Move serial number string conversion to class `\CfdiUtils\Certificado\SerialNumber`. This class is not for public use but for use inside `Certificate`. -# Version 2.4.0 2018-02-08 +## Version 2.4.0 2018-02-08 + - Add the feature to order the children nodes for a `CfdiUtils\Nodes\Nodes` object. - This feature is used in the namespace `CfdiUtils\Elements` to set the correct order of the - children nodes without worry about the creation order. + This feature is used in the namespace `CfdiUtils\Elements` to set the correct order of the children nodes without worry about the creation order. - Add `CfdiUtils\Elements\Addenda` helper class. - Add `CfdiUtils\Elements\Pagos10` namespace for "complemento de pagos 1.0". - Add `CfdiUtils\Cleaner\Cleaner` utility class that allows to remove `cfdi:Addenda`, @@ -113,41 +147,46 @@ - Build: The project no longer depends on `jakub-onderka/php-parallel-lint`, now uses `overtrue/phplint` that does the same task but stores a cache. -# Version 2.3.2 2018-01-29 +## Version 2.3.2 2018-01-29 + - Fix how total is formatted in the expression of `\CfdiUtils\ConsultaCfdiSat\RequestParameters` - - Version 3.2 was removing zero trailing decimals instead of using 6 fixed chars - - Version 3.3 was not using 1 leading zero (for integers) and 1 trailing zero (for decimals) + - Version 3.2 was removing zero trailing decimals instead of using 6 fixed chars + - Version 3.3 was not using 1 leading zero (for integers) and 1 trailing zero (for decimals) - On method `\CfdiUtils\Certificado\NodeCertificado::obtain()` change logic and throw exception if temporary file cannot be created -# Version 2.3.1 2018-01-25 +## Version 2.3.1 2018-01-25 + - Add elements helpers `CfdiUtils\Elements\Tfd11\TimbreFiscalDigital` to work with "TimbreFiscalDigital" -# Version 2.3.0 2018-01-25 + +## Version 2.3.0 2018-01-25 + - Add a client `\CfdiUtils\ConsultaCfdiSat\WebService` for the SAT WebService - https://consultaqr.facturaelectronica.sat.gob.mx/ConsultaCFDIService.svc?singleWsdl + `https://consultaqr.facturaelectronica.sat.gob.mx/ConsultaCFDIService.svc?singleWsdl` - Fix bug, must use `children()` method instead of `children` property. Did not appears before because the variable using the property was always a `Node` but other implementation of `NodeInterface` would cause this to break. - Add a lot of fixes in docblocks to move `@param $var` to `@param type $var`. - Add extensions requirements to composer.json: libxml, openssl & soap. - Upgrade `phpstan/phpstan-shim` to version 0.9.1, the not-simple-to-see bug fixed - in this version was found by `phpstan` - https://github.com/phpstan/phpstan + in this version was found by `phpstan` - -# Version 2.2.0 2018-01-24 +## Version 2.2.0 2018-01-24 + - Refactor namespace `\CfdiUtils\CadenaOrigen` (backwards compatible): - - Instead of one only xslt builder now it includes: - - `DOMBuilder`: Uses the regular PHP based method - - `GenkgoXslBuilder`: Uses the library genkgo/xsl xslt version 2 library - - `SaxonbCliBuilder`: Uses the command line saxonb-xslt command - - Build process implementations must return `XsltBuildException` (before they return `RuntimeException`) - - All builders must implement `XsltBuilderInterface` - - Add `XsltBuilderPropertyInterface` and `XsltBuilderPropertyTrait`. - It does not have `hasXsltBuilderProperty`method. - - `DefaultLocations` has been deprecated in favor of `CfdiDefaultLocations` - - `CadenaOrigenBuilder` has been deprecated in favor of `DOMBuilder` - - `CadenaOrigenLocations` has been deprecated, will not be replaced + - Instead of one only xslt builder now it includes: + - `DOMBuilder`: Uses the regular PHP based method + - `GenkgoXslBuilder`: Uses the library genkgo/xsl xslt version 2 library + - `SaxonbCliBuilder`: Uses the command line saxonb-xslt command + - Build process implementations must return `XsltBuildException` (before they return `RuntimeException`) + - All builders must implement `XsltBuilderInterface` + - Add `XsltBuilderPropertyInterface` and `XsltBuilderPropertyTrait`. + It does not have `hasXsltBuilderProperty`method. + - `DefaultLocations` has been deprecated in favor of `CfdiDefaultLocations` + - `CadenaOrigenBuilder` has been deprecated in favor of `DOMBuilder` + - `CadenaOrigenLocations` has been deprecated, will not be replaced - Implement `XsltBuilderPropertyInterface` and `XsltBuilderPropertyTrait` in objects that use to create `CadenaOrigenBuilder` objects. - For `CfdiCreator33` and `CfdiValidator33` will create a default DOMBuilder object if none set. @@ -156,7 +195,8 @@ - Improve the tests. -# Version 2.1.0 2018-01-17 +## Version 2.1.0 2018-01-17 + - Fix `SumasConceptos` to work also with "ImpuestosLocales" - Add elements helpers `CfdiUtils\Elements\ImpLocal10\ImpuestosLocales` to work with "ImpuestosLocales" - Add `CfdiUtils\Certificado\CerRetriever` that works with `CfdiUtils\XmlResolver\XmlResolver` to download @@ -167,12 +207,14 @@ - Update test with `cfdi33-valid.xml` to allow fail `TimbreFiscalDigitalSello` - Travis: Remove xdebug for all but PHP 7.0 -# Version 2.0.1 2018-01-03 +## Version 2.0.1 2018-01-03 + - Small bugfixes thanks to scrutinizer-ci.com - Fix some docblocks - Travis: Build also with PHP 7.2 -# Version 2.0.0 2018-01-01 +## Version 2.0.0 2018-01-01 + - This library has been changed deeply. - It can write CFDI version 3.3 using `CfdiUtils\Elements\Cfdi33` and helper class `CfdiUtils\CfdiCreator33` - It can read CFDI files version 3.2 and 3.3 using `CfdiUtils\Cfdi` @@ -183,12 +225,14 @@ - Include wiki for documentation -# Version 1.0.3 2017-10-09 +## Version 1.0.3 2017-10-09 + - Fix a bug to read the RFC when a certificate does not contain the pattern RFC / CURP but only RFC in the subject x500UniqueIdentifier field -# Version 1.0.2 2017-09-28 - Thanks phpstan! +## Version 1.0.2 2017-09-28 - Thanks phpstan! + - After using `phpstan/phpstan` change the execution plan on `CadenaOrigenLocations`. The function previous function `throwLibXmlErrorOrMessage(string $message)` always throw an exception but it was not clear in the flow of `build` method. @@ -198,9 +242,11 @@ - Check with `isset` that `LibXMLError::$message` exists, phpstan was failing for this. -# Version 1.0.1 2017-09-27 +## Version 1.0.1 2017-09-27 + - Remove Travis CI PHP nightly builds, it fail with require-dev dependencies. -# Version 1.0.0 2017-09-27 +## Version 1.0.0 2017-09-27 + - Initial release diff --git a/docs/TODO.md b/docs/TODO.md index 4dfc675c..a9396bcb 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -1,38 +1,49 @@ -# eclipxe/CfdiUtils To Do List +# Lista de tareas pendientes e ideas -### Prepare for version 3 +## Documentación del proyecto -Version 3 will deprecate some classes and methods, it may be good point of start to migrate the project -to a new namespace `PhpCfdi\CfdiUtils` that is managed by +Documentar los otros helpers de `Elements`: + +- Complemento de comercio exterior +- Impuestos locales +- Pagos + +Documentar los validadores: -#### Deprecations: +- Revisar todos los validadores documentados en CFDI +- Pagos -### CfdiVersion & TfdVersion +## Prepare for version 3 + +Version 3 will deprecate some classes and methods, it may be good point of start to migrate the project +to a new namespace `PhpCfdi\CfdiUtils` + + +## CfdiVersion & TfdVersion The classes `CfdiUtils\CfdiVersion` and `CfdiUtils\TimbreFiscalDigital\CfdiVersion` share the same logic and methos. They are detected as code smell and it would be better to have a single class to implement the logic and extend that class to provide configuration. -### Status of a Cfdi using the SAT webservice +## Status of a Cfdi using the SAT webservice This is already implemented in `CfdiUtils\ConsultaCfdiSat\WebService` but there are two ideas than need a solution: -* Find a way to not depend on PHP SOAP but in something that can do async +- Find a way to not depend on PHP SOAP but in something that can do async request and configure the connection like setting a proxy, maybe depending on guzzle. +- Create a cache of the WSDL page (?) -* Create a cache of the WSDL page (?) - -### Validation rules for Pagos +## Validation rules for Pagos The validation rules for "Complemento de Recepción de pagos" are included since version 2.6 but they require more cases of use and a better understanding of the rules published by SAT. -### Validation rules for ComercioExterior +## Validation rules for ComercioExterior Create validation rules for "Complemento de Comercio Exterior" @@ -43,7 +54,7 @@ Create validation rules for "Complemento de Comercio Exterior" This will be implemented on a different project, for testing proposes there is the file `tests/validate.php` - + ### Implement catalogs published by SAT This will be implemented on a different project. diff --git a/docs/componentes/cadena-de-origen.md b/docs/componentes/cadena-de-origen.md new file mode 100644 index 00000000..5352abfb --- /dev/null +++ b/docs/componentes/cadena-de-origen.md @@ -0,0 +1,121 @@ +# Cadena de origen + +El SAT utiliza este método de generar cadenas originales para agrupar +en una forma y orden determinados información que no debería ser alterada. + +Una vez que se cuenta con la cadena de origen, se genera una firma con la llave +privada que puede ser verificada con un certificado (llave pública). + +Si algún dato que es un componente para la cadena de origen fue modificado +entonces producirá un sello diferente o la verificación será negativa. + +Esto significa que un CFDI puede ser alterado posteriormente a su elaboración. +Por ejemplo, se le puede agregar una adenda o poner/quitar formato al XML, +pues ni el nodo Addenda ni el "XML Whitespace" forman parte de la cadena de origen. + +Incluso, es frecuente "reparar" un CFDI que tiene errores como una adenda sin XSD +o errores sintácticos como poner un número par de rutas en el `xsi:schemaLocation` +o eliminar espacios de nombres no utilizados. + + +## Método del SAT para generar cadenas de origen + +El método que utiliza el SAT es convertir archivos XML (o parte de los archivos) +a texto simple utilizando la tecnología XSLT. + +Nota: Este no es el único método, siguiendo la especificación del Anexo 20 +y todas y cada una de las especificaciones de los complementos se podría +hacer un generador de cadenas de origen. + +Yo ya lo hice antes y es mucho código para fabricar y testear, además de +que deberá cambiar conforme cambien las especificaciones. + + +## Generar una cadena de origen + +Para generar cadenas de origen tenemos diferentes implementaciones de +la interfaz `\CfdiUtils\CadenaOrigen\XsltBuilderInterface`. +Contiene un único método `build(string $xmlContent, string $xsltLocation)`. + +El `$xmlContent` es el XML que se desea convertir y `$xsltLocation` es la +ubicación del archivo XSLT (local o remoto). + +Las implementaciones son: + +- `DOMBuilder`: Genera la transformación usando PHP, aunque no existe + soporte nativo para Xslt versión 2, la transformación es compatible + y genera el resultado esperado. +- `GenkgoXslBuilder`: Funciona igual que `DOMBuilder` pero al momento de hacer + la transformación utiliza la librería [genkgo/xsl](https://github.com/genkgo/xsl) + que es una implementación de Xslt versión 2 en PHP. + Para usarla debes hacer algo como `composer require genkgo/xsl`. +- `SaxonbCliBuilder`: Utiliza la herramienta + [Saxon-B XSLT Processor](https://en.wikipedia.org/wiki/Saxon_XSLT) desde la + ejecución por línea de comandos. Esta utilería presume la implementación de Xslt versión 2. + Para usarla debes hacer algo como `apt-get install libsaxonb-java`. + + +### Generar la cadena de origen de un Comprobante + +Se puede seguir esta receta: + +```php +resolveCadenaOrigenLocation('3.3'); + +// fabricar la cadena de origen +$builder = new DOMBuilder(); +$cadenaorigen = $builder->build($xmlContent, $location); +``` + +Sin embargo, en la práctica es poco probable que desees generar la cadena de origen. +Básicamente porque si estás creando un CFDI esta será generada automáticamente. +Si estás leyendo o validando también será generada automáticamente por los validadores. + + +### Generar la cadena de origen de un Timbre Fiscal Digital + +A diferencia de la cadena de origen del Comprobante, la cadena de origen del Timbre Fiscal Digital +sí se necesita al menos para mostrarla en la representación impresa del CFDI. + +Para ello puedes utilizar la clase `\CfdiUtils\TimbreFiscalDigital\TfdCadenaDeOrigen`. +Esta clase solo funciona con TFD versiones 1.0 y 1.1. En caso de otra versión genera una excepción. + +Esta clase requiere de un `XmlResolver` y puede establecerse +en el constructor o en el método `setXmlResolver`. + +```php +'; + +$builder = new TfdCadenaDeOrigen(); +$builder->setXmlResolver(new XmlResolver()); + +$tfdCadenaOrigen = $builder->build($tfdXmlString); +``` + +## PHP y XLST versión 2 + +Es importante notar que hasta el momento (enero/2018) no es posible en PHP +procesar XSLT versión 2.0. Sin embargo el procesador que sí tiene PHP genera +las cadenas de origen a pesar de la versión. +Esto no garantiza que si el SAT modifica los archivos XSLT utilizando +características incompatibles se producirá el resultado correcto. + +En la versión 2.2.0 de la librería se ha implementado Saxon-B y Genkgo Xsl +como alternativas al método de PHP. Las tres entregan el mismo resultado en los test. +La que se usa de forma predeterminada es la de PHP `DOMBuilder`. diff --git a/docs/componentes/certificado.md b/docs/componentes/certificado.md new file mode 100644 index 00000000..3b3e2b23 --- /dev/null +++ b/docs/componentes/certificado.md @@ -0,0 +1,67 @@ +# Certificado + +La clase `\CfdiUtils\Certificado\Certificado` obtiene la información de un archivo de tipo certificado. + +El archivo puede ser un archivo en formato PEM o en formato CER. +En este último caso es convertido a PEM y luego interpretado. + +Una vez cargado el certificado permite obtener los siguientes datos: + +- RFC +- Nombre +- Número de serie +- Válido desde y hasta +- Llave pública +- Nombre del archivo cargado + +Adicionalmente cuenta con los métodos: + +- Permite verificar si una llave privada corresponde a este certificado: + + `belongsTo(string $pemKeyFile, string $passPhrase = ''): bool` + +- Permite verificar si una firma dada corresponde a los datos, como por ejemplo, + si el sello corresponde con la cadena de origen. + + `verify(string $data, string $signature, int $algorithm = OPENSSL_ALGO_SHA256): bool` + + +## Relación con `\CfdiUtils\Certificado\NodeCertificado` + +La clase `\CfdiUtils\Certificado\Certificado` funciona con un archivo previamente almacenado. +Para extraer un certificado de un CFDI se ofrece la clase `\CfdiUtils\Certificado\NodeCertificado`. + + +## Acerca de los formatos de archivo + +El certificado (el archivo extensión CER) puede ser leído directamente. + +La llave privada (el archivo extensión KEY) debe ser convertido a tipo PEM +para ser correctamente interpretado por esta clase (en realidad por PHP). + +## Comandos útiles de openssl + +- Obtener información del certificado: + +```shell +openssl x509 -nameopt utf8,sep_multiline,lname -inform DER -noout -dates -serial \ + -subject -fingerprint -pubkey -in CSD01_AAA010101AAA.cer +``` + +- Convertir la llave privada a un archivo PEM sin contraseña: + +```shell +openssl pkcs8 -inform DER -in CSD01_AAA010101AAA.key -out CSD01_AAA010101AAA.key.pem +``` + +- Establecer la contraseña a un archivo PEM: + +```shell +openssl rsa -in CSD01_AAA010101AAA.key.pem -des3 -out CSD01_AAA010101AAA_password.key.pem +``` + +- Convertir el certificado a formato PEM: + +```shell +openssl x509 -inform DER -outform PEM -in CSD01_AAA010101AAA.cer -pubkey -out CSD01_AAA010101AAA.cer.pem +``` diff --git a/docs/componentes/elements.md b/docs/componentes/elements.md new file mode 100644 index 00000000..e8112e69 --- /dev/null +++ b/docs/componentes/elements.md @@ -0,0 +1,71 @@ +# Estructura de datos Elements + +El espacio de nombres `CfdiUtils\Elements` es una especialización de [`CfdiUtils\Nodes`](nodes.md). + +Se trata solo de una estructura de datos, no caigas en la tentación de insertar lógica más allá de la propia estructura. + +Todo *elemento* es un *nodo*, entonces todos los métodos y propiedades de `NodeInterface` están presentes. + +Entonces, para escribir atributos se recomienda usar la forma de arreglo, por ejemplo: +`$comprobante['Descuento'] = '1234.56'` para establecer el atributo "Descuento" a "1234.56". + +Cualquier elemento debe cumplir con la interfaz `CfdiUtils\Elements\Common\ElementInterface` +que es una extensión de `CfdiUtils\Nodes\NodeInterface` y agrega: + +- `getElementName(): string`: Devuelve el nombre del elemento, como `cfdi:Complemento` +- `getFixedAttributes(): array`: Establece la lista de nodos predefinidos al crearse (útil para Complementos y Comprobante) + +En última instancia, un *elemento* (`ElementInterface`) es un *nodo* (`NodeInterface`) +por lo que puedes utilizar a bajo nivel todo el poder de los nodos para trabajar con esta estructura de datos. + + +## Nomenclatura genérica + +Los elementos deberían seguir esta nomenclatura genérica para nombrar sus métodos. + + +### Prefijo `get*` + +La nomenclatura con el prefijo `get*` se escribe en la forma `ElementoPadre::getElementoHijo(): ElementoHijo` +y se espera devolver una única instancia de `ElementoHijo`. Si la instancia no existe entonces se crea. + + +### Prefijo `add*` + +La nomenclatura con el prefijo `add*` se escribe la forma `ElementoPadre::addElementoHijo($attributes): ElementoHijo` +y se espera crear una instancia de `ElementoHijo` con los atributos datos, agregarla a los hijos de `ElementoPadre` +y la instancia de `ElementoHijo` creada. + +Cuando se utiliza `add*` hay dos comportamientos esperados: + +- Si solo debe haber un hijo de un determinado tipo, entonces no se crea uno nuevo y se ocupa el existente. +- Si puede haber más de un hijo de un determinado tipo, entonces se crea uno nuevo y se agrega a los hijos. + +Por eso, como solo debe haber un nodo emisor dentro de un comprobante, +entonces `Comprobante::addEmisor(['RegimenFiscal' => '601'])` tiene este comportamiento: + +- Se obtiene el elemento `Emisor`, si no existe se crea uno vacío. +- Se escriben los atributos pasados al elemento obtenido. +- Se devuelve el elemento. + +Por el contrario, como puede haber varios Cfdi Relacionados, entonces +`CfdiRelacionados::addCfdiRelacionado(['UUID' => $uuid])` tiene este comportamiento: + +- Se crea un elemento de tipo `CfdiRelacionado` con los atributos pasados. +- Se agrega el elemento recién creado a los hijos de `CfdiRelacionados`. +- Se devuelve el elemento creado. + +Existe un caso donde lo que se espera entregar como atributo al pefijo `add*` es en realidad un hijo. +Esto sucede en `addComplemento` y `addAddenda`. + + +### Prefijo `multi*` + +La nomenclatura con el prefijo `multi*` se escribe la forma `ElementoPadre::multiElementoHijo(...$attributes): ElementoPadre` +y se espera crear múltiples una instancia de `ElementoHijo` con los atributos datos, agregarla a los hijos de `ElementoPadre` +y la instancia de `ElementoPadre` creada. + +Otra forma de decirlo: es como los métodos `add*` pero se le pueden mandar varios arreglos de atributos y se creará un elemento para cada parámetro enviado. + +Por lo anterior, `CfdiRelacionados::multiCfdiRelacionado([ ['UUID' => $uuid1], ['UUID' => $uuid2] ])` agregará dos hijos +y devolverá la misma instancia del objeto llamado. diff --git a/docs/componentes/estado-sat.md b/docs/componentes/estado-sat.md new file mode 100644 index 00000000..0bbd620a --- /dev/null +++ b/docs/componentes/estado-sat.md @@ -0,0 +1,130 @@ +# Consulta del estado de un CFDI en el WebService del SAT + +El SAT cuenta con un webservice para consultar el estado de un CFDI. + +- Servicio: +- Documentación: + +Para poderlo consumir se han implementado varias clases dentro del espacio de nonbres `\CfdiUtils\ConsultaCfdiSat`. + +- `WebService`: Objeto que permite consumir el servicio. +- `Config`: Objeto que permite configurar la consulta. +- `RequestParameters`: Objeto que contiene los parámetros del CFDI que se va a consultar. +- `StatusResponse`: Objeto que contiene la respuesta del servicio. + + +## Datos que se requieren para hacer una consulta + +Ocurre que el webservice de consulta SAT toma como entrada la información que está dentro del código QR del CFDI. +Este código tiene diferentes representaciones para diferentes versiones, entre los cambios más significativos están: + +- En el CFDI 3.3 se incluye una ruta +- En el CFDI 3.3 se incluyen los últimos 8 caracteres del sello (de los cuales los últimos 2 siempre serán `==`) +- En el CFDI 3.3 el total del comprobante se expresa con diferente formato +- El orden de los componentes cambian de la versión 3.2 a la versión 3.3 + +Por lo anterior, al construir la cadena de consulta, dependemos de la versión del CFDI. + + +### Acerca del total en la expresión impresa en CFDI 3.3 + +El SAT extablece en el Anexo 20 que el total se debe expresar como: + +- Total del comprobante máximo a 25 posiciones +- 18 posiciones para los enteros +- 1 para caracter `"."` +- 6 para los decimales +- Se deben omitir los ceros no significativos +- Precedido por el texto `"&tt="` +- Todo el conjunto va de 7 a 29 posiciones + +Por lo anterior, al excluir los ceros no significativos, no considerar como +no significativo si el grupo (decimal o entero) no existe, por ejemplo: + +Si el total es por 99 centavos entonces la expresión deberá ser `&tt=0.99` +Si el total es por 1 peso entonces la expresión deberá ser `&tt=1.0` +Si el total es por cero entonces la expresión deberá ser `&tt=0.0` + + +## Configuración de la consulta + +La consulta se puede configurar enviándole un objeto `Config` al consumidor. +Las opciones disponibles son: + +- `timeout`: Define en segundos el tiempo máximo de espera, por omisión es 10. +- `verifyPeer`: Define si SSL debe verificar el certificado de conexión. +- `wsUrl`: Define la ubicación del WSDL. + +La consulta usa la librería de SOAP de PHP y por el momento no es posible configurarla +con un contexto o un cliente de conexión, por lo que si estás detrás de un proxy lo mejor +que puedes hacer es poner en la lista blanca el recurso del SAT o instalar un proxy inverso +y cambiar la URL. + + +## Datos que entrega la consulta + +El servicio entrega dos valores: estado de la consulta y estado del cfdi + +El estado de la consulta tiene tres posibles respuestas: + +- `S - Comprobante obtenido satisfactoriamente` +- `N - 601: La expresión impresa proporcionada no es válida` +- `N - 602: Comprobante no encontrado` de este no he podido encontrar un input que me lo devuelva + +El estado del cfdi tiene tres posibles respuestas: + +- `Vigente` +- `No Encontrado` +- `Cancelado` + +Dado lo anterior, los estados normales que podría entregar el servicio son: + +Consulta | CFDI | Explicación +-------- | ------------- | --------------------------------------------------------------- +S | Vigente | La consulta fue hecha y al momento el CFDI estaba vigente +S | Cancelado | La consulta fue hecha y al momento el CFDI estaba cancelado +N - 601 | No Encontrado | La consulta fue hecha pero la información impresa es incorrecta +N - 602 | No Encontrado | La consulta fue hecha pero el CFDI no existe + +El problema que encontré es que alterando solo 1 dato (el total) esperaba encontrar un estado de `N - 602` +pero el estado devuelto fue `N - 601`. + + +## Ejemplo de uso + +```php +request($request); + +// suponiendo que la consulta fue hecha y el resultado es que el CFDI está cancelado +$response->responseWasOk(); // true +$response->isVigente(); // false +$response->isCancelled(); // true +$response->isNotFound(); // false +$response->getCode(); // S - ... +$response->getCfdi(); // Cancelado +``` + +## Posibles futuros cambios + +Usar alguna librería como o +en lugar de la extensión SOAP de PHP. + +Esto podría llevar a mejores opciones de configuración como establecer un proxy o generar consultas asíncronas. + +Crear un objeto que permita, a partir de un contenido XML, generar objeto `RequestParameters` apropiado. diff --git a/docs/componentes/nodes.md b/docs/componentes/nodes.md new file mode 100644 index 00000000..73f0d873 --- /dev/null +++ b/docs/componentes/nodes.md @@ -0,0 +1,142 @@ +# Estructura de datos Node + +Esta estructura de datos permite administrar en memoria una colección de nodos con hijos de tipo nodo +donde cada uno tiene una colección de atributos. Los nodos no tienen referencia de padre. + + +## Objeto `CfdiUtils\Nodes\Node` + +Esta es la estructura básica. Un nodo debe tener un nombre y esta propiedad no se puede cambiar. +Su contructor admite tres parámetros: + +- `string $name`: Nombre del nodo, se eliminan espacios en blanco al inicio y al final, no permite vacíos. +- `string[] $attributes`: arreglo de elementos clave/valor que serán importados como atributos +- `string[] $nodes`: arreglo de elementos `Node` que serán importados como nodos hijo + + +### Atributos de nodos `attributes(): CfdiUtils\Nodes\Attributes` + +Se accesa a sus atributos utilizando la forma de arreglos de php siguiendo estas reglas básicas: + +- La lectura de un nodo siempre devuelve una cadena de caracteres aunque el atributo no exista. +- La escritura de un nodo es siempre con una cadena de caracteres, también puede ser un objeto + que implemente el método `__toString()` + +Los atributos se manejan con una colección de tipo `Attributes` y se pueden obtener usando el método +`attributes()`. + +```php + '1' +]); + +echo $node['id']; // '1' +echo $node['no-existe']; // cadena de caracteres vacía +echo isset($node['no-existe']) ? 'sí' : 'no'; // no +$node['atributo'] = 'valor'; // establece el valor +unset($node['foo']); // elimina el atributo 'foo' + +// recorrer la colección de atributos +foreach ($node->attributes() as $attributeName => $attributeValue) { + echo $attributeName, ': ', $attributeValue; +} +``` + + +### Nodos (`children(): CfdiUtils\Nodes\Nodes`) + +Los nodos hijos se manejan a través de una colección de nodos `Nodes`. +Se puede acceder al objeto `Nodes` usando el método `children()`. + +Cuanto se itera el objeto en realidad se está iterando sobre la colección de nodos. + +La clase `Node` tiene estos métodos de ayuda que sirven para trabajar directamente sobre la colección Nodes: + +- iterador: el `foreach` se realiza sobre la colección de nodos. +- `addChild(Node $node)`: agrega un nodo en la colección de nodos. + + +### Métodos de búsqueda + +Un objeto de tipo `Node` tiene los siguientes métodos para poder interactuar con sus hijos: + +- `searchAttribute(string ...$searchPath): string`: Devuelve el valor de un atributo según una búsqueda. +- `searchNode(string ...$searchPath): Node|NULL`: Devuelve un objeto de tipo `Node` o `NULL` según una búsqueda. +- `searchNodes(string ...$searchPath): Nodes`: Devuelve un objeto de tipo `Nodes` según una búsqueda. + +La búsqueda se refiere a los nombres de los hijos, por ejemplo: +`$node->searchNode('orden', 'articulos', 'articulo')` +busca dentro de los hijos de `$node` el **primer** nodo llamado `orden`, +si existe busca el primer nodo dentro de `orden` que se llame `articulos`, +si existe busca el primer nodo dentro de `articulos` que se llame `articulo`, +si existe devuelve dicho elemento. +Si alguno no existiera entonces devuelve un valor nulo. + +De la misma forma, `$node->searchNodes('orden', 'articulos', 'articulo')` devuelve una colección +con los elementos llamados `articulo` que están dentro de `orden/articulos`. + +Nota: Si se agrega o elimina un elemento a colección devuelta por `searchNodes`, dicho nodo no se agregará o modificará en el padre. +Esto es porque la colección devuelta por `searchNodes` es simplemente una agrupación adicional a la estructura principal +de nodos. Sin embargo, los hijos de esta colección sí hacen referencia a los nodos de la estructura principal, +por lo que cualquier cambio a los hijos sí será reflejado. + +Cuando se require obtener solamente un valor de atributo de un nodo se puede utilizar `searchAtribute`, +la búsqueda se comporta igual que `searchNode`. Si el nodo buscado no existe devolverá una cadena vacía. +Por ejemplo: `$node->searchNodes('orden', 'articulos', 'nota')` devolverá el atributo `nota` de `orden/articulos`. + + +## Clase CfdiUtils\Nodes\Nodes + +Esta clase representa una colección de `Node`. Al iterar en el objeto se recorrerá cada uno de los nodos. + +Se pueden hacer las operaciones básicas como: +`add(Node $node)`, +`indexOf(Node $node)`, +`remove(Node $node)`, +`removeAll()`, +`exists(Node $node)`, +`get(int $index)`. + +Adicionalmente se pueden usar los métodos: +`firstNodeWithName(string name): Node|null`, +`getNodesByName(string $nodeName): Nodes` y +`importFromArray(Nodes[] $nodes)` + + +## Clase CfdiUtils\Nodes\Attributes + +Esta clase representa una colección de atributos identificados por nombre. +Al iterar en el objeto se devolverá cada uno de los attributos en forma de clave/valor. + +Adicionalmente esta clase permite el uso de acceso como arreglo, por lo que permite: + +- `$attributes[$name]` como equivalente de `$attributes->get($name)` +- `$attributes[$name] = $value` como equivalente de `$attributes->set($name, $value)` +- `isset($attributes[$name])` como equivalente de `$attributes->exists($name)` +- `unset($attributes[$name])` como equivalente de `$attributes->remove($name)` + +Se pueden hacer las operaciones básicas como: + +- `get(string $name): string` +- `set(string $name, string $value)` +- `remove(string $name)` +- `removeAll()` +- `exists(string $name)` + + +## XmlNodeUtils + +Esta es una clase de utilerías que contiene métodos estáticos que permiten crear estructuras de nodos desde XML +y generar XML a partir de los nodos. Recuerde que los nodos solo pueden almacenar atributos y nodos hijos. + +Actualmente permite exportar e importar a/desde: `DOMDocument`, `DOMElement`, `SimpleXmlElement` y `string` (con contenido válido). + +**Advertencias:** + +- Los nodos no tienen campo de contenido y no son una reescritura fiel de DOM. +- Los nodos solo contienen atributos e hijos. +- Importar XML que no siga la estructura de atributos/hijos exclusivamente puede resultar en pérdida de datos. diff --git a/docs/componentes/xmlresolver.md b/docs/componentes/xmlresolver.md new file mode 100644 index 00000000..6e2dd63d --- /dev/null +++ b/docs/componentes/xmlresolver.md @@ -0,0 +1,116 @@ +# Almacenamiento local de recursos del SAT + +El SAT publica diferentes recursos para diferentes tareas, +los recursos más usuales son: + +- Archivos XSD: Son archivos de esquemas XML y sirven para comprobar que + un archivo es correcto con respecto a ciertas reglas. +- Archivos XSLT: Son archivos de transformaciones XML y sirven para transformar + el contenido de un archivo XML en otro contenido. + El SAT los utiliza para generar cadenas de origen. +- Archivos CER: Son archivos de certificado comúnmente utilizados para verificar + que una firma es válida con respecto a un emisor. + La firma es lo que el sat llama sello y el emisor se distingue por un certificado. + +Estos recursos están disponibles en internet, pero son grandes y tienen cambios esporádicos. Por ejemplo, el archivo de catálogos del SAT mide 6.3 MB. +Por ello es conveniente tener una copia local de los recursos. + +El problema viene cuando esos recursos no se pueden simplemente descargar y almacenar. +Muchos recursos dependen de otros y sus rutas de dependencia no son relativas, +por esto es necesario descargar y manipular los recursos para cambiar las dependencias. + +Por suerte esta librería viene con una utilería para mantener copias locales de los recursos según nos convenga. + +Internamente, cuando se solicita un recurso, la librería busca la mejor opción según esté configurada: + +- Si no se ha configurado un repositorio local entonces devuelve la ruta del recurso remoto. +- Si se ha configurado un repositorio local entonces busca si existe. + - Si existe devuelve la ruta del recurso local. + - Si no existe lo descarga y devuelve la ruta del recurso local. + + +## Repositorio local por defecto + +Por defecto, la librería utilizará como repositorio local el lugar donde esté instalada y le agregará: +`/build/resources/`. Por lo que, generalmente, si estás usando `composer` entonces el lugar donde están +los recursos es: `/vendor/eclipxe/cfdiutils/build/resources/`. + + +## Cómo manipular el lugar por defecto del repositorio + +La forma de modificarlo es creando una instancia del objeto `CfdiUtils\XmlResolver\XmlResolver` +y especificando la dirección del repositorio en `localPath`, esto se puede lograr con el constructor del objeto +o bien con el método `setLocalPath(string $localPath = null)`. +En ambos casos (constructor o método) se aplican las siguientes reglas: + +- Si se pasa una cadena de caracteres vacía se desconfigura el repositorio local, + por lo que no se almacenarán recursos localmente. +- Si se pasa `null` el valor se establece a `defaultLocalPath()`, + es decir `/build/resources/`. +- Si se pasa otro valor entonces será usado, por ejemplo `/tmp/sat/`. + +Después, es necesario que ese objeto se utilice en los otros objetos que estamos usando, por ejemplo: + +```php +setXmlResolver($myResolver); + +// ponerlo utilizando el constructor +$cfdiValidator = new \CfdiUtils\CfdiValidator33($myResolver); +``` + +Mi recomendación es que, si vas a modificar o desactivar el repositorio local, tengas +una fábrica de objetos (factory pattern) o bien una función que siempre te devuelva +el objeto configurado tal y como lo necesitas. + + +## Cómo invalidar el caché del almacenamiento local + +**En corto: Simplemente bórralos.** + +No se trata de un caché, porque no hay fechas de caducidad de los recursos. + +Cuando descargas algún recurso este podría descargar hijos y a su vez estos podrían descargar nuevos hijos. +De igual forma, no solo se descargan recursos del SAT, también podrían descargarse recursos de terceros. +Por eso te recomiendo que, si hubo algún cambio en los archivos XSD del SAT elimines entonces cualquier archivo +de tipo `*.xsd` dentro de la carpeta `/www/www.sat.gob.mx`. + + +## Configurando el objeto que se encarga de la descarga de archivos + +Imagina ahora que tu proyecto corre en un servidor dentro de una red corporativa que tiene +salida a internet usando un proxy con usuario y contraseña. +La librería por defecto no puede obtener los recursos que necesita. +Sin embargo, para ello existe la interface `\XmlResourceRetriever\Downloader\DownloaderInterface` +(esta interface no pertenece a este proyecto, pertenece a `XmlResourceRetriever`). + +Tu puedes implementar el `DownloaderInterface` en una clase que utilice `curl` o `guzzle` +o ejecute un comando en la shell como `wget` y luego crear tu objeto `XmlResolver` con este descargador. + +```php +setDownloader($myDownloader); + +// establecer el descargador a un descargador simple (ver PhpDownloader) +$myResolver->setDownloader(null); +``` diff --git a/docs/contribuir/guia-desarrollador.md b/docs/contribuir/guia-desarrollador.md new file mode 100644 index 00000000..56c5bc05 --- /dev/null +++ b/docs/contribuir/guia-desarrollador.md @@ -0,0 +1,82 @@ +# Guía para desarrollar CfdiUtils + +Esta es una guía rápida que pretende guiarte para que puedas desarrollar la librería. + +## Código de conducta + +Revisa nuesto [COC][] y nuestra página de [CONTRIBUTING][]. + +En resumen: + +* No toleraremos la discriminación y el maltrato. +* Cuando reportes un problema procura documentar lo más posible tu caso. +* Cuando desees contribuir al código, realiza pruebas. +* Apégate al estándar de codificación. +* Usa las herramientas básicas, antes de enviar tu PR ejecuta: `composer build`. + +## Dependencias de desarrollo + +Requieres tener instalado y disponible `git` `composer` y `php`. + +Opcionalmente podrías tener instalado `saxonb-xslt`. + +El proyecto es compatible con PHP 7.0. +Respeta esta compatilibilidad, no agregues características de versiones superiores. + +## Primeros pasos + +Descargar el proyecto + +```shell +git clone https://github.com/eclipxe13/cfdiutils +mkdir -p cfdiutils/build/ +``` + +Instalar las dependencias, opcionalmente puedes poner `--prefer-dist` para instalar +los paquetes con + +```shell +composer install +``` + +## Pruebas + +Para probar que todos los archivos no contienen errores de sintaxis + +```shell +vendor/bin/phplint +``` + +Para probar que no se están violando las reglas de estilo + +```shell +vendor/bin/phpcs -sp --colors src/ tests/ +vendor/bin/php-cs-fixer fix --using-cache=no --dry-run --verbose +``` + + +El proyecto viene acompañado de archivos de pruebas de PHPUnit + +```shell +vendor/bin/phpunit +``` + +También ejecutamos PHPStan sobre archivos de orígenes y pruebas + +```shell +vendor/bin/phpstan.phar analyse --level max src/ tests/ +``` + + +## Comandos de ayuda + +Para corregir todos los problemas de estilo que encuentre + +```shell +vendor/bin/php-cs-fixer fix --verbose +vendor/bin/phpcbf --colors -sp src/ tests/ +``` + + +[coc]: https://github.com/eclipxe13/CfdiUtils/blob/master/CODE_OF_CONDUCT.md +[contributing]: https://github.com/eclipxe13/CfdiUtils/blob/master/CONTRIBUTING.md \ No newline at end of file diff --git a/docs/contribuir/guia-documentador.md b/docs/contribuir/guia-documentador.md new file mode 100644 index 00000000..c119c768 --- /dev/null +++ b/docs/contribuir/guia-documentador.md @@ -0,0 +1,74 @@ +# Guía para documentar CfdiUtils + +En esta guía conocerás lo básico para crear y modificar la documentación para la librería + + +## Ubicación de la documentación + +La documentación se encuentra publicada en . + +Los archivos fuente de la documentación están en la carpeta `docs/` y además se apoya +de los archivos `mkdocs.yml` y `.markdownlint.json`. + +La documentación es compilada (o transformada, o como le quieras decir) utilizando +la herramienta [`mkdocs`](https://www.mkdocs.org/). + +Si deseas realizar un cambio en la documentación realiza el proceso normal de cualquier cambio +en GitHub (fork, pull, push & pull-request). + +No somos expertos ni en ReadTheDocs ni en mkdocs, así que si tienes experiencia cuéntanos cómo +podemos mejorar el proyecto y su integración. + + +## Reglas + +- La documentación se debe escribir en español con excepción del archivo `CHANGELOG.md` +- Términos como XML, XSD, XSLT se escriben en mayúsculas. +- Los archivos van escritos en minúsculas y estructurados en grupo, a excepción de `TODO.md` y `CHANGELOG.md` +- Todos los nombres de funciones, clases, propiedades, métodos, etc. deben escribirse con ` (acento grave) +- Se debe cumplir con la sintaxis de markdown aceptada por `markdownlint`, excepto: + - Se puede usar la longitud de línea que sea + - Se admiten hasta dos `NEW_LINE` seguidos + - Los encabezados (*headings*) pueden acabar con signo de admiración e interrogación + - Mira el archivo `.markdownlint.json` + + +## Flujo de trabajo + +Estas herramientas te ayudarán para realizar la documentación y no tener problemas de construcción: + +- [`mkdocs`](https://www.mkdocs.org/)`: Usada para previsualizar los cambios. +- [`markdownlint`](https://github.com/DavidAnson/markdownlint): Revisión de la sintaxis. +- `git`: Control de cambios. + +Descargar el proyecto + +```shell +git clone https://github.com/eclipxe13/cfdiutils +``` + +Ver los cambios en el navegador mientras suceden, esto abre un puerto en tu equipo +que puedes consultar en el navegador, por ejemplo: `http://127.0.0.1:8000/` + +```shell +mkdocs serve +``` + +Realiza tus cambios, te recomiendo usar alguno de los editores que tienen soporte para +`markdownlink`, puedes encontrar una lista en + +Antes de publicar, verifica tus cambios + +```shell +node_modules/markdownlint-cli/markdownlint.js *.md docs/ +``` + + +## Instalación de `markdownlint` + +El proyecto cuenta con un archivo `package.json` que contiene la dependencia de `markdownlint-cli`, +por lo que si no lo tienes instalado globalmente lo único que tendrías que hacer para instalarlo en el proyecto es: + +```shell +npm install +``` diff --git a/docs/crear/complementos-aun-no-implementados.md b/docs/crear/complementos-aun-no-implementados.md new file mode 100644 index 00000000..7d18e292 --- /dev/null +++ b/docs/crear/complementos-aun-no-implementados.md @@ -0,0 +1,74 @@ +# Complementos que no están implementados + +No todos los complementos están disponibles para utilizarse con las clases +de ayuda `CfdiUtils\Elements`. Sin embargo, este no es motivo para no poder +agregar el nodo a la estructura del CFDI. + +Recuerda que en realidad, la forma en como esta librería almacena la +información es utilizando [nodos](../componentes/nodes.md) `CfdiUtils\Nodes\Node`. +Por lo que usando esta estructura será muy fácil agregar la información. + +Nodos de `` y ``: + + +En el siguiente ejemplo voy a agregar la información necesaria del complemento de +[leyenda fiscal](http://www.sat.gob.mx/informacion_fiscal/factura_electronica/Documents/Complementoscfdi/leyendasFisc.pdf) + +Y voy a partir de la **suposición** (**no real**) de que al facturar consultoría en +desarrollo de software tengo que poner una leyenda fiscal con la licencia +del software desarrollado. + +```php +comprobante(); +$comprobante->addAttributes([ + // ... atributos del comprobante +]); +// ... llenar la información del comprobante + +// Creación del nodo de LeyendasFiscales +$leyendasFisc = new \CfdiUtils\Nodes\Node( + 'leyendasFisc:LeyendasFiscales', // nombre del elemento raíz + [ // nodos obligatorios de XML y del nodo + 'xmlns:leyendasFisc' => 'http://www.sat.gob.mx/leyendasFiscales', + 'xsi:schemaLocation' => 'http://www.sat.gob.mx/leyendasFiscales' + . ' http://www.sat.gob.mx/sitio_internet/cfd/leyendasFiscales/leyendasFisc.xsd', + 'version' => '1.0', + ] +); + +$leyendasFisc->addChild(new \CfdiUtils\Nodes\Node('leyendasFisc:Leyenda', [ + 'disposicionFiscal' => 'RESDERAUTH', + 'norma' => 'Artíclo 2. Fracción IV.', + 'textoLeyenda' => 'El software desarrollado se entrega con licencia MIT' +])); + +// Agregar el nodo $leyendasFisc a los complementos del CFDI +$comprobante->addComplemento($leyendasFisc); + +// ... más instrucciones + +$creator->saveXml('archivo_con_complemento.xml'); +``` + +Dado el ejemplo anterior, el comprobante contendrá la siguiente información: + +```xml + + + + + + + + + +``` diff --git a/docs/crear/crear-cfdi.md b/docs/crear/crear-cfdi.md new file mode 100644 index 00000000..ea957982 --- /dev/null +++ b/docs/crear/crear-cfdi.md @@ -0,0 +1,161 @@ +# Creación de CFDI 3.3 + +Para crear un CFDI versión 3.3 se ofrece el objeto `CfdiUtils\CfdiCreator33`. + +Este objeto trabaja directamente con la estructura `CfdiUtils\Elements\Cfdi33\Complemento` +para facilitar la manipulación de la estructura y los datos, y contiene métodos que ayudan +a establecer el certificado, generar el sello, generar o almacenar el XML, y validar la estructura recién creada. + +Esta clase es una especie de pegamento de todas las pequeñas utilerías y estructuras de datos. + +## Métodos de ayuda + +- `comprobante(): Comprobante`: Obtiene el nodo raíz `Comprobante`. Todos los métodos utilizan este objeto. + +- `putCertificado(Certificado $certificado, bool $putEmisorRfcNombre = true)`: Establece el valor de los atributos + `NoCertificado` y `Certificado`, y si `$putEmisorRfcNombre` es verdadero entonces también establece el valor + de `Rfc` y `Nombre` en el nodo `Emisor`. + +- `asXml(): string`: Genera y devuelve el contenido XML de la exportación del nodo `Comprobante`. + +- `saveXml(string $filename): bool`: Genera y almacena el contenido XML. + +- `buildCadenaDeOrigen(): string`: Construye la cadena de origen siempre que exista un resolvedor de recursos XML. + +- `buildSumasConceptos(int $precision = 2): SumasConceptos`: Genera un objeto de tipo `SumasConceptos` según los datos de los `Conceptos`. + +- `addSumasConceptos(SumasConceptos $sumasConceptos = null, int $precision = 2)`: Establece los valores de `$sumasConceptos` + en el comprobante, si no se pasó el objeto entonces lo fabrica con `buildSumasConceptos()`. + +- `addSello(string $key, string $passPhrase = '')`: Realiza el procedimiento de firma con la llave primaria y + almacena el valor de dicha llave en base64 en el atributo `Sello`. + Si el certificado existe como un objeto `Certificado` entonces este método también valida que la llave primaria + pertenece al certificado y genera una excepción si no es así. + +- `validate(): Asserts`: Crea un validador que verifica la estructura XML contra su archivo XSD + y realiza validaciones adicionales. + Consulta la [documentación de validaciones](../validar/validacion-cfdi.md) para más información. + + +## Pasos básicos de creación de un CFDI + +No hay una sola forma de hacer las cosas, pero la receta de creación sería algo como: + +```php + 'XXX', + 'Folio' => '0000123456', + // y otros atributos más... +]; +$creator = new \CfdiUtils\CfdiCreator33($comprobanteAtributos, $certificado); + +$comprobante = $creator->comprobante(); + +// No agrego (aunque puedo) el Rfc y Nombre porque uso los que están establecidos en el certificado +$comprobante->addEmisor([ + 'RegimenFiscal' => '601', // General de Ley Personas Morales +]); + +$comprobante->addReceptor([/* Atributos del receptor */]); + +$comprobante->addConcepto([ + /* Atributos del concepto */ +])->addTraslado([ + /* Atributos del impuesto trasladado */ +]); + +// método de ayuda para establecer las sumas del comprobante e impuestos +// con base en la suma de los conceptos y la agrupación de sus impuestos +$creator->addSumasConceptos(null, 2); + +// método de ayuda para generar el sello (obtener la cadena de origen y firmar con la llave privada) +$creator->addSello('file:// ... ruta para mi archivo key convertido a PEM ...', 'contraseña de la llave'); + +// método de ayuda para validar usando las validaciones estándar de creación de la librería +$asserts = $creator->validate(); +$asserts->hasErrors(); // contiene si hay o no errores + +// método de ayuda para generar el xml y guardar los contenidos en un archivo +$creator->saveXml('... lugar para almacenar el cfdi ...'); + +// método de ayuda para generar el xml y retornarlo como un string +$creator->asXml(); +``` + +En el ejemplo anterior en la línea que dice `$comprobante = $creator->comprobante();` +se está obteniendo el **elemento** `CfdiUtils\Elements\Cfdi33\Comprobante`. + +Todos los [elementos](../componentes/elements.md) son una especialización de los [nodos](../componentes/nodes.md). +A diferencia de los nodos, los elementos contienen métodos de ayuda que pemiten entender los hijos que manejan, +por ejemplo `CfdiUtils\Elements\Cfdi33\Comprobante` contiene un método llamado `addReceptor()` +con el que se puede insertar en el lugar correcto el nodo "Receptor" incluyendo un arreglo de atributos. + + +## Formación de el texto de los códigos QR + +La formación del texto que se incluye en los códigos QR tiene reglas específicas +y puede utilizarse el objeto `\CfdiUtils\ConsultaCfdiSat\RequestParameters` +para obtener el texto contenido en el código QR. + +Este es un ejemplo para la obtener la URL directamente de un contenido XML. + +```php +$xmlContents = '...'; +$cfdi = \CfdiUtils\Cfdi::newFromString($xmlContents); +$comprobante = $cfdi->getNode(); // Nodo de trabajo del nodo cfdi:Comprobante + +$parameters = new RequestParameters( + $comprobante['Version'], + $comprobante->searchAttribute('cfdi:Emisor', 'Rfc'), + $comprobante->searchAttribute('cfdi:Receptor', 'Rfc'), + $comprobante['Total'], + $comprobante->searchAttribute('cfdi:Complemento', 'tfd:TimbreFiscalDigital', 'UUID'), + $comprobante['Sello'] +); + +echo $parameters->expression(); // https://verificacfdi.facturaelectronica.sat.gob.mx/... +``` + + +## Orden de los nodos de un CFDI + +A pesar de tratarse de una estructura XML el SAT por las reglas impuestas en los +archivos XSD ha puesto reglas de orden de aparición de nodos. + +Por lo anterior **esta estructura presentará error** porque el nodo `Receptor` +debe ir después del nodo `Emisor`: + +```xml + + + + +``` + +Cuando se está usando el espacio de nombres `CfdiUtils\Elements` las estructuras conocen el +orden en el que deben existir los nodos, por lo que no es necesario preocuparse por el orden de aparición. +Esta mejora fue introducida en la versión 2.4.0. + +Si se está utilizando `CfdiUtils\Nodes` de forma independiente a `CfdiUtils\Elements` entonces será necesario +establecer el orden de los nodos con el método `CfdiUtils\Nodes\Nodes::setOrder(array $order)`. +O simplemente insertar los nodos en el orden correcto. + + +## Resolvedor de recursos XML + +Los archivos XSD necesarios para validar la estructura XML de un CFDI y +los archivos XSLT necesarios para generar la cadena de origen +son almacenados localmente y reutilizados cada vez que se require. + +Para establecer dicha configuración diferente a la predeterminada establezca el objeto `XmlResolver` +usando el método `CfdiCreator33::setXmlResolver(XmlResolver $xmlResolver = null)`. + +Si establece el valor a nulo (`CfdiCreator33::hasXmlResolver()` es `false`) entonces no se podrá +crear la cadena de origen (necesario para obtener la ruta de los archivos XSLT) y tampoco se podrá abastecer +a los objetos de validación que requieran de un resolvedor con el objeto apropiado resultando en varias revisiones +sin ejecutar. + +Si lo que desea es no almacenar localmente los recursos entonces lo que debe hacer es establecer +una cadena de caracteres vacía mediante el método `XmlResolver::setLocalPath`. diff --git a/docs/crear/elements-cfdi33.md b/docs/crear/elements-cfdi33.md new file mode 100644 index 00000000..428b4f4b --- /dev/null +++ b/docs/crear/elements-cfdi33.md @@ -0,0 +1,143 @@ +# Elementos de Cfdi versión 33 + +El espacio de nombres de `CfdiUtils\Elements\Cfdi33` permite trabajar en forma más fácil +con los nodos con nombres y acciones específicas y es la base de la creación de un CFDI 3.3. + +Es la implementación de [elementos](../componentes/elements.md), +que son [nodos](../componentes/nodes.md) con métodos de ayuda. + +## Comprobante `cfdi:Comprobante` + +Representa el nodo raiz Comprobante. +Contiene los siguientes métodos de ayuda: + +- `getCfdiRelacionados(): CfdiRelacionados`: Crea (si no existe) y obtiene el nodo único CfdiRelacionados +- `addCfdiRelacionados(array $attributes = []): CfdiRelacionados`: Agrega y devuelve el único nodo CfdiRelacionados +- `addCfdiRelacionado(array $attributes = []): CfdiRelacionado`: Agrega y devuelve un nuevo nodo CfdiRelacionado +- `addCfdiRelacionados(array ...$attributes): Comprobante`: Agrega nuevos nodos CfdiRelacionado, es una forma rápida de llamar al método `addCfdiRelacionado` múltiples veces +- `getEmisor(): Emisor`: Crea (si no existe) y obtiene el nodo único Emisor +- `addEmisor(array $attributes = []): Emisor`: Agrega y devuelve el único nodo Emisor +- `getReceptor(): Receptor`: Crea (si no existe) y obtiene el nodo único Receptor +- `addReceptor(array $attributes = []): Receptor`: Agrega y devuelve el único nodo Receptor +- `getConceptos(): Conceptos`: Crea (si no existe) y obtiene el nodo único Conceptos +- `addConcepto(array $attributes = [], array $children = []): Concepto`: Agrega y devuelve un nuevo nodo Concepto +- `getImpuestos(): Impuestos`: Crea (si no existe) y obtiene el nodo único Impuestos +- `addTraslado(array $attributes = []): Traslado`: Agrega y devuelve un nuevo nodo Traslado (en Impuestos / Traslados) +- `multiTraslado(array ...$elementAttributes): Comprobante`: Agrega nuevos nodo Traslado, es una forma rápida de llamar al método `addTraslado` múltiples veces +- `addRetencion(array $attributes = []): Retencion`: Agrega y devuelve un nuevo nodo Retencion (en Impuestos / Retenciones) +- `multiRetencion(array ...$elementAttributes): Comprobante`: Agrega nuevos nodo Retencion, es una forma rápida de llamar al método `addRetencion` múltiples veces +- `getComplemento(): Complemento`: Crea (si no existe) y obtiene el nodo único Complemento +- `addComplemento(NodeInterface $children): Comprobante`: Agrega el nodo $children dentro del único nodo Complemento +- `getAddenda(): Addenda`: Crea (si no existe) y obtiene el nodo único Addenda +- `addAddenda(NodeInterface $children): Comprobante`: Agrega el nodo $children dentro del único nodo Addenda + + +## CfdiRelacionados `cfdi:CfdiRelacionados` + +Representa el nodo Comprobante / CfdiRelacionados. + +- `addCfdiRelacionado(array $attributes = []): CfdiRelacionado`: Agrega y devuelve un nuevo nodo CfdiRelacionado + + +## CfdiRelacionado `cfdi:CfdiRelacionado` + +Representa el nodo Comprobante / CfdiRelacionados / CfdiRelacionado. + + +## Emisor `cfdi:Emisor` + +Representa el nodo Comprobante / Emisor. + + +## Receptor `cfdi:Receptor` + +Representa el nodo Comprobante / Receptor. + + +## Conceptos `cfdi:Conceptos` + +Representa el nodo Comprobante / Conceptos. + +- `addConcepto(array $attributes = []): Concepto`: Agrega y devuelve un nuevo nodo Conceptos + + +## Conceptos `cfdi:Concepto` + +Representa el nodo Comprobante / Conceptos / Concepto. + +- `getImpuestos(): Impuestos`: Crea (si no existe) y obtiene el nodo único Impuestos +- `addTraslado(array $attributes = []): Traslado`: Agrega y devuelve un nuevo nodo Traslado (en Impuestos / Traslados) +- `multiTraslado(array ...$elementAttributes): Concepto`: Agrega nuevos nodo Traslado, es una forma rápida de llamar al método `addTraslado` múltiples veces +- `addRetencion(array $attributes = []): Retencion`: Agrega y devuelve un nuevo nodo Retencion (en Impuestos / Retenciones) +- `multiRetencion(array ...$elementAttributes): Concepto`: Agrega nuevos nodo Retencion, es una forma rápida de llamar al método `addRetencion` múltiples veces +- `addInformacionAduanera(array $attributes = []): InformacionAduanera`: Agrega y devuelve un nuevo nodo InformacionAduanera +- `multiInformacionAduanera(array ...$elementAttributes): Concepto`: Agrega nuevos nodo InformacionAduanera, es una forma rápida de llamar al método `addInformacionAduanera` múltiples veces +- `addCuentaPredial(array $attributes = []): CuentaPredial`: Agrega y devuelve el único nodo CuentaPredial +- `getComplementoConcepto(): ComplementoConcepto`: Crea (si no existe) y obtiene el nodo único ComplementoConcepto +- `addComplementoConcepto(array $attributes = [], array $children = []): ComplementoConcepto`: Agrega y devuelve el único nodo Complementoconcepto +- `addParte(array $attributes = [], array $children = []): Parte`: Agrega y devuelve un nuevo nodo Parte +- `multiParte(array ...$elementAttributes)`: Agrega nuevos nodo Parte, es una forma rápida de llamar al método `addParte` múltiples veces + + +## InformacionAduanera `cfdi:InformacionAduanera` + +Representa el nodo Comprobante / Conceptos / Concepto / InformacionAduanera +y Comprobante / Conceptos / Concepto / Parte / InformacionAduanera. + + +## CuentaPredial `cfdi:CuentaPredial` + +Representa el nodo Comprobante / Conceptos / Concepto / CuentaPredial. + + +## ComplementoConcepto `cfdi:ComplementoConcepto` + +Representa el nodo Comprobante / Conceptos / Concepto / ComplementoConcepto. + + +## Parte `cfdi:Parte` + +Representa el nodo Comprobante / Conceptos / Concepto / Parte. + +- `addInformacionAduanera(array $attributes = []): InformacionAduanera`: Agrega y devuelve un nuevo nodo InformacionAduanera +- `multiInformacionAduanera(array ...$elementAttributes): Parte`: Agrega nuevos nodo InformacionAduanera, es una forma rápida de + llamar al método `addInformacionAduanera` múltiples veces + + +## Impuestos `cfdi:Impuestos` + +Representa el nodo Comprobante / Impuestos y también Comprobante / Conceptos / Concepto / Impuestos. + +- `getTraslados(): Traslados`: Crea (si no existe) y obtiene el nodo único Traslados. +- `getRetenciones(): Retenciones`: Crea (si no existe) y obtiene el nodo único Retenciones. + +Aunque el nodo impuestos (hijo de comprobante) es diferente que el nodo impuestos (hijo de concepto) +se puede utilizar la misma estructura de datos porque los cambios se dan a nivel de atributos y no de hijos. + + +## Traslados `cfdi:Traslados` + +Representa el nodo Comprobante / Impuestos / Traslados y Comprobante / Conceptos / Concepto / Impuestos / Traslados. + +- `addTraslado(array $attributes = []): Traslado`: Agrega y devuelve un nuevo nodo Traslado. +- `multiTraslado(array ...$elementAttributes): Traslados`: Agrega nuevos nodo Traslado, es una forma rápida de llamar al método `addTraslado` múltiples veces + + +## Retenciones `cfdi:Retenciones` + +Representa el nodo Comprobante / Impuestos / Retenciones y Comprobante / Conceptos / Concepto / Impuestos / Retenciones. + +- `addRetencion(array $attributes = []): Retencion`: Agrega y devuelve un nuevo nodo Retencion. +- `multiRetencion(array ...$elementAttributes): Retenciones`: Agrega nuevos nodo Retencion, es una forma rápida de llamar al método `addRetencion` múltiples veces + + +## Traslado `cfdi:Traslado` + +Representa el nodo Comprobante / Impuestos / Retenciones / Traslado +y Conceptos / Concepto / Impuestos / Retenciones / Traslado. + + +## Retencion `cfdi:Retencion` + +Representa el nodo Comprobante / Impuestos / Retenciones / Retencion +y Conceptos / Concepto / Impuestos / Retenciones / Retencion. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..db239553 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,80 @@ +# CfdiUtils + +[`eclipxe/CfdiUtils`](https://github.com/eclipxe13/CfdiUtils) +es una librería de PHP para leer, validar y crear CFDI 3.3. + +Mira el archivo [README][] para información rápida (en inglés). + +**Este proyecto se migrará eventualmente a `phpcfdi/cfdiutils`, aun no hay fecha planeada.** + +La motivación de crear esta librería es contar con una herramienta flexible, rápida y +confiable para trabajar con CFDI. Se pretende que sea utilizada por la comunidad de PHP +México, en proyectos privados o proyectos libres como el futuro "BuzonCFDI". + +Esta librería se ha liberado como software libre para ayudar a otros desarrolladores a +trabajar con CFDI y también para obtener su ayuda, todo lo que la comunidad pueda +contribuir será bien apreciado. Tenemos una comunidad activa y dinámica, nos puedes +encontrar en [el canal de gitter](https://gitter.im/eclipxe13/php-cfdi). + +## Lectura de CFDI + +La librería ofrece métodos para leer CFDI versión 3.2 y 3.3. + +- [Lectura formal de un CFDI](leer/leer-cfdi.md) +- [Lectura rápida de un CFDI](leer/quickreader.md) +- [Limpieza de un CFDI](leer/limpieza-cfdi.md) + + +## Validación de CFDI + +Solo hay validadores para CFDI 3.3. + +- [Validar un CFDI 3.3](validar/validacion-cfdi.md) +- [Validaciones estándar](validar/validaciones-estandar.md) + + +## Escritura de CFDI + +Solo hay métodos específicos para CFDI 3.3. + +- [Crear un CFDI 3.3](crear/crear-cfdi.md) +- [Elementos de CFDI](crear/elements-cfdi33.md) +- [Agregar complementos](crear/complementos-aun-no-implementados.md) + + +## Componentes comunes + +- [Estructura de datos `Nodes`](componentes/nodes.md) +- [Estructura de datos `Elements`](componentes/elements.md) +- [Almacenamiento local de recursos del SAT](componentes/xmlresolver.md) +- [Certificados](componentes/certificado.md) +- [Consultar estado de un CFDI](componentes/estado-sat.md) +- [Generación de cadena original](componentes/cadena-de-origen.md) + + +## Contribuciones + +- [Listado de tareas pendientes e ideas](TODO.md) +- [Guía de contribución para desarrolladores](contribuir/guia-desarrollador.md) +- [Guía de contribución para documentadores](contribuir/guia-documentador.md) +- Reportar un problema + + +## Recursos útiles + +- [Listado de cambios](CHANGELOG.md) (en inglés) +- [Página del SAT de CFDI](http://omawww.sat.gob.mx/informacion_fiscal/factura_electronica/Paginas/Anexo_20_version3.3.aspx) + + +## Copyright and License + +The `eclipxe/CfdiUtils` library is copyright © [Carlos C Soto](http://eclipxe.com.mx/) and +licensed for use under the MIT License (MIT). Please see [LICENSE][] for more information. + +La librería `eclipxe/CfdiUtils` tiene copyright © [Carlos C Soto](http://eclipxe.com.mx/) +y se encuentra amparada por la Licencia MIT (MIT). +Consulte el archivo [LICENSE][] para más información. + + +[license]: https://github.com/eclipxe13/CfdiUtils/blob/master/LICENSE +[readme]: https://github.com/eclipxe13/CfdiUtils/blob/master/README.md diff --git a/docs/leer/leer-cfdi.md b/docs/leer/leer-cfdi.md new file mode 100644 index 00000000..d801a51e --- /dev/null +++ b/docs/leer/leer-cfdi.md @@ -0,0 +1,170 @@ +# Leer un comprobante fiscal digital + +El problema de leer un CFDI es que la información entre versiones 3.3, 3.2 +y previas no es compatible. Por ello es necesario necesario primero +averiguar la versión del archivo que deseamos interpretar. + +En esta sección se describe la lectura formal de la librería para leer nodos. +Si tu intensión es solamente leer CFDI entonces te convendría brincar a la +[lectura rápida usando `QuickReader`](quickreader.md). + + +## Procesar un CFDI + +Esta librería almacena la información de un CFDI en una estructura interna llamada +[`Nodes`](../componentes/nodes.md). Por lo que, al leer un CFDI lo que en realidad sucede es +una conversión del contenido XML a esta estructura interna. + + +### El objeto `CfdiUtils\Cfdi` + +Este es un ejemplo básico de lectura de un contenido XML a un objeto de +tipo `CfdiUtils\Cfdi`. Por lo general se utiliza el método estático +`CfdiUtils\Cfdi::newFromString`. + +La clase ofrece cuatro principales *getters* para trabajo, siendo el más importante +el método `CfdiUtils\Cfdi::getNode` que devuelve la instancia del objeto de tipo +`NodeInterface` del elemento principal ``. + +```php +...'; +$cfdi = \CfdiUtils\Cfdi::newFromString($xmlContents); +$cfdi->getVersion(); // (string) 3.3 +$cfdi->getDocument(); // clon del objeto DOMDocument +$cfdi->getSource(); // (string) getNode(); // Nodo de trabajo del nodo cfdi:Comprobante +``` + + +#### Uso de `CfdiUtils\Cfdi::newFromString` + +El método estático `CfdiUtils\Cfdi::newFromString` verifica que el contenido XML +no esté vacío y no contenga errores (se pueda crear un `DOMDocument` a partir +de este contenido). +Posteriormente invoca la creación de un objeto de tipo `CfdiUtils\Cfdi` pasando +el objeto `DOMDocument` como parámetro. + + +#### Constructor de `CfdiUtils\Cfdi` + +Al crear un objeto de tipo `CfdiUtils\Cfdi` se verifican las siguientes reglas +del objeto `DOMDocument`: + +1. el documento implementa el espacio de nombres del cfdi `http://www.sat.gob.mx/cfd/3` +1. con el prefijo `cfdi` +1. en el elemento raíz `` + +No realiza ninguna validación. La validación de un CFDI está fuera de los límites de esta clase. + + +#### Ejemplos básicos de uso de `NodeInterface` + +Para obtener el atributo `Serie` de un complemento, sin importar que el atributo fuera definido +originalmente, si no se definió entonces devolverá una cadena de caracteres vacía. + +```php +getNode(); +echo $complemento['Serie']; +``` + +Para verificar si está especificado el atributo `MetodoPago` + +```php +getNode(); +if (isset($complemento['MetodoPago']) { + // ... +} +``` + +Para recorrer todos los nodos `cfdi:Concepto` (que está dentro de `cfdi:Conceptos`) +se puede utilizar el método `CfdiUtils\Nodes\NodeInterface::searchNodes` que devuelve +una colección de nodos iterable (que se puede utilizar dentro de un `foreach`). + +```php +getNode(); +$conceptos = $complemento->searchNodes('cfdi:Conceptos', 'cfdi:Concepto'); +foreach ($conceptos as $concepto) { + echo $concepto['Unidad']; +} +``` + +Para obtener la primer ocurrencia de un nodo de determinado nombre se puede usar +el método `CfdiUtils\Nodes\NodeInterface::searchNode`. Este método devolverá el nodo +si fue encontrado o devolverá `null` si no se encontró. + +No confundir con el método anterior que devuelve una colección de nodos. + +```php +getNode(); +$tfd = $complemento->searchNodes('cfdi:Complemento', 'tfd:TimbreFiscalDigital'); +if (null === $tfd) { + echo 'No existe el timbre fiscal digital'; +} else { + echo 'UUID: ', $tfd['UUID'], PHP_EOL; +} +``` + +Recuerde consultar la entrada completa relacionada con la [Estructura de datos `Nodes`](../componentes/nodes.md). + + +## Obteniendo la versión de un CFDI sin la clase `CfdiUtils\Cfdi` + +Obtener la versión de un CFDI es sencillo con la clase `CfdiUtils\CfdiVersion`. + +El método que usarás para obtener la versión depende de la información que ya +tengas instanciada: + +- `getFromXmlString()`: Cuando ya tienes el contenido del XML en una variable +- `getFromNode()`: Cuando tienes el nodo principal en un objeto de tipo `CfdiUtils\Nodes\NodeInterface` +- `getFromDOMDocument()` y `getFromDOMElement()`: Cuando tienes el contenido XML + instanciado en un objeto de tipo DOM. + +El resultado de estos métodos será un string con el número de versión y vacío en +caso de no encontrarse un número de versión compatible. + +```php +getFromXmlString($xmlContents); +``` + +Nota: la clase `CfdiUtils\Cfdi` ya realiza este proceso por lo que no es recomendado +duplicar el trabajo de averiguar la versión. + + +## Limpieza de CFDI + +Es frecuente que los archivos CFDI contengan errores. +Para entender más el tema vea el artículo de [Limpieza de un CFDI](limpieza-cfdi-md). + +Si está leyendo un CFDI recibido o no confiable este es un ejemplo de cómo limpiar y crear el objeto CFDI: + +```php +getQuickReader(); +``` + + +## Acceder a los atributos + +Utiliza el objeto como un arreglo usando la notación de corchetes. + +Puedes averiguar si un atributo existe usando `isset()`. + +```php +getQuickReader(); + +echo $comprobante['version']; // (string) "3.3" +echo $comprobante['Version']; // (string) "3.3" +echo $comprobante['vErSiOn']; // (string) "3.3" +var_dump(isset($comprobante['version'])); // (bool) true + + +var_dump($comprobante['no-existe']); // (string) "" +var_dump(isset($comprobante['no-existe'])); // (bool) false +``` + + +## Acceder al primer elemento hijo + +Puedes acceder al primer elemento hijo (exista o no) usando la notación de propiedades. + +Al acceder por medio de propiedades siempre devuelve un objeto de tipo `QuickReader` +aun cuando no exista. Si existe devolverá el primero. + +Puedes averiguar si existe al menos un elemento usando `isset()`. + +```php +getQuickReader(); + +/* + * + * ... + * + */ +echo $comprobante->impuestos['totalImpuestosTrasladados']; // (string) "123.45" + +/* + * + * + * + * + * + */ +echo $comprobante->complemento->timbreFiscalDigital['fechatimbrado']; // 2017-03-21T08:18:08 + +// aun si no existe un elemento no generará una excepción +var_dump(isset($comprobante->foo)); // (bool) false +echo $comprobante->foo->bar->baz->xee['info']; // (string) "" +``` + + +## Acceder a todos los hijos + +Entrar a todos los hijos requiere de una sintaxis especial que consiste en llamar al objeto +como una función. Al realizar la llamada lo que se devuelve es un arreglo de objetos `QuickReader` con los hijos. + +```php +getQuickReader(); + +// $hijos es un arreglo de QuickReader +$hijos = $comprobante(); + +foreach($hijos as $hijo) { + echo $hijo; // Emisor, Receptor, Conceptos, Impuestos, etc... +} +``` + +La sintaxis de esta operación al principio puede ser un poco complicada, pero una vez que te acostumbras es +bastante entendible. + +Por ejemplo, para acceder a todos los hijos del nodo `Conceptos` **no se puede hacer**: +`$comprobante->conceptos()` porque esto significa invocar al método `conceptos` del objeto `$comprobante`. + +Hay dos alternativas para poder hacer esta llamada: + +* Asignar a una variable y luego hacer la invocación: + + `$conceptos = $comprobante->conceptos; $conceptos();` + +* Usar paréntesis para separar la propiedad, y luego invocarla: + + `($comprobante->conceptos)()` + +Si uso dependerá de tu preferencia, aquí dos formas que ejemplifican lo mismo, +primero una variable para acceder a los hijos (conceptos), luego se hace una invocación de la propiedad (traslado). + +```php +getQuickReader(); + +// usando asignación de variable +$conceptos = $comprobante->conceptos; +foreach($conceptos() as $concepto) { + // usando propiedad + foreach(($concepto->impuestos->traslados)() as $traslado) { + echo $traslado['impuesto']; + } +} +``` + + +## Acceder a hijos con un mismo nombre + +Si requieres todos los nodos hijos a los que les corresponda un mismo nombre +simplemente pasa el nombre como argumento de la ejecución del objeto. + +```php +getQuickReader(); + +// $hijos es un arreglo que contiene solo aquellos hijos llamados "concepto" +$hijos = ($comprobante->conceptos)('concepto'); +``` + + +## Nombre del nodo + +Difícilmente lo utilizarás, pero si necesitas saber el nombre del nodo entonces puedes +convertir el nodo a cadena de caracteres y te devolverá su nombre. + +```php +getQuickReader(); + +echo (string) $comprobante; // (string) "Comprobante" +``` diff --git a/docs/validar/validacion-cfdi.md b/docs/validar/validacion-cfdi.md new file mode 100644 index 00000000..754adf2d --- /dev/null +++ b/docs/validar/validacion-cfdi.md @@ -0,0 +1,97 @@ +# Validaciones de CFDI version 3.3 + +Esta librería provee recursos para realizar validaciones en el espacio de nombres `CfdiUtils\Validate`. + +Se busca que al validar no solo se reporten las validaciones con error. También se reportan aquellas +exitosas, las que tienen una advertencia y las correctas, incluso algunas podrían contener una explicación. + +A diferencia de los mensajes de error de toda la librería, todos los mensajes de las validaciones están en español. + +El espacio de nombres contiene un validador `MultiValidator` +que comúnmente se genera con una fábrica `MultiValidatorFactory`. +Gracias a este proceso validar documentos creados o recibidos se simplifica. + + +## Validación de documentos creados + +Si se está creando un documento usando la clase `CfdiUtils\CfdiCreator33` +entonces se puede validar usando el método `validate(): Asserts`, por ejemplo: + +```php +validate(); +$asserts->hasErrors(); // devuelve verdadero en caso de error +``` + + +## Validación de documentos recibidos + +Para esta validar un documento recibido se puede utilizar la clase `CfdiUtils\CfdiValidator33`, por ejemplo: + +```php +validateXml(file_get_contents($cfdiFile)); +$asserts->hasErrors(); // devuelve verdadero en caso de error +``` + +Un objeto de tipo `CfdiValidator33` contiene un `XmlResolver`. +Si se elimina entonces algunos validadores no realizarán el proceso o bien saldrán a internet a encontrar +los recursos que necesitan. Por omisión se crea un nuevo `XmlResolver` pero puede ser establecido +desde su contructor o bien con el método `setXmlResolver`. + +Recuerda que la validación trabajará con la información tal comno es presentada, por lo que tal vez +desees usar el método rápido de limpieza `CfdiUtils\Cleaner\Cleaner::staticClean()`. + + +## ValidatorInterface + +Para que un validador funcione necesita ser de tipo `ValidatorInterface` e implementar: + +- `validate(NodeInterface $comprobante, Asserts $asserts): void`: Método que se llama para validar. +- `canValidateCfdiVersion(string $version): bool`: Devuelve si el validador es compatible con una versión dada. + + +## Assert + +Cada validador debe inyectar uno o más objetos de tipo `Assert` en la colección `Asserts`. +Se puede considerar que un `Assert` es una prueba o un aseguramiento, y cada `Assert` tiene un estado dado por `Status`. + +Gracias al registro de todos los `Assert` en una validación se puede saber no solo lo que falló o generó +una advertencia; también se puede saber lo que estuvo bien o no se pudo comprobar. + +El `Assert` es un "aseguramiento", se trata de un enunciado afirmativo, no un enunciado de error, por ello, +un ejemplo de título del aseguramiento podría ser: *El CFDI tiene una moneda definida y que pertenece al catálogo de monedas*. + + +## Status + +Esta es una clase de tipo "value object" por lo que solamente se puede instanciar con un valor y no modificar. + +Un objeto `Status` puede contener uno de cuatro valores: + +- error: Existe un fallo y se debe considerar que el CFDI es inválido y debería ser rechazado. +- warning: Existe un fallo pero se desconoce si esto es correcto o incorrecto. +- ok: Se realizó la prueba y no se encontró fallo +- none: Ninguno de los estados anteriores, úsese para describir que la prueba no se realizó. + + +## Asserts + +`Asserts` es una colección de objetos de tipo `Assert`. +Esta colección no permite que existan dos `Assert` con el mismo código, cuando se encuentra que se quiere +escribir un `Assert` con el mismo código entonces el previo es sobre escrito. + +```php +validate(); +foreach ($asserts as $assert) { + echo $assert, "\n"; +} +``` diff --git a/docs/validar/validaciones-estandar.md b/docs/validar/validaciones-estandar.md new file mode 100644 index 00000000..fa48c82f --- /dev/null +++ b/docs/validar/validaciones-estandar.md @@ -0,0 +1,183 @@ +# Validaciones estándar para CFDI 3.3 + +Las validaciones estándar se deben realizar tanto para CFDI crceados como para CFDI recibidos. + + +## XmlFollowSchema + +Valida que el archivo XML sigue con los esquemas que tiene declarados contra los +archivos XSD que tenga declarados en los campos `xsi:schemaLocation`. +Este es uno de los validadores más útiles porque revisa la estructura contra el SAT, incluyendo los catálogos. +Cuando este validador falla regresa un estado `mustStop` que previene la ejecución de futuros +validadores dentro de un objeto `MultiValidator`. + +- XDS01: El contenido XML sigue los esquemas XSD + + +## ComprobanteDecimalesMoneda + +Valida que los atributos no tengan más del máximo número de decimales que permite la moneda, +esto incluye los ceros a la izquierda. Si la moneda no es USD, EUR, MXN o XXX entonces todos los estados son NONE. + +- MONDEC01: El subtotal del comprobante no contiene más de los decimales de la moneda (CFDI33106) +- MONDEC02: El descuento del comprobante no contiene más de los decimales de la moneda (CFDI33111) +- MONDEC03: El total del comprobante no contiene más de los decimales de la moneda +- MONDEC04: El total de impuestos trasladados no contiene más de los decimales de la moneda (CFDI33182) +- MONDEC05: El total de impuestos retenidos no contiene más de los decimales de la moneda (CFDI33180) + + +## ComprobanteFormaPago + +Valida que exista si no existe el complemento de pagos y que no exista si existe el complemento de pagos + +- FORMAPAGO01: El campo forma de pago no debe existir cuando existe el complemento para recepción de pagos (CFDI33103) + + +## ComprobanteImpuestos + +Valida el nodo impuestos del comprobante + +- COMPIMPUESTOSC01: Si existe el nodo impuestos entonces debe incluir el total de traslados y/o el total de retenciones +- COMPIMPUESTOSC02: Si existe al menos un traslado entonces debe existir el total de traslados +- COMPIMPUESTOSC03: Si existe al menos una retención entonces debe existir el total de retenciones + + +## ComprobanteMetodoPago + +Validaciones específicas relacionadas con el método de pago + +- METPAG01: Si el tipo de documento es T, P ó N, entonces el metodo de pago no debe existir (CFDI33123, CFDI33124) +- METPAG02: Si el tipo de documento es I ó E, entonces el metodo de pago debe ser "PUE" o "PPD" (CFDI33121, CFDI33122) + + +## ComprobanteTipoCambio + +- TIPOCAMBIO01: La moneda exista y no tenga un valor vacío +- TIPOCAMBIO02: Si la moneda es "MXN", entonces el tipo de cambio debe ser "1" o no debe existir (CFDI33113) +- TIPOCAMBIO03: Si la moneda es "XXX", entonces el tipo de cambio no debe existir (CFDI33115) +- TIPOCAMBIO04: Si la moneda no es "MXN" ni "XXX", entonces el tipo de cambio entonces + el tipo de cambio debe seguir el patrón [0-9]{1,18}(.[0-9]{1,6})? (CFDI33114, CFDI33117) + + +## ComprobanteTipoDeComprobante + +Realiza diferentes validaciones relacionadas con el tipo de comprobante: + +- TIPOCOMP01: Si el tipo de comprobante es T, P ó N, entonces no debe existir las condiciones de pago +- TIPOCOMP02: Si el tipo de comprobante es T, P ó N, entonces no debe existir la definición de impuestos (CFDI33179) +- TIPOCOMP03: Si el tipo de comprobante es T, P ó N, entonces no debe existir la forma de pago +- TIPOCOMP04: Si el tipo de comprobante es T, P ó N, entonces no debe existir el método de pago (CFDI33123) +- TIPOCOMP05: Si el tipo de comprobante es T ó P, entonces no debe existir el descuento del comprobante (CFDI33110) +- TIPOCOMP06: Si el tipo de comprobante es T ó P, entonces no debe existir el descuento de los conceptos (CFDI33179) +- TIPOCOMP07: Si el tipo de comprobante es T ó P, entonces el subtotal debe ser cero (CFDI33108) +- TIPOCOMP08: Si el tipo de comprobante es T ó P, entonces el total debe ser cero +- TIPOCOMP09: Si el tipo de comprobante es I, E ó N, entonces el valor unitario de todos los conceptos debe ser mayor que cero +- TIPOCOMP010: Si el tipo de comprobante es N, entonces la moneda debe ser MXN + + +## ComprobanteTotal + +- TOTAL01: El atributo Total existe, no está vacío y cumple con el patrón [0-9]+(.[0-9]+)? + + +## ConceptoDescuento + +Estas validaciones son exclusivas del atributo descuento del concepto: + +- CONCEPDESC01: Si existe el atributo descuento, entonces debe ser menor o igual que el subtotal y mayor o igual que cero (CFDI33109) + + +## ConceptoImpuestos + +Estas validaciones son exclusivas del nodo impuestos del concepto: + +- CONCEPIMPC01: El nodo impuestos de un concepto debe incluir traslados y/o retenciones (CFDI33152) +- CONCEPIMPC02: Los traslados de los impuestos de un concepto deben tener una base y ser mayor a cero (CFDI33154) +- CONCEPIMPC03: No se debe registrar la tasa o cuota ni el importe cuando el tipo de factor de traslado es exento (CFDI33157) +- CONCEPIMPC04: Se debe registrar la tasa o cuota y el importe cuando el tipo de factor de traslado es tasa o cuota (CFDI33158) +- CONCEPIMPC05: Las retenciones de los impuestos de un concepto deben tener una base y ser mayor a cero (CFDI33154) +- CONCEPIMPC06: Las retenciones de los impuestos de un concepto deben tener un tipo de factor diferente de exento (CFDI33166) + + +## EmisorRegimenFiscal + +- REGFIS01: El régimen fiscal contenga un valor apropiado según el tipo de RFC emisor (CFDI33130 y CFDI33131) + + +## FechaComprobante + +Valida que la fecha del comprobante: + +- FECHA01: La fecha del comprobante cumple con el formato +- FECHA02: La fecha existe en el comprobante y es mayor que 2017-07-01 y menor que el futuro + - La fecha en el futuro se puede configurar a un valor determinado + - La fecha en el futuro es por defecto el momento de validación más una tolerancia + - La tolerancia puede ser configurada y es por defecto 300 segundos + + +## ReceptorResidenciaFiscal + +- RESFISC01: Si el RFC no es XEXX010101000, entonces la residencia fiscal no debe existir (CFDI33134) +- RESFISC02: Si el RFC sí es XEXX010101000 y existe el complemento de comercio exterior, entonces la residencia fiscal debe establecerse y no puede ser "MEX" (CFDI33135 y CFDI33136) +- RESFISC03: Si el RFC sí es XEXX010101000 y se registró el número de registro de identificación fiscal, entonces la residencia fiscal debe establecerse y no puede ser "MEX" (CFDI33135 y CFDI33136) + + +## SelloDigitalCertificado + +Valida el Sello del comprobante y el Certificado + +- SELLO01: Se puede obtener el certificado del comprobante +- SELLO02: El número de certificado del comprobante igual al encontrado en el certificado +- SELLO03: El RFC del comprobante igual al encontrado en el certificado +- SELLO04: El nombre del emisor del comprobante igual al encontrado en el certificado +- SELLO05: La fecha del documento es mayor o igual a la fecha de inicio de vigencia del certificado +- SELLO06: La fecha del documento menor o igual a la fecha de fin de vigencia del certificado +- SELLO07: El sello del comprobante está en base 64 +- SELLO08: El sello del comprobante coincide con el certificado y la cadena de origen generada + + +## SumasConceptosComprobanteImpuestos + +Obtiene las sumas de los importes de los conceptos y las sumas agrupadas de los impuestos y las valida contra la información del comprobante y el nodo principal de impuestos. + +- SUMAS01: La suma de los importes de conceptos es igual a el subtotal del comprobante +- SUMAS02: La suma de los descuentos es igual a el descuento del comprobante +- SUMAS03: El cálculo del total es igual a el total del comprobante +- SUMAS04: El cálculo de impuestos trasladados es igual a el total de impuestos trasladados +- SUMAS05: Todos los impuestos trasladados existen en el comprobante +- SUMAS06: Todos los valores de los impuestos trasladados conciden con el comprobante +- SUMAS07: No existen más nodos de impuestos trasladados en el comprobante de los que se han calculado +- SUMAS08: El cálculo de impuestos retenidos es igual a el total de impuestos retenidos +- SUMAS09: Todos los impuestos retenidos existen en el comprobante +- SUMAS10: Todos los valores de los impuestos retenidos conciden con el comprobante +- SUMAS11: No existen más nodos de impuestos trasladados en el comprobante de los que se han calculado +- SUMAS12: El cálculo del descuento debe ser menor o igual al cálculo del subtotal + + +## TimbreFiscalDigitalSello + +Posiblemente este es el **validador más importante** porque se encarga de comprobar que +el CFDI no fue modificado después de haber sido sellado. + +- TFDSELLO01: El Sello SAT del Timbre Fiscal Digital corresponde al certificado SAT + +Esto lo hace de la siguiente forma: + +1. Obtiene el TimbreFiscalDigital, si no existe entonces no hay qué validar. +1. Corrobora que sea versión 1.1, si no lo es entonces no hay qué validar +1. Se asegura que cuente con SelloCFD y que coincida con el Sello del comprobante. +1. Se asegura que NoCertificadoSAT contenga un número válido. +1. Obtiene el certificado con el que fue sellado desde el sitio del SAT `https://rdc.sat.gob.mx/`. + Si no se pudo obtener entonces el resultado será de error. +1. Fabrica la cadena de origen del TimbreFiscalDigital. +1. Verifica que el sello corresponde con la cadena de origen usando el certificado. + +Es posible que un emisor intente modificar el comprobante, simplemente debe alterar el contenido +sin modificar el TimbreFiscalDigital ni el atributo Sello del comprobante. +En ese caso este validador no marcará error, pero sí lo hará el validador `SelloDigitalCertificado` +al encontrar que el Sello del comprobante no coincide con la cadena de origen. + + +## TimbreFiscalDigitalVersion + +- TFDVERSION01: Si existe el complemento timbre fiscal digital, entonces su versión debe ser 1.1 diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..738fcadd --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,14 @@ +site_name: CfdiUtils +site_description: Librería de utilerías comunes para trabajar con PHP y CFDI 3.2 & 3.3 (México) + +theme: readthedocs + +markdown_extensions: + - toc: + permalink:  + - admonition + - def_list + - fenced_code + +copyright: Copyright © MIT License 2017 - 2018 Carlos C Soto & contibutors + diff --git a/package.json b/package.json new file mode 100644 index 00000000..6c9d2d1c --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "devDependencies": { + "markdownlint-cli": "^0.10.0" + } +} diff --git a/src/CfdiUtils/Elements/Cfdi33/CfdiRelacionados.php b/src/CfdiUtils/Elements/Cfdi33/CfdiRelacionados.php index f2da1e38..0c627082 100644 --- a/src/CfdiUtils/Elements/Cfdi33/CfdiRelacionados.php +++ b/src/CfdiUtils/Elements/Cfdi33/CfdiRelacionados.php @@ -16,4 +16,12 @@ public function addCfdiRelacionado(array $attributes = []): CfdiRelacionado $this->addChild($cfdiRelacionado); return $cfdiRelacionado; } + + public function multiCfdiRelacionado(array $elementAttributes = []): self + { + foreach ($elementAttributes as $attributes) { + $this->addCfdiRelacionado($attributes); + } + return $this; + } } diff --git a/src/CfdiUtils/Elements/Cfdi33/Comprobante.php b/src/CfdiUtils/Elements/Cfdi33/Comprobante.php index 1270a2b8..58fac88e 100644 --- a/src/CfdiUtils/Elements/Cfdi33/Comprobante.php +++ b/src/CfdiUtils/Elements/Cfdi33/Comprobante.php @@ -14,9 +14,28 @@ public function getElementName(): string return 'cfdi:Comprobante'; } - public function getCfdiRelacionados(array $attributes = []): CfdiRelacionados + /** + * @todo Remove this deprecation error on version 3.0.0 + * @return CfdiRelacionados + */ + public function getCfdiRelacionados(): CfdiRelacionados { - return $this->helperGetOrAdd(new CfdiRelacionados($attributes)); + $arguments = func_get_args(); + if (count($arguments) > 0) { + trigger_error( + 'El método getCfdiRelacionados ya no admite atributos, use addCfdiRelacionados en su lugar', + E_USER_NOTICE + ); + return $this->addCfdiRelacionados($arguments[0]); + } + return $this->helperGetOrAdd(new CfdiRelacionados()); + } + + public function addCfdiRelacionados(array $attributes = []): CfdiRelacionados + { + $cfdiRelacionados = $this->getCfdiRelacionados(); + $cfdiRelacionados->addAttributes($attributes); + return $cfdiRelacionados; } public function addCfdiRelacionado(array $attributes = []): CfdiRelacionado @@ -24,6 +43,12 @@ public function addCfdiRelacionado(array $attributes = []): CfdiRelacionado return $this->getCfdiRelacionados()->addCfdiRelacionado($attributes); } + public function multiCfdiRelacionado(array ...$elementAttributes): self + { + $this->getCfdiRelacionados()->multiCfdiRelacionado($elementAttributes); + return $this; + } + public function getEmisor(): Emisor { return $this->helperGetOrAdd(new Emisor()); diff --git a/src/CfdiUtils/Utils/Rfc.php b/src/CfdiUtils/Utils/Rfc.php index f25ce9a7..a7319c48 100644 --- a/src/CfdiUtils/Utils/Rfc.php +++ b/src/CfdiUtils/Utils/Rfc.php @@ -15,11 +15,19 @@ class Rfc /** @var int */ private $length; + /** @var string contains calculated checksum */ + private $checkSum; + + /** @var bool */ + private $checkSumMatch; + public function __construct(string $rfc, int $flags = 0) { $this->checkIsValid($rfc, $flags); $this->rfc = $rfc; - $this->length = mb_strlen($this->rfc); + $this->length = mb_strlen($rfc); + $this->checkSum = static::obtainCheckSum($rfc); + $this->checkSumMatch = ($this->checkSum === (string) substr($rfc, -1)); } public function rfc(): string @@ -47,6 +55,16 @@ public function isForeign(): bool return (static::RFC_FOREIGN === $this->rfc); } + public function checkSum(): string + { + return $this->checkSum; + } + + public function checkSumMatch(): bool + { + return $this->checkSumMatch; + } + public function __toString(): string { return $this->rfc(); @@ -91,13 +109,6 @@ public static function checkIsValid(string $value, int $flags = 0) if (0 === static::obtainDate($value)) { throw new \UnexpectedValueException('La fecha obtenida no es lógica'); } - if (! in_array($value, [static::RFC_FOREIGN, static::RFC_GENERIC], true)) { - $last = substr($value, -1); - $expected = static::obtainCheckSum($value); - if ($last !== $expected) { - throw new \UnexpectedValueException("El dígito verificador no coincide, debería ser $expected"); - } - } } public static function obtainCheckSum(string $rfc): string diff --git a/tests/CfdiUtilsTests/CreateComprobanteCaseTest.php b/tests/CfdiUtilsTests/CreateComprobanteCaseTest.php index 5b3ff0b5..9a32bd03 100644 --- a/tests/CfdiUtilsTests/CreateComprobanteCaseTest.php +++ b/tests/CfdiUtilsTests/CreateComprobanteCaseTest.php @@ -111,9 +111,6 @@ public function testCreateCfdiUsingComprobanteElement() // validate the comprobante and check it has no errors or warnings $asserts = $creator->validate(); - // EMISORRFC01 - The RFC AAA010101AAA used for testing is not valid ! - $asserts->removeByCode('EMISORRFC01'); - $this->assertFalse($asserts->hasErrors()); $this->assertFalse($asserts->hasStatus(Status::warn())); diff --git a/tests/CfdiUtilsTests/Elements/Cfdi33/ComprobanteGetCfdiRelacionadosTest.php b/tests/CfdiUtilsTests/Elements/Cfdi33/ComprobanteGetCfdiRelacionadosTest.php new file mode 100644 index 00000000..26ba9425 --- /dev/null +++ b/tests/CfdiUtilsTests/Elements/Cfdi33/ComprobanteGetCfdiRelacionadosTest.php @@ -0,0 +1,60 @@ +{'getCfdiRelacionados'}([]); to avoid phpstan errors + * + */ + +class ComprobanteGetCfdiRelacionadosTest extends TestCase +{ + private $errors = []; + + protected function setUp() + { + parent::setUp(); + set_error_handler([$this, 'errorHandler'], E_USER_ERROR | E_USER_WARNING | E_USER_NOTICE | E_USER_DEPRECATED); + } + + public function errorHandler($errno, $errstr, $errfile, $errline, $errcontext) + { + $this->errors[] = compact('errno', 'errstr', 'errfile', 'errline', 'errcontext'); + } + + public function testGetCfdiRelacionadoDontTriggerErrorsWhenCallWithoutArgument() + { + $comprobante = new Comprobante(); + $comprobante->getCfdiRelacionados(); + $this->assertCount(0, $this->errors); + } + + public function testErrorWhenPassAnArrayAsArgument() + { + $comprobante = new Comprobante(); + $comprobante->{'getCfdiRelacionados'}([]); + $this->assertCount(1, $this->errors); + + $expectedError = [ + 'errno' => E_USER_NOTICE, + 'errstr' => 'El método getCfdiRelacionados ya no admite atributos, use addCfdiRelacionados en su lugar', + ]; + + $this->assertArraySubset($expectedError, $this->errors[0]); + } + + public function testStillIsWorkingWhenPassAnArrayAsArgument() + { + $comprobante = new Comprobante(); + $cfdiRelacionados = $comprobante->{'getCfdiRelacionados'}(['foo' => 'bar']); + $this->assertSame('bar', $cfdiRelacionados['foo']); + } +} diff --git a/tests/CfdiUtilsTests/Elements/Cfdi33/Helpers/SumasConceptosWriterTest.php b/tests/CfdiUtilsTests/Elements/Cfdi33/Helpers/SumasConceptosWriterTest.php index 73ae3da9..2cb349c5 100644 --- a/tests/CfdiUtilsTests/Elements/Cfdi33/Helpers/SumasConceptosWriterTest.php +++ b/tests/CfdiUtilsTests/Elements/Cfdi33/Helpers/SumasConceptosWriterTest.php @@ -164,14 +164,37 @@ public function testDescuentoWithValueZeroExistsIfAConceptoHasDescuento() public function testDescuentoNotSetIfAllConceptosDoesNotHaveDescuento() { $comprobante = new Comprobante(['Descuento' => '']); // set value with discount - $comprobante->addConcepto([]); // first concepto does not have Descuento - $comprobante->addConcepto(); // second concepto has Descuento + $comprobante->addConcepto(); // first concepto does not have Descuento + $comprobante->addConcepto(); // second concepto does not have Descuento neither $precision = 2; $sumasConceptos = new SumasConceptos($comprobante, $precision); $writer = new SumasConceptosWriter($comprobante, $sumasConceptos, $precision); $writer->put(); + // the Comprobante@Descuento attribute must not exists since there is no Descuento in concepts $this->assertFalse(isset($comprobante['Descuento'])); } + + public function testOnComplementoImpuestosImporteSumIsRounded() + { + $comprobante = new Comprobante(); + $comprobante->addConcepto()->addTraslado( + ['Importe' => '7.777777', 'Impuesto' => '002', 'TipoFactor' => 'Tasa', 'TasaOCuota' => '0.160000'] + ); + $comprobante->addConcepto()->addTraslado( + ['Importe' => '2.222222', 'Impuesto' => '002', 'TipoFactor' => 'Tasa', 'TasaOCuota' => '0.160000'] + ); + + $precision = 3; + $sumasConceptos = new SumasConceptos($comprobante, $precision); + $writer = new SumasConceptosWriter($comprobante, $sumasConceptos, $precision); + $writer->put(); + + $this->assertSame('10.000', $comprobante->searchAttribute('cfdi:Impuestos', 'TotalImpuestosTrasladados')); + $this->assertSame( + '10.000', + $comprobante->searchAttribute('cfdi:Impuestos', 'cfdi:Traslados', 'cfdi:Traslado', 'Importe') + ); + } } diff --git a/tests/CfdiUtilsTests/SumasConceptos/SumasConceptosTest.php b/tests/CfdiUtilsTests/SumasConceptos/SumasConceptosTest.php index 4accbadd..e1706610 100644 --- a/tests/CfdiUtilsTests/SumasConceptos/SumasConceptosTest.php +++ b/tests/CfdiUtilsTests/SumasConceptos/SumasConceptosTest.php @@ -137,4 +137,21 @@ public function testFoundAnyConceptWithDiscount() $comprobante->addConcepto(['Importe' => '333.33', 'Descuento' => '']); $this->assertTrue((new SumasConceptos($comprobante))->foundAnyConceptWithDiscount()); } + + public function testImpuestoImporteWithMoreDecimalsThanThePrecisionIsRounded() + { + $comprobante = new Comprobante(); + $comprobante->addConcepto()->addTraslado( + ['Importe' => '7.777777', 'Impuesto' => '002', 'TipoFactor' => 'Tasa', 'TasaOCuota' => '0.160000'] + ); + $comprobante->addConcepto()->addTraslado( + ['Importe' => '2.222222', 'Impuesto' => '002', 'TipoFactor' => 'Tasa', 'TasaOCuota' => '0.160000'] + ); + + $sumas = new SumasConceptos($comprobante, 3); + + $this->assertTrue($sumas->hasTraslados()); + $this->assertEquals(10.0, $sumas->getImpuestosTrasladados(), '', 0.0001); + $this->assertEquals(10.0, $sumas->getTraslados()['002:Tasa:0.160000']['Importe'], '', 0.0000001); + } } diff --git a/tests/CfdiUtilsTests/Utils/RfcTest.php b/tests/CfdiUtilsTests/Utils/RfcTest.php index f1ede247..80cf186d 100644 --- a/tests/CfdiUtilsTests/Utils/RfcTest.php +++ b/tests/CfdiUtilsTests/Utils/RfcTest.php @@ -15,6 +15,8 @@ public function testCreateRfcPerson() $this->assertFalse($rfc->isForeign()); $this->assertFalse($rfc->isMoral()); $this->assertTrue($rfc->isPerson()); + $this->assertSame('A', $rfc->checkSum()); + $this->assertTrue($rfc->checkSumMatch()); } public function testCreateRfcMoral() @@ -76,9 +78,9 @@ public function testCreateBadDate() public function testCreateBadDigit() { - $this->expectException(\UnexpectedValueException::class); - $this->expectExceptionMessage('dígito verificador'); - new Rfc('COSC8001137N9'); + $rfc = new Rfc('COSC8001137N9'); + $this->assertSame('A', $rfc->checkSum()); + $this->assertFalse($rfc->checkSumMatch()); } public function testIsValid() @@ -88,7 +90,7 @@ public function testIsValid() public function testIsNotValid() { - $this->assertFalse(Rfc::isValid('COSC8001137N9')); + $this->assertFalse(Rfc::isValid('COSC8099137NA')); } public function testWithMultiByte() diff --git a/tests/CfdiUtilsTests/Validate/Cfdi33/RecepcionPagos/CfdiRelacionadosTest.php b/tests/CfdiUtilsTests/Validate/Cfdi33/RecepcionPagos/CfdiRelacionadosTest.php index ae113f7c..7498468b 100644 --- a/tests/CfdiUtilsTests/Validate/Cfdi33/RecepcionPagos/CfdiRelacionadosTest.php +++ b/tests/CfdiUtilsTests/Validate/Cfdi33/RecepcionPagos/CfdiRelacionadosTest.php @@ -18,7 +18,7 @@ protected function setUp() public function testValidTipoRelacion() { $comprobante = $this->getComprobante(); - $comprobante->getCfdiRelacionados(['TipoRelacion' => '04']); + $comprobante->addCfdiRelacionados(['TipoRelacion' => '04']); $this->runValidate(); @@ -28,7 +28,7 @@ public function testValidTipoRelacion() public function testInvalidTipoRelacion() { $comprobante = $this->getComprobante(); - $comprobante->getCfdiRelacionados(['TipoRelacion' => 'XX']); + $comprobante->addCfdiRelacionados(['TipoRelacion' => 'XX']); $this->runValidate(); diff --git a/tests/CfdiUtilsTests/Validate/Cfdi33/RecepcionPagos/Pagos/BancoBeneficiarioRfcCorrectoTest.php b/tests/CfdiUtilsTests/Validate/Cfdi33/RecepcionPagos/Pagos/BancoBeneficiarioRfcCorrectoTest.php index 4d69dd02..032e09be 100644 --- a/tests/CfdiUtilsTests/Validate/Cfdi33/RecepcionPagos/Pagos/BancoBeneficiarioRfcCorrectoTest.php +++ b/tests/CfdiUtilsTests/Validate/Cfdi33/RecepcionPagos/Pagos/BancoBeneficiarioRfcCorrectoTest.php @@ -26,7 +26,7 @@ public function testValid($rfc) /** * @param string|null $rfc - * @testWith ["COSC8001137N1"] + * @testWith ["COSC8099137N1"] * ["XAXX010101000"] * [""] */ diff --git a/tests/CfdiUtilsTests/Validate/Cfdi33/RecepcionPagos/Pagos/BancoOrdenanteRfcCorrectoTest.php b/tests/CfdiUtilsTests/Validate/Cfdi33/RecepcionPagos/Pagos/BancoOrdenanteRfcCorrectoTest.php index b7c52b83..9a415582 100644 --- a/tests/CfdiUtilsTests/Validate/Cfdi33/RecepcionPagos/Pagos/BancoOrdenanteRfcCorrectoTest.php +++ b/tests/CfdiUtilsTests/Validate/Cfdi33/RecepcionPagos/Pagos/BancoOrdenanteRfcCorrectoTest.php @@ -26,7 +26,7 @@ public function testValid($rfc) /** * @param string|null $rfc - * @testWith ["COSC8001137N1"] + * @testWith ["COSC8099137N1"] * ["XAXX010101000"] * [""] */ diff --git a/tests/CfdiUtilsTests/Validate/Cfdi33/Standard/EmisorRfcTest.php b/tests/CfdiUtilsTests/Validate/Cfdi33/Standard/EmisorRfcTest.php index 050f54ac..9337c066 100644 --- a/tests/CfdiUtilsTests/Validate/Cfdi33/Standard/EmisorRfcTest.php +++ b/tests/CfdiUtilsTests/Validate/Cfdi33/Standard/EmisorRfcTest.php @@ -45,10 +45,9 @@ public function providerInvalidCases() return [ 'none' => [null], 'empty' => [''], - 'wrong' => ['COSC8001137N0'], + 'wrong' => ['COSC8099137NA'], 'generic' => [Rfc::RFC_GENERIC], 'foreign' => [Rfc::RFC_FOREIGN], - 'testing' => ['AAA010101AAA'], ]; } diff --git a/tests/CfdiUtilsTests/Validate/Cfdi33/Standard/ReceptorRfcTest.php b/tests/CfdiUtilsTests/Validate/Cfdi33/Standard/ReceptorRfcTest.php index 3bcec490..835952a9 100644 --- a/tests/CfdiUtilsTests/Validate/Cfdi33/Standard/ReceptorRfcTest.php +++ b/tests/CfdiUtilsTests/Validate/Cfdi33/Standard/ReceptorRfcTest.php @@ -47,7 +47,7 @@ public function providerInvalidCases() return [ 'none' => [null], 'empty' => [''], - 'wrong' => ['COSC8001137N0'], + 'wrong' => ['COSC8099137NA'], ]; }