Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG_SCHEMAS.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,13 @@ commit message. A binding without a section here simply gets no metadata injecte
- published_at: `2026-02-09`
- source_url:
`https://www.gov.br/nfse/pt-br/biblioteca/documentacao-tecnica/documentacao-atual/nfse-esquemas_xsd-v1-01-20260209.zip`

## nfse (v1_01)

- binding_dir: `nfse/bindings/v1_01`
- version: `NFSe-ESQUEMAS_XSD-v1.01-20260209 (leiaute 1.01)`
- package: `NFSe-ESQUEMAS_XSD-v1.01-20260209, diretório Schemas/1.01`
- nota_tecnica: `Leiaute NFSe 1.01 (grupos IBSCBS da RTC, NT SE/CGNFS-e 004) + CNC`
- published_at: `2026-02-09`
- source_url:
`https://www.gov.br/nfse/pt-br/biblioteca/documentacao-tecnica/documentacao-atual/nfse-esquemas_xsd-v1-01-20260209.zip`
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,14 @@ to support the various versions at the same time using different folders. It
would be possible, for example, to issue the future NF-e 5.0 and still import
an NF-e 4.0.

The NFS-e is the exception to the 2 digits rule: its 1.01 layout (the IBSCBS
groups of the tax reform) cannot read every 1.00 document (`explRod` and other
groups are gone), and the government accepts both layouts at the same time. So
`nfse/bindings/v1_0` (layout 1.00) and `nfse/bindings/v1_01` (layout 1.01) live
side by side. They share the namespace and the root element names, so
`from_xml`/`from_path` return the classes of the package of the class you call
them on, and an `XmlParser` used directly should be given the target class.


## Credits

Expand Down
8 changes: 8 additions & 0 deletions generate_bindings.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,14 @@ def __init__(
schema_dir="nfelib/nfse/schemas/v1_0",
package="nfelib.nfse.bindings.v1_0",
),
# The NFS-e 1.01 layout is not a superset of 1.00 (explRod and other groups
# are gone) and both are valid at the same time, so it gets its own folder
# instead of superseding v1_0. The schema version folder keeps the 3 digits.
"nfse_v1_01": SchemaConfig(
name="nfse_v1_01",
schema_dir="nfelib/nfse/schemas/v1_01",
package="nfelib.nfse.bindings.v1_01",
),
}

DOWNLOAD_URLS: Final[dict[str, str]] = {
Expand Down
38 changes: 35 additions & 3 deletions nfelib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import base64
import os
import warnings
from io import BytesIO
from os import environ
from pathlib import Path
from typing import Any
Expand All @@ -29,9 +30,32 @@ class CommonMixin:
@classmethod
def from_xml(cls, xml: str, config: ParserConfig = None) -> Any:
"""Parse xml and return an instance of the class."""
if config is None:
return XmlParser().from_string(xml)
return XmlParser(config=config).from_string(xml)
parser = XmlParser() if config is None else XmlParser(config=config)
return parser.from_string(xml, cls._find_root_class(parser, xml))

@classmethod
def _find_root_class(cls, parser: XmlParser, xml: str | bytes) -> Any:
"""Return the root element class from the binding package of cls.

Two layout versions of a document can share the namespace and the
element names (NFS-e 1.00 and 1.01). xsdata would then return the
class imported last, whatever the class from_xml was called on. When
several classes match the root, keep the one of the same package.
Return None to let xsdata detect the root class as usual.
"""
data = xml.encode("utf-8") if isinstance(xml, str) else xml
try:
_event, root = next(etree.iterparse(BytesIO(data), events=("start",)))
except (etree.XMLSyntaxError, StopIteration):
return None
candidates: list[type] = parser.context.find_types(root.tag)
if len(candidates) < 2:
return None
package = cls.__module__.rpartition(".")[0]
for candidate in reversed(candidates):
if candidate.__module__.rpartition(".")[0] == package:
return candidate
return None

@classmethod
def from_path(cls, path: str) -> Any:
Expand Down Expand Up @@ -73,6 +97,14 @@ def _get_schema_path(cls) -> str:
"nfe_v4.00.xsd",
)
if package == "nfse":
if ".bindings.v1_01." in cls.__module__:
return os.path.join(
os.path.dirname(__file__),
"nfse",
"schemas",
"v1_01",
"DPS_v1.01.xsd",
)
return os.path.join(
os.path.dirname(__file__),
"nfse",
Expand Down
Loading
Loading