Skip to content

Commit

Permalink
Add CFDI v4 support (#9)
Browse files Browse the repository at this point in the history
* Creating schema v4.0

* Adding new tests

* Update version
  • Loading branch information
peguerosdc authored Jul 15, 2022
1 parent 7dc63bf commit 0606e91
Show file tree
Hide file tree
Showing 24 changed files with 1,272 additions and 260 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ docs/_autosummary
# Pyre type checker
.pyre/

# examples and cfdis to test
# examples and schemas to test
data

# from pycharm
Expand Down
24 changes: 18 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@
[![PyPI Latest Release](https://img.shields.io/pypi/v/cfdibills.svg)](https://pypi.org/project/cfdibills/)
[![codecov](https://codecov.io/gh/peguerosdc/cfdibills/branch/main/graph/badge.svg?token=IE6CNFJJMQ)](https://codecov.io/gh/peguerosdc/cfdibills)

Utility to inspect and verify CFDI (Mexican invoice) versions 3.3 and 4.0
Utility to parse CFDI (Mexican invoice) versions 3.3 and 4.0 and validate their status against the SAT.

## Features

* Load a CFDI in XML format into a [pydantic](https://github.com/samuelcolvin/pydantic) object
* CFDIs are validated against the XSD schema, but a thorough check (i.e. conditional values) is not performed.
* Query the status of a CFDI via SAT's web service
* Only presence of required fields is validated, but this package doesn't perform a thorough validation of the CFDI
standard.
* **DOESN'T REQUIRE** additional dependencies to read the XML like libxml2-dev, libxslt-dev


Expand All @@ -23,12 +22,12 @@ pip install cfdibills

## Examples

You can load a verify a bill directly from its XML:
You can load and verify a bill directly from its XML:

````python
import cfdibills

cfdi = cfdibills.read_xml("path/to/invoice.xml")
cfdi = cfdibills.read_xml("path/to/bill.xml")
status = cfdibills.verify(cfdi)
````

Expand All @@ -37,9 +36,22 @@ Or you can verify it manually:
````python
import cfdibills

cfdibills.verify(uuid="folio fiscal", rfc_emisor="re", rfc_receptor="rr", total_facturado=150.00)
status = cfdibills.verify(uuid="folio fiscal", rfc_emisor="re", rfc_receptor="rr", total_facturado=150.00)
````

In both cases, `status` would look something like this:

````python
SATConsultaResponse(
codigo_estatus='S - Comprobante obtenido satisfactoriamente.',
es_cancelable='Cancelable con aceptación',
estado='Vigente',
estatus_cancelacion=None,
validacion_efos='200',
)
````


## Contributing

This repository uses [pre-commit](https://pre-commit.com/) to help developers perform almost the same validations as in
Expand Down
2 changes: 1 addition & 1 deletion cfdibills/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.2.0.a3
0.2.0.a4
26 changes: 16 additions & 10 deletions cfdibills/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
import pydantic
import xmltodict

from cfdibills.cfdis.cfdi33 import CFDI33
from cfdibills.errors import InvalidCFDIError, UnsupportedCFDIError
from cfdibills.schemas.cfdi33 import CFDI33
from cfdibills.schemas.cfdi40 import CFDI40

_name_pattern = re.compile(r"(.)([A-Z][a-z]+)")
_snake_pattern = re.compile(r"([a-z0-9])([A-Z])")
Expand All @@ -27,12 +28,19 @@ def _get_cfdi_with_version(candidate: dict) -> tuple[dict, str]:
return cfdi, version


def _parse_cfdi(cfdi: dict, version: str) -> CFDI33:
mapper = {"3.3": CFDI33}
def _xml_to_json(path: str, normalize: bool = True) -> dict:
with open(path, "rb") as f:
raw_xml = xmltodict.parse(f, dict_constructor=dict)
return normalize_dict_keys(raw_xml) if normalize else raw_xml


def _parse_cfdi(cfdi: dict, version: str) -> Union[CFDI33, CFDI40]:
mapper = {"3.3": CFDI33, "4.0": CFDI40}
if (parser := mapper.get(version, None)) is None:
raise UnsupportedCFDIError(f"Version '{version}' is not supported. It must be one of {mapper.keys()}.")
try:
parsed = parser.parse_obj(cfdi)
# Mypy doesn't know that the parser is also of type BaseModel, so we have to tell it to ignore this line
parsed = parser.parse_obj(cfdi) # type: ignore
except pydantic.ValidationError as e:
raise InvalidCFDIError(str(e)) from None
return parsed
Expand Down Expand Up @@ -103,7 +111,7 @@ def normalize_dict_keys(ugly_dict: dict) -> dict:
return result


def read_xml(path: str) -> CFDI33:
def read_xml(path: str) -> Union[CFDI33, CFDI40]:
"""
Reads a CFDI in a .xml and maps it to a pydantic object.
Expand All @@ -113,8 +121,8 @@ def read_xml(path: str) -> CFDI33:
Returns
-------
CFDI33
Pydantic version of the
Union[CFDI33, CFDI40]
Pydantic object of the CFDI
Raises
------
Expand All @@ -123,8 +131,6 @@ def read_xml(path: str) -> CFDI33:
UnsupportedCFDIError
If the CFDI version of the XML is not supported
"""
with open(path, "rb") as f:
raw_xml = xmltodict.parse(f, dict_constructor=dict)
normalized_xml = normalize_dict_keys(raw_xml)
normalized_xml = _xml_to_json(path)
cfdi, version = _get_cfdi_with_version(normalized_xml)
return _parse_cfdi(cfdi, version)
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""
CFDIs definition.
"""
from . import cfdi33, cfdi40
from .catalogs import *
from .cfdi33 import *
from .complementos import *
98 changes: 98 additions & 0 deletions cfdibills/cfdis/catalogs.py → cfdibills/schemas/catalogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -650,3 +650,101 @@ class TipoRelacion(str, Enum):
pagos_en_parcialidades = "08"
#: Factura Generada por Pagos Diferidos
pagos_diferidos = "09"


class RegimenFiscal(str, Enum):
"""
Catalog of "Regimen Fiscal".
http://www.sat.gob.mx/sitio_internet/cfd/catalogos/catCFDI.xsd
"""

r601 = "601"
r603 = "603"
r605 = "605"
r606 = "606"
r607 = "607"
r608 = "608"
r609 = "609"
r610 = "610"
r611 = "611"
r612 = "612"
r614 = "614"
r615 = "615"
r616 = "616"
r620 = "620"
r621 = "621"
r622 = "622"
r623 = "623"
r624 = "624"
r625 = "625"
r626 = "626"
r628 = "628"
r629 = "629"
r630 = "630"


class ObjetoImp(str, Enum):
"""
Catalog of "Objecto Imp".
http://www.sat.gob.mx/sitio_internet/cfd/catalogos/catCFDI.xsd
"""

o01 = "01"
o02 = "02"
o03 = "03"


class Periodicidad(str, Enum):
"""
Catalog of "Periodicidad".
http://www.sat.gob.mx/sitio_internet/cfd/catalogos/catCFDI.xsd
"""

p01 = "01"
p02 = "02"
p03 = "03"
p04 = "04"
p05 = "05"


class Meses(str, Enum):
"""
Catalog of "Meses".
http://www.sat.gob.mx/sitio_internet/cfd/catalogos/catCFDI.xsd
"""

m01 = "01"
m02 = "02"
m03 = "03"
m04 = "04"
m05 = "05"
m06 = "06"
m07 = "07"
m08 = "08"
m09 = "09"
m10 = "10"
m11 = "11"
m12 = "12"
m13 = "13"
m14 = "14"
m15 = "15"
m16 = "16"
m17 = "17"
m18 = "18"


class Exportacion(str, Enum):
"""
Catalog of "Exportacion".
http://www.sat.gob.mx/sitio_internet/cfd/catalogos/catCFDI.xsd
"""

e01 = "01"
e02 = "02"
e03 = "03"
e04 = "04"
Loading

0 comments on commit 0606e91

Please sign in to comment.