Generate Odoo abstract models (mixins) from XSD schemas using xsdata. Heavily inspired by xsdata-plantuml.
Video Tutorial: YouTube - xsdata-odoo Overview
- Install
- Quick Start
- Real-World Usage
- Architecture Pattern
- Configuration
- Advanced Examples
- Field Prefixing Strategy
- Custom Filters
- Troubleshooting
$ pip install xsdata[cli]
$ pip install git+https://github.com/akretion/xsdata-odoo$ xsdata generate tests/fixtures/po/po.xsd --output=odoo
Parsing schema po.xsd
Compiling schema po.xsd
Builder: 6 main and 1 inner classes
Analyzer input: 6 main and 1 inner classes
Analyzer output: 5 main and 1 inner classes
Generating package: generated.po$ export XSDATA_SCHEMA=nfe
$ export XSDATA_VERSION=40
$ export XSDATA_SKIP="^ICMS.ICMS\d+|^ICMS.ICMSSN\d+"
$ export XSDATA_LANG="portuguese"
$ xsdata generate nfelib/nfe/schemas/v4_0 \
--package nfelib.nfe.odoo.v4_0 \
--output=odooxsdata-odoo is used in production by the OCA Brazilian Localization (l10n-brazil) to generate models for complex fiscal documents:
| Document | Fields | Model | Version | OCA Module |
|---|---|---|---|---|
| NF-e (Nota Fiscal Eletrônica) | 800+ | 55/65 | 4.0 | l10n_br_nfe_spec |
| CT-e (Transport Document) | 1000+ | 57 | 4.0 | l10n_br_cte_spec |
| MDF-e (Manifest) | 500+ | 58 | 3.0 | l10n_br_mdfe_spec |
| SPED (Fiscal Reports) | Variable | - | - | l10n_br_sped_* |
- NF-e: 800+ fields across multiple hierarchical structures (identification, items, taxes, transport, payment)
- CT-e: Multiple transport modes (road, air, waterway, rail, pipeline, multimodal) with specific fields each
- MDF-e: Aggregation of multiple NF-e/CT-e documents with municipal grouping
- SPED: Complex register hierarchies with parent-child relationships
Integration with nfelib: xsdata-odoo generates Odoo models, while nfelib handles XML serialization. Both use the same XSD schemas for consistency.
The OCA l10n-brazil uses a proven two-layer architecture:
- 100% generated from XSD schemas
- Abstract models (mixins) with field definitions
- No business logic
- Versioned per schema version (nfe40, cte40, mdfe30)
- Inherits from spec modules
- Maps to Odoo fiscal documents (
l10n_br_fiscal.document) - Business rules and validations
- Web service communication (SEFAZ)
- User interface
- Schema Updates: Regenerate spec modules without touching business logic
- Version Migration: Different schema versions coexist (nfe40 vs nfe50)
- Testing: Business logic tested separately from generated code
- Maintainability: Clear separation of concerns
| Variable | Description | Default |
|---|---|---|
XSDATA_SCHEMA |
Schema name | spec |
XSDATA_VERSION |
Schema version | 10 |
XSDATA_SKIP |
Regex patterns to skip (pipe-separated) | [] |
XSDATA_LANG |
Language for text processing | "" |
XSDATA_MONETARY_TYPE |
XSD type to force fields.Monetary |
"" |
XSDATA_NUM_TYPE |
XSD type for numeric detection | TDec_[5:7.7:9] |
XSDATA_CURRENCY_FIELD |
Currency field name | currency_id |
XSDATA_GENDS |
GenerateDS compatibility mode | false |
from xsdata_odoo import get_config
config = get_config()
print(config.schema) # "spec"
print(config.version) # "10"
print(config.field_safe_prefix) # "spec10_"
print(config.inherit_model) # "spec.mixin.spec"The NF-e schema defines ICMS taxes with multiple groups (ICMS00, ICMS10, ICMS40, etc.) that share field names. Skip them to avoid conflicts:
export XSDATA_SCHEMA=nfe
export XSDATA_VERSION=40
export XSDATA_SKIP="^ICMS.ICMS\d+|^ICMS.ICMSSN\d+"
export XSDATA_LANG="portuguese"
export XSDATA_CURRENCY_FIELD="brl_currency_id"
xsdata generate nfelib/nfe/schemas/v4_0 \
--package nfelib.nfe.odoo.v4_0 \
--output=odoo
# Move generated files to your module
mv nfelib/odoo/nfe/v4_0/* your_addon/models/v4_0/export XSDATA_SCHEMA=cte
export XSDATA_VERSION=40
export XSDATA_SKIP="^ICMS\d+|^ICMSSN+|ICMSOutraUF|ICMSUFFim|INFESPECIE_TPESPECIE"
export XSDATA_LANG="portuguese"
xsdata generate nfelib/cte/schemas/v4_0 \
--package nfelib.cte.odoo.v4_0 \
--output=odooexport XSDATA_SCHEMA=mdfe
export XSDATA_VERSION=30
export XSDATA_LANG="portuguese"
xsdata generate nfelib/mdfe/schemas/v3_0 \
--package nfelib.mdfe.odoo.v3_0 \
--output=odooFor complex non-XML fiscal reports with register hierarchies:
from xsdata_odoo.generator import OdooGenerator
from xsdata_odoo.filters import OdooFilters
class SpedFilters(OdooFilters):
def registry_name(self, name="", parents=[], type_names=[]):
# Custom naming: schema.version.register_code
name = self.class_name(name)
return f"{self.schema}.{self.version}.{name[-4:].lower()}"
def class_properties(self, obj, parents):
# Add custom metadata
register = lookup_register(obj.name)
return f"_sped_level = {register['level']}"With 800+ fields in NF-e alone, plus thousands of OCA modules, field name collisions are a real risk. Additionally, schemas evolve (3.0 → 4.0 → 5.0).
Each field gets a prefix combining schema name + version digits:
- NF-e v4.0:
nfe40_prefix →nfe40_vBC,nfe40_vICMS - CT-e v4.0:
cte40_prefix →cte40_vTPrest - MDF-e v3.0:
mdfe30_prefix →mdfe30_qNFe
| Scenario | Example | Database Impact |
|---|---|---|
| Minor update (4.00 → 4.01) | Same nfe40_ prefix |
Fields updated in place, --update sufficient |
| Major update (3.0 → 4.0) | nfe30_ → nfe40_ prefix |
New fields/tables, data migration needed |
from xsdata_odoo import get_config
config = get_config()
config.schema = "nfe"
config.version = "40"
print(config.field_safe_prefix) # "nfe40_"Extend OdooFilters for specialized use cases:
from xsdata_odoo.filters import OdooFilters
from collections import OrderedDict
class MyCustomFilters(OdooFilters):
def registry_name(self, name="", parents=[], type_names=[]):
# Custom model naming logic
return f"my_module.{self.version}.{name.lower()}"
def _extract_field_attributes(self, parents, attr):
# Add custom field attributes
kwargs = super()._extract_field_attributes(parents, attr)
# Add custom metadata
kwargs["my_custom_attr"] = True
return kwargs
# Use custom filters
generator = OdooGenerator(config)
generator.filters = MyCustomFilters(
config,
all_simple_types=[],
all_complex_types=[],
registry_names={},
implicit_many2ones={},
)
generator.filters.register(generator.env)Problem: XSD defines multiple groups with same field names (e.g., ICMS00.vBC, ICMS10.vBC).
Solution: Use XSDATA_SKIP to skip conflicting classes:
export XSDATA_SKIP="^ICMS.ICMS\d+|^ICMS.ICMSSN\d+"Then implement business logic in your implementation module to handle the different ICMS groups.
Problem: Monetary fields reference non-existent currency field.
Solution: Set XSDATA_CURRENCY_FIELD to match your Odoo module's currency field:
export XSDATA_CURRENCY_FIELD="company_currency_id" # or "currency_id", "brl_currency_id"Problem: Field labels not extracted properly from XSD documentation.
Solution: Set XSDATA_LANG for stopword processing:
export XSDATA_LANG="portuguese" # or "english", "spanish", etc.MIT License - See LICENSE file for details
2025 Akretion - Raphaël Valyi raphael.valyi@akretion.com