Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[15.0][MIG][l10n_br_mdfe_spec] #3583

Merged
merged 17 commits into from
Jan 16, 2025
Merged
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
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ exclude: |
# END NOT INSTALLABLE ADDONS
^l10n_br_nfe_spec/models/v4_0/| # (don't reformat generated code)
^l10n_br_cte_spec/models/v4_0/| # (don't reformat generated code)
^l10n_br_mdfe_spec/models/v3_0/| # (don't reformat generated code)
^spec_driven_model/tests/| # (tests include generated code)
# Files and folders generated by bots, to avoid loops
^setup/|/static/description/index\.html$|
114 changes: 114 additions & 0 deletions l10n_br_mdfe_spec/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
=========
mdfe spec
=========

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:1571bedf7688fbc9828182da1e7e54b95b89a340f85056bdceb00101dec72690
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png
:target: https://odoo-community.org/page/development-status
:alt: Alpha
.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fl10n--brazil-lightgray.png?logo=github
:target: https://github.com/OCA/l10n-brazil/tree/14.0/l10n_br_mdfe_spec
:alt: OCA/l10n-brazil
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/l10n-brazil-14-0/l10n-brazil-14-0-l10n_br_mdfe_spec
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/l10n-brazil&target_branch=14.0
:alt: Try me on Runboat

|badge1| |badge2| |badge3| |badge4| |badge5|

Este módulo contem a estrutura de dados do ​Manifesto Eletrônico de Documentos Fiscais (MDF-e).
Este módulo não faz nada sozinho, ele precisaria de um modulo `l10n_br_mdfe` que mapearia esses mixins
nos documentos fiscais Odoo de forma semlhante a forma como o módulo `l10n_br_nfe` faz como o módulo `l10n_br_nfe_spec`.

Este módulo inclui os leiautes persistantes dos modos de transporte do MDF-e:

* modo aéreo
* modo aquaviário
* modo ferroviário
* modo rodoviário



Geração
~~~~~~~

O código dos mixins Odoo desse módulo é 100% gerado a partir dos últimos esquemas xsd da Fazenda usando xsdata e essa extensão dele:

https://github.com/akretion/xsdata-odoo


O comando usado foi:

export 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=odoo

.. IMPORTANT::
This is an alpha version, the data model and design can change at any time without warning.
Only for development or testing purpose, do not use in production.
`More details on development status <https://odoo-community.org/page/development-status>`_

**Table of contents**

.. contents::
:local:

Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/OCA/l10n-brazil/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/l10n-brazil/issues/new?body=module:%20l10n_br_mdfe_spec%0Aversion:%2014.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

Do not contact contributors directly about support or help with technical issues.

Credits
=======

Authors
~~~~~~~

* Akretion

Contributors
~~~~~~~~~~~~

* Raphaël Valyi <raphael.valyi@akretion.com.br>

Maintainers
~~~~~~~~~~~

This module is maintained by the OCA.

.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org

OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.

.. |maintainer-rvalyi| image:: https://github.com/rvalyi.png?size=40px
:target: https://github.com/rvalyi
:alt: rvalyi

Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:

|maintainer-rvalyi|

This module is part of the `OCA/l10n-brazil <https://github.com/OCA/l10n-brazil/tree/14.0/l10n_br_mdfe_spec>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
1 change: 1 addition & 0 deletions l10n_br_mdfe_spec/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
14 changes: 14 additions & 0 deletions l10n_br_mdfe_spec/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "mdfe spec",
"version": "15.0.1.0.0",
"author": "Akretion, Odoo Community Association (OCA)",
"license": "LGPL-3",
"category": "Accounting",
"summary": "MDF-e abstract models generated by xsdata-odoo from the oficial xsd",
"depends": ["base"],
"installable": True,
"application": False,
"development_status": "Alpha",
"maintainers": ["rvalyi"],
"website": "https://github.com/OCA/l10n-brazil",
}
4,765 changes: 4,765 additions & 0 deletions l10n_br_mdfe_spec/i18n/l10n_br_mdfe_spec.pot

Large diffs are not rendered by default.

4,824 changes: 4,824 additions & 0 deletions l10n_br_mdfe_spec/i18n/pt_BR.po

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions l10n_br_mdfe_spec/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import spec_mixin
from . import v3_0
32 changes: 32 additions & 0 deletions l10n_br_mdfe_spec/models/spec_mixin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Copyright 2023-TODAY Akretion - Raphael Valyi <raphael.valyi@akretion.com>
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0.en.html).

from odoo import fields, models


class MdfeSpecMixin(models.AbstractModel):
_description = "Abstract Model"
_name = "spec.mixin.mdfe"
_mdfe30_odoo_module = (
"odoo.addons.l10n_br_mdfe_spec.models.v3_0.mdfe_tipos_basico_v3_00"
)
_mdfe30_binding_module = "nfelib.mdfe.bindings.v3_0.mdfe_tipos_basico_v3_00"

brl_currency_id = fields.Many2one(
comodel_name="res.currency",
string="Moeda",
# FIXME compute method is better, but not working in v14.
default=lambda self: self.env.ref("base.BRL"),
)

def _valid_field_parameter(self, field, name):
if name in (
"xsd_type",
"xsd_required",
"choice",
"xsd_implicit",
"xsd_choice_required",
):
return True
else:
return super()._valid_field_parameter(field, name)
5 changes: 5 additions & 0 deletions l10n_br_mdfe_spec/models/v3_0/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from . import mdfe_tipos_basico_v3_00
from . import mdfe_modal_aereo_v3_00
from . import mdfe_modal_aquaviario_v3_00
from . import mdfe_modal_ferroviario_v3_00
from . import mdfe_modal_rodoviario_v3_00
288 changes: 288 additions & 0 deletions l10n_br_mdfe_spec/models/v3_0/ev_pagto_oper_mdfe_v3_00.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
# Copyright 2023 Akretion - Raphaël Valyi <raphael.valyi@akretion.com>
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0.en.html).
# Generated by https://github.com/akretion/xsdata-odoo
#
import textwrap
from odoo import fields, models

__NAMESPACE__ = "http://www.portalfiscal.inf.br/mdfe"

"Tipo do Componente"
COMP_TPCOMP = [
("01", "Vale Pedágio"),
("02", "Impostos, taxas e contribuições"),
("03", "Despesas (bancárias, meios de pagamento, outras)"),
("99", "Outros"),
]

"Descrição do Evento - “Pagamento Operação MDF-e”"
EVPAGTOOPERMDFE_DESCEVENTO = [
("Pagamento Operação MDF-e", "Pagamento Operação MDF-e"),
("Pagamento Operacao MDF-e", "Pagamento Operacao MDF-e"),
]

"Indicador da Forma de Pagamento"
INFPAG_INDPAG = [
("0", "Pagamento à Vista"),
("1", "Pagamento à Prazo"),
]


class EvPagtoOperMdfe(models.AbstractModel):
"""Schema XML de validação do evento de pagamento da operação de transporte
110116"""

_description = textwrap.dedent(" %s" % (__doc__,))
_name = "mdfe.30.evpagtoopermdfe"
_inherit = "spec.mixin.mdfe"
_binding_type = "EvPagtoOperMdfe"

mdfe30_descEvento = fields.Selection(
EVPAGTOOPERMDFE_DESCEVENTO,
string="Descrição do Evento",
xsd_required=True,
help="Descrição do Evento - “Pagamento Operação MDF-e”",
)

mdfe30_nProt = fields.Char(
string="Número do Protocolo de Status do MDF-e",
xsd_required=True,
xsd_type="TProt",
help=(
"Número do Protocolo de Status do MDF-e. \n1 posição tipo de "
"autorizador (9 - SEFAZ Nacional ); \n2 posições ano;\n10 "
"seqüencial no ano."
),
)

mdfe30_infViagens = fields.Many2one(
comodel_name="mdfe.30.infviagens",
string="Informações do total",
xsd_required=True,
help=(
"Informações do total de viagens acobertadas pelo Evento "
"“pagamento do frete”"
),
)

mdfe30_infPag = fields.One2many(
"mdfe.30.evpagtoopermdfe_infpag",
"mdfe30_infPag_evPagtoOperMDFe_id",
string="Informações do Pagamento do Frete",
)


class InfViagens(models.AbstractModel):
"""Informações do total de viagens acobertadas pelo Evento “pagamento do
frete”"""

_description = textwrap.dedent(" %s" % (__doc__,))
_name = "mdfe.30.infviagens"
_inherit = "spec.mixin.mdfe"
_binding_type = "EvPagtoOperMdfe.InfViagens"

mdfe30_qtdViagens = fields.Char(
string="Quantidade total",
xsd_required=True,
help=("Quantidade total de viagens realizadas com o pagamento do Frete"),
)

mdfe30_nroViagem = fields.Char(
string="Número de referência da viagem",
xsd_required=True,
help="Número de referência da viagem do MDFe referenciado.",
)


class EvPagtoOperMdfeInfPag(models.AbstractModel):
"Informações do Pagamento do Frete"
_description = textwrap.dedent(" %s" % (__doc__,))
_name = "mdfe.30.evpagtoopermdfe_infpag"
_inherit = "spec.mixin.mdfe"
_binding_type = "EvPagtoOperMdfe.InfPag"

mdfe30_infPag_evPagtoOperMDFe_id = fields.Many2one(
comodel_name="mdfe.30.evpagtoopermdfe", xsd_implicit=True, ondelete="cascade"
)
mdfe30_xNome = fields.Char(
string="Razão social ou Nome do responsavel",
help="Razão social ou Nome do responsavel pelo pagamento",
)

mdfe30_CPF = fields.Char(
string="Número do CPF do responsável pelo pgto",
choice="infpag",
xsd_choice_required=True,
xsd_type="TCpf",
help=(
"Número do CPF do responsável pelo pgto\nInformar os zeros não "
"significativos."
),
)

mdfe30_CNPJ = fields.Char(
string="Número do CNPJ do responsável pelo pgto",
choice="infpag",
xsd_choice_required=True,
xsd_type="TCnpjOpc",
help=(
"Número do CNPJ do responsável pelo pgto\nInformar os zeros não "
"significativos."
),
)

mdfe30_idEstrangeiro = fields.Char(
string="Identificador do responsável pelo pgto",
choice="infpag",
xsd_choice_required=True,
help=("Identificador do responsável pelo pgto em caso de ser estrangeiro"),
)

mdfe30_comp = fields.One2many(
"mdfe.30.evpagtoopermdfe_comp",
"mdfe30_Comp_infPag_id",
string="Componentes do Pagamentoi do Frete",
)

mdfe30_vContrato = fields.Monetary(
string="Valor Total do Contrato",
xsd_required=True,
xsd_type="TDec_1302",
currency_field="brl_currency_id",
)

mdfe30_indPag = fields.Selection(
INFPAG_INDPAG,
string="Indicador da Forma",
xsd_required=True,
help=(
"Indicador da Forma de Pagamento:0-Pagamento à Vista;1-Pagamento à"
" Prazo;"
),
)

mdfe30_vAdiant = fields.Monetary(
string="Valor do Adiantamento",
xsd_type="TDec_1302",
currency_field="brl_currency_id",
help="Valor do Adiantamento (usar apenas em pagamento à Prazo",
)

mdfe30_infPrazo = fields.One2many(
"mdfe.30.evpagtoopermdfe_infprazo",
"mdfe30_infPrazo_infPag_id",
string="Informações do pagamento a prazo",
help=(
"Informações do pagamento a prazo.\nInformar somente se indPag for"
" à Prazo"
),
)

mdfe30_infBanc = fields.Many2one(
comodel_name="mdfe.30.evpagtoopermdfe_infbanc",
string="Informações bancárias",
xsd_required=True,
)


class EvPagtoOperMdfeComp(models.AbstractModel):
"Componentes do Pagamentoi do Frete"
_description = textwrap.dedent(" %s" % (__doc__,))
_name = "mdfe.30.evpagtoopermdfe_comp"
_inherit = "spec.mixin.mdfe"
_binding_type = "EvPagtoOperMdfe.InfPag.Comp"

mdfe30_Comp_infPag_id = fields.Many2one(
comodel_name="mdfe.30.evpagtoopermdfe_infpag",
xsd_implicit=True,
ondelete="cascade",
)
mdfe30_tpComp = fields.Selection(
COMP_TPCOMP,
string="Tipo do Componente",
xsd_required=True,
help=(
"Tipo do Componente\n01 - Vale Pedágio; \n02 - Impostos, taxas e "
"contribuições; \n03 - Despesas (bancárias, meios de pagamento, "
"outras)\n; 99 - Outros"
),
)

mdfe30_vComp = fields.Monetary(
string="Valor do componente",
xsd_required=True,
xsd_type="TDec_1302",
currency_field="brl_currency_id",
)

mdfe30_xComp = fields.Char(string="Descrição do componente do tipo Outros")


class EvPagtoOperMdfeInfPrazo(models.AbstractModel):
"""Informações do pagamento a prazo.
Informar somente se indPag for à Prazo"""

_description = textwrap.dedent(" %s" % (__doc__,))
_name = "mdfe.30.evpagtoopermdfe_infprazo"
_inherit = "spec.mixin.mdfe"
_binding_type = "EvPagtoOperMdfe.InfPag.InfPrazo"

mdfe30_infPrazo_infPag_id = fields.Many2one(
comodel_name="mdfe.30.evpagtoopermdfe_infpag",
xsd_implicit=True,
ondelete="cascade",
)
mdfe30_nParcela = fields.Char(string="Número da Parcela", xsd_required=True)

mdfe30_dVenc = fields.Date(
string="Data de vencimento da Parcela",
xsd_required=True,
xsd_type="TData",
help="Data de vencimento da Parcela (AAAA-MM-DD)",
)

mdfe30_vParcela = fields.Monetary(
string="Valor da Parcela",
xsd_required=True,
xsd_type="TDec_1302Opc",
currency_field="brl_currency_id",
)


class EvPagtoOperMdfeInfBanc(models.AbstractModel):
"Informações bancárias"
_description = textwrap.dedent(" %s" % (__doc__,))
_name = "mdfe.30.evpagtoopermdfe_infbanc"
_inherit = "spec.mixin.mdfe"
_binding_type = "EvPagtoOperMdfe.InfPag.InfBanc"

mdfe30_codBanco = fields.Char(
string="Número do banco", choice="infbanc", xsd_choice_required=True
)

mdfe30_codAgencia = fields.Char(
string="Número da agência bancária", choice="infbanc", xsd_choice_required=True
)

mdfe30_CNPJIPEF = fields.Char(
string="Número do CNPJ da Instituição",
choice="infbanc",
xsd_choice_required=True,
xsd_type="TCnpjOpc",
help=(
"Número do CNPJ da Instituição de Pagamento Eletrônico do "
"Frete\nInformar os zeros não significativos."
),
)

mdfe30_PIX = fields.Char(
string="Chave PIX",
choice="infbanc",
xsd_choice_required=True,
help=(
"Chave PIX\nInformar a chave PIX para recebimento do frete. \nPode"
" ser email, CPF/ CNPJ (somente numeros), Telefone com a seguinte "
"formatação (+5599999999999) ou a chave aleatória gerada pela "
"instituição."
),
)
61 changes: 61 additions & 0 deletions l10n_br_mdfe_spec/models/v3_0/mdfe_modal_aereo_v3_00.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Copyright 2023 Akretion - Raphaël Valyi <raphael.valyi@akretion.com>
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0.en.html).
# Generated by https://github.com/akretion/xsdata-odoo
#
import textwrap
from odoo import fields, models

__NAMESPACE__ = "http://www.portalfiscal.inf.br/mdfe"


class Aereo(models.AbstractModel):
"Informações do modal Aéreo"
_description = textwrap.dedent(" %s" % (__doc__,))
_name = "mdfe.30.aereo"
_inherit = "spec.mixin.mdfe"
_binding_type = "Aereo"

mdfe30_nac = fields.Char(
string="Marca da Nacionalidade da aeronave", xsd_required=True
)

mdfe30_matr = fields.Char(
string="Marca de Matrícula da aeronave", xsd_required=True
)

mdfe30_nVoo = fields.Char(
string="Número do Voo",
xsd_required=True,
help=(
"Número do Voo\nFormato = AB1234, sendo AB a designação da empresa"
" e 1234 o número do voo. Quando não for possível incluir as "
"marcas de nacionalidade e matrícula sem hífen."
),
)

mdfe30_cAerEmb = fields.Char(
string="Aeródromo de Embarque",
xsd_required=True,
help=(
"Aeródromo de Embarque\nO código de três letras IATA do aeroporto "
"de partida deverá ser incluído como primeira anotação. Quando não"
" for possível, utilizar a sigla OACI."
),
)

mdfe30_cAerDes = fields.Char(
string="Aeródromo de Destino",
xsd_required=True,
help=(
"Aeródromo de Destino\nO código de três letras IATA do aeroporto "
"de destino deverá ser incluído como primeira anotação. Quando não"
" for possível, utilizar a sigla OACI."
),
)

mdfe30_dVoo = fields.Date(
string="Data do Voo",
xsd_required=True,
xsd_type="TData",
help="Data do Voo\nFormato AAAA-MM-DD",
)
244 changes: 244 additions & 0 deletions l10n_br_mdfe_spec/models/v3_0/mdfe_modal_aquaviario_v3_00.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
# Copyright 2023 Akretion - Raphaël Valyi <raphael.valyi@akretion.com>
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0.en.html).
# Generated by https://github.com/akretion/xsdata-odoo
#
import textwrap
from odoo import fields, models

__NAMESPACE__ = "http://www.portalfiscal.inf.br/mdfe"

"Tipo de Navegação"
AQUAV_TPNAV = [
("0", "Interior"),
("1", "Cabotagem"),
]

"Tipo da unidade de carga vazia"
INFUNIDCARGAVAZIA_TPUNIDCARGAVAZIA = [
("1", "Container"),
("2", "ULD"),
("3", "Pallet"),
("4", "Outros"),
]

"""Tipo da unidade de transporte vazia
Deve ser preenchido com “1” para Rodoviário Tração do tipo caminhão ou “2” para
Rodoviário reboque do tipo carreta"""
INFUNIDTRANSPVAZIA_TPUNIDTRANSPVAZIA = [
("1", "1"),
("2", "2"),
]


class Aquav(models.AbstractModel):
"Informações do modal Aquaviário"
_description = textwrap.dedent(" %s" % (__doc__,))
_name = "mdfe.30.aquav"
_inherit = "spec.mixin.mdfe"
_binding_type = "Aquav"

mdfe30_irin = fields.Char(
string="Irin do navio sempre deverá",
xsd_required=True,
help="Irin do navio sempre deverá ser informado",
)

mdfe30_tpEmb = fields.Char(
string="Código do tipo de embarcação",
xsd_required=True,
help=(
"Código do tipo de embarcação\nPreencher com código da Tabela de "
"Tipo de Embarcação definida no Ministério dos Transportes"
),
)

mdfe30_cEmbar = fields.Char(string="Código da embarcação", xsd_required=True)

mdfe30_xEmbar = fields.Char(string="Nome da embarcação", xsd_required=True)

mdfe30_nViag = fields.Char(string="Número da Viagem", xsd_required=True)

mdfe30_cPrtEmb = fields.Char(
string="Código do Porto de Embarque",
xsd_required=True,
help=(
"Código do Porto de Embarque\nPreencher de acordo com Tabela de "
"Portos definida no Ministério dos Transportes"
),
)

mdfe30_cPrtDest = fields.Char(
string="Código do Porto de Destino",
xsd_required=True,
help=(
"Código do Porto de Destino\nPreencher de acordo com Tabela de "
"Portos definida no Ministério dos Transportes"
),
)

mdfe30_prtTrans = fields.Char(string="Porto de Transbordo")

mdfe30_tpNav = fields.Selection(
AQUAV_TPNAV,
string="Tipo de Navegação",
help=(
"Tipo de Navegação\nPreencher com: \n\t\t\t\t\t\t0 - "
"Interior;\n\t\t\t\t\t\t1 - Cabotagem"
),
)

mdfe30_infTermCarreg = fields.One2many(
"mdfe.30.inftermcarreg",
"mdfe30_infTermCarreg_aquav_id",
string="Grupo de informações dos terminais",
help="Grupo de informações dos terminais de carregamento.",
)

mdfe30_infTermDescarreg = fields.One2many(
"mdfe.30.inftermdescarreg",
"mdfe30_infTermDescarreg_aquav_id",
string="infTermDescarreg",
help="Grupo de informações dos terminais de descarregamento.",
)

mdfe30_infEmbComb = fields.One2many(
"mdfe.30.infembcomb",
"mdfe30_infEmbComb_aquav_id",
string="Informações das Embarcações do Comboio",
)

mdfe30_infUnidCargaVazia = fields.One2many(
"mdfe.30.infunidcargavazia",
"mdfe30_infUnidCargaVazia_aquav_id",
string="Informações das Undades de Carga vazias",
)

mdfe30_infUnidTranspVazia = fields.One2many(
"mdfe.30.infunidtranspvazia",
"mdfe30_infUnidTranspVazia_aquav_id",
string="Informações das Undades",
help="Informações das Undades de Transporte vazias",
)


class InfTermCarreg(models.AbstractModel):
"Grupo de informações dos terminais de carregamento."
_description = textwrap.dedent(" %s" % (__doc__,))
_name = "mdfe.30.inftermcarreg"
_inherit = "spec.mixin.mdfe"
_binding_type = "Aquav.InfTermCarreg"

mdfe30_infTermCarreg_aquav_id = fields.Many2one(
comodel_name="mdfe.30.aquav", xsd_implicit=True, ondelete="cascade"
)
mdfe30_cTermCarreg = fields.Char(
string="Código do Terminal de Carregamento",
xsd_required=True,
help=(
"Código do Terminal de Carregamento\nPreencher de acordo com a "
"Tabela de Terminais de Carregamento. O código de cada Porto está "
"definido no Ministério de Transportes."
),
)

mdfe30_xTermCarreg = fields.Char(
string="Nome do Terminal de Carregamento", xsd_required=True
)


class InfTermDescarreg(models.AbstractModel):
"Grupo de informações dos terminais de descarregamento."
_description = textwrap.dedent(" %s" % (__doc__,))
_name = "mdfe.30.inftermdescarreg"
_inherit = "spec.mixin.mdfe"
_binding_type = "Aquav.InfTermDescarreg"

mdfe30_infTermDescarreg_aquav_id = fields.Many2one(
comodel_name="mdfe.30.aquav", xsd_implicit=True, ondelete="cascade"
)
mdfe30_cTermDescarreg = fields.Char(
string="Código do Terminal de Descarregamento",
xsd_required=True,
help=(
"Código do Terminal de Descarregamento\nPreencher de acordo com a "
"Tabela de Terminais de Descarregamento. O código de cada Porto "
"está definido no Ministério de Transportes."
),
)

mdfe30_xTermDescarreg = fields.Char(
string="Nome do Terminal de Descarregamento", xsd_required=True
)


class InfEmbComb(models.AbstractModel):
"Informações das Embarcações do Comboio"
_description = textwrap.dedent(" %s" % (__doc__,))
_name = "mdfe.30.infembcomb"
_inherit = "spec.mixin.mdfe"
_binding_type = "Aquav.InfEmbComb"

mdfe30_infEmbComb_aquav_id = fields.Many2one(
comodel_name="mdfe.30.aquav", xsd_implicit=True, ondelete="cascade"
)
mdfe30_cEmbComb = fields.Char(
string="Código da embarcação do comboio", xsd_required=True
)

mdfe30_xBalsa = fields.Char(string="Identificador da Balsa", xsd_required=True)


class InfUnidCargaVazia(models.AbstractModel):
"Informações das Undades de Carga vazias"
_description = textwrap.dedent(" %s" % (__doc__,))
_name = "mdfe.30.infunidcargavazia"
_inherit = "spec.mixin.mdfe"
_binding_type = "Aquav.InfUnidCargaVazia"

mdfe30_infUnidCargaVazia_aquav_id = fields.Many2one(
comodel_name="mdfe.30.aquav", xsd_implicit=True, ondelete="cascade"
)
mdfe30_idUnidCargaVazia = fields.Char(
string="Identificação da unidades de carga vazia",
xsd_required=True,
xsd_type="TContainer",
)

mdfe30_tpUnidCargaVazia = fields.Selection(
INFUNIDCARGAVAZIA_TPUNIDCARGAVAZIA,
string="Tipo da unidade de carga vazia",
xsd_required=True,
help=(
"Tipo da unidade de carga vazia\n1 - Container; 2 - ULD;3 - "
"Pallet;4 - Outros;"
),
)


class InfUnidTranspVazia(models.AbstractModel):
"Informações das Undades de Transporte vazias"
_description = textwrap.dedent(" %s" % (__doc__,))
_name = "mdfe.30.infunidtranspvazia"
_inherit = "spec.mixin.mdfe"
_binding_type = "Aquav.InfUnidTranspVazia"

mdfe30_infUnidTranspVazia_aquav_id = fields.Many2one(
comodel_name="mdfe.30.aquav", xsd_implicit=True, ondelete="cascade"
)
mdfe30_idUnidTranspVazia = fields.Char(
string="Identificação da unidades",
xsd_required=True,
xsd_type="TContainer",
help="Identificação da unidades de transporte vazia",
)

mdfe30_tpUnidTranspVazia = fields.Selection(
INFUNIDTRANSPVAZIA_TPUNIDTRANSPVAZIA,
string="Tipo da unidade de transporte vazia",
xsd_required=True,
help=(
"Tipo da unidade de transporte vazia\nDeve ser preenchido com “1” "
"para Rodoviário Tração do tipo caminhão ou “2” para Rodoviário "
"reboque do tipo carreta"
),
)
111 changes: 111 additions & 0 deletions l10n_br_mdfe_spec/models/v3_0/mdfe_modal_ferroviario_v3_00.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Copyright 2023 Akretion - Raphaël Valyi <raphael.valyi@akretion.com>
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0.en.html).
# Generated by https://github.com/akretion/xsdata-odoo
#
import textwrap
from odoo import fields, models

__NAMESPACE__ = "http://www.portalfiscal.inf.br/mdfe"


class Ferrov(models.AbstractModel):
"Informações do modal Ferroviário"
_description = textwrap.dedent(" %s" % (__doc__,))
_name = "mdfe.30.ferrov"
_inherit = "spec.mixin.mdfe"
_binding_type = "Ferrov"

mdfe30_trem = fields.Many2one(
comodel_name="mdfe.30.trem",
string="Informações da composição do trem",
xsd_required=True,
)

mdfe30_vag = fields.One2many(
"mdfe.30.vag", "mdfe30_vag_ferrov_id", string="informações dos Vagões"
)


class Trem(models.AbstractModel):
"Informações da composição do trem"
_description = textwrap.dedent(" %s" % (__doc__,))
_name = "mdfe.30.trem"
_inherit = "spec.mixin.mdfe"
_binding_type = "Ferrov.Trem"

mdfe30_xPref = fields.Char(string="Prefixo do Trem", xsd_required=True)

mdfe30_dhTrem = fields.Datetime(
string="Data e hora de liberação do trem",
xsd_type="TDateTimeUTC",
help="Data e hora de liberação do trem na origem",
)

mdfe30_xOri = fields.Char(
string="Origem do Trem",
xsd_required=True,
help="Origem do Trem\nSigla da estação de origem",
)

mdfe30_xDest = fields.Char(
string="Destino do Trem",
xsd_required=True,
help="Destino do Trem\nSigla da estação de destino",
)

mdfe30_qVag = fields.Char(
string="Quantidade de vagões carregados", xsd_required=True
)


class Vag(models.AbstractModel):
"informações dos Vagões"
_description = textwrap.dedent(" %s" % (__doc__,))
_name = "mdfe.30.vag"
_inherit = "spec.mixin.mdfe"
_binding_type = "Ferrov.Vag"

mdfe30_vag_ferrov_id = fields.Many2one(
comodel_name="mdfe.30.ferrov", xsd_implicit=True, ondelete="cascade"
)
mdfe30_pesoBC = fields.Float(
string="Peso Base de Cálculo de Frete",
xsd_required=True,
xsd_type="TDec_0303",
digits=(
3,
3,
),
help="Peso Base de Cálculo de Frete em Toneladas",
)

mdfe30_pesoR = fields.Float(
string="Peso Real em Toneladas",
xsd_required=True,
xsd_type="TDec_0303",
digits=(
3,
3,
),
)

mdfe30_tpVag = fields.Char(string="Tipo de Vagão")

mdfe30_serie = fields.Char(
string="Serie de Identificação do vagão", xsd_required=True
)

mdfe30_nVag = fields.Char(
string="Número de Identificação do vagão", xsd_required=True
)

mdfe30_nSeq = fields.Char(string="Sequencia do vagão na composição")

mdfe30_TU = fields.Char(
string="Tonelada Útil",
xsd_required=True,
help=(
"Tonelada Útil\nUnidade de peso referente à carga útil (apenas o "
"peso da carga transportada), expressa em toneladas."
),
)
833 changes: 833 additions & 0 deletions l10n_br_mdfe_spec/models/v3_0/mdfe_modal_rodoviario_v3_00.py

Large diffs are not rendered by default.

2,236 changes: 2,236 additions & 0 deletions l10n_br_mdfe_spec/models/v3_0/mdfe_tipos_basico_v3_00.py

Large diffs are not rendered by default.

183 changes: 183 additions & 0 deletions l10n_br_mdfe_spec/models/v3_0/tipos_geral_mdfe_v3_00.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
# Copyright 2022 Akretion - Raphaël Valyi <raphael.valyi@akretion.com>
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0.en.html).
# Generated by https://github.com/akretion/xsdata-odoo
#
import textwrap
from odoo import fields, models

__NAMESPACE__ = "http://www.portalfiscal.inf.br/mdfe"

"Tipo Ambiente"
TAMB = [
("1", "1"),
("2", "2"),
]

"""Tipo Código de orgão (UF da tabela do IBGE + 90 SUFRAMA + 91 RFB + 92
BRId)"""
TCORGAOIBGE = [
("11", "11"),
("12", "12"),
("13", "13"),
("14", "14"),
("15", "15"),
("16", "16"),
("17", "17"),
("21", "21"),
("22", "22"),
("23", "23"),
("24", "24"),
("25", "25"),
("26", "26"),
("27", "27"),
("28", "28"),
("29", "29"),
("31", "31"),
("32", "32"),
("33", "33"),
("35", "35"),
("41", "41"),
("42", "42"),
("43", "43"),
("50", "50"),
("51", "51"),
("52", "52"),
("53", "53"),
("90", "90"),
("91", "91"),
("92", "92"),
("93", "93"),
]

"Tipo Código da UF da tabela do IBGE"
TCODUFIBGE = [
("11", "11"),
("12", "12"),
("13", "13"),
("14", "14"),
("15", "15"),
("16", "16"),
("17", "17"),
("21", "21"),
("22", "22"),
("23", "23"),
("24", "24"),
("25", "25"),
("26", "26"),
("27", "27"),
("28", "28"),
("29", "29"),
("31", "31"),
("32", "32"),
("33", "33"),
("35", "35"),
("41", "41"),
("42", "42"),
("43", "43"),
("50", "50"),
("51", "51"),
("52", "52"),
("53", "53"),
]

"Tipo Código da UF da tabela do IBGE + 99 para Exterior"
TCODUFIBGE_EX = [
("11", "11"),
("12", "12"),
("13", "13"),
("14", "14"),
("15", "15"),
("16", "16"),
("17", "17"),
("21", "21"),
("22", "22"),
("23", "23"),
("24", "24"),
("25", "25"),
("26", "26"),
("27", "27"),
("28", "28"),
("29", "29"),
("31", "31"),
("32", "32"),
("33", "33"),
("35", "35"),
("41", "41"),
("42", "42"),
("43", "43"),
("50", "50"),
("51", "51"),
("52", "52"),
("53", "53"),
("99", "99"),
]

"Tipo Emitente"
TEMIT = [
("1", "1"),
("2", "2"),
("3", "3"),
]

"Tipo Modelo Manifesto de Documento Fiscal Eletrônico"
TMODMD = [
("58", "58"),
]

"Tipo Transportador"
TTRANSP = [
("1", "1"),
("2", "2"),
("3", "3"),
]

"Tipo Sigla da UF"
TUF = [
("AC", "AC"),
("AL", "AL"),
("AM", "AM"),
("AP", "AP"),
("BA", "BA"),
("CE", "CE"),
("DF", "DF"),
("ES", "ES"),
("GO", "GO"),
("MA", "MA"),
("MG", "MG"),
("MS", "MS"),
("MT", "MT"),
("PA", "PA"),
("PB", "PB"),
("PE", "PE"),
("PI", "PI"),
("PR", "PR"),
("RJ", "RJ"),
("RN", "RN"),
("RO", "RO"),
("RR", "RR"),
("RS", "RS"),
("SC", "SC"),
("SE", "SE"),
("SP", "SP"),
("TO", "TO"),
("EX", "EX"),
]

"Tipo da Unidade de Carga"
TTIPOUNIDCARGA = [
("1", "1"),
("2", "2"),
("3", "3"),
("4", "4"),
]

"Tipo da Unidade de Transporte"
TTIPOUNIDTRANSP = [
("1", "1"),
("2", "2"),
("3", "3"),
("4", "4"),
("5", "5"),
("6", "6"),
("7", "7"),
]
1 change: 1 addition & 0 deletions l10n_br_mdfe_spec/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Raphaël Valyi <raphael.valyi@akretion.com.br>
26 changes: 26 additions & 0 deletions l10n_br_mdfe_spec/readme/DESCRIPTION.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
Este módulo contem a estrutura de dados do ​Manifesto Eletrônico de Documentos Fiscais (MDF-e).
Este módulo não faz nada sozinho, ele precisaria de um modulo `l10n_br_mdfe` que mapearia esses mixins
nos documentos fiscais Odoo de forma semlhante a forma como o módulo `l10n_br_nfe` faz como o módulo `l10n_br_nfe_spec`.

Este módulo inclui os leiautes persistantes dos modos de transporte do MDF-e:

* modo aéreo
* modo aquaviário
* modo ferroviário
* modo rodoviário



Geração
~~~~~~~

O código dos mixins Odoo desse módulo é 100% gerado a partir dos últimos esquemas xsd da Fazenda usando xsdata e essa extensão dele:

https://github.com/akretion/xsdata-odoo


O comando usado foi:

export 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=odoo
Binary file added l10n_br_mdfe_spec/static/description/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
443 changes: 443 additions & 0 deletions l10n_br_mdfe_spec/static/description/index.html

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions l10n_br_mdfe_spec/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import test_mdfe_import
279 changes: 279 additions & 0 deletions l10n_br_mdfe_spec/tests/test_mdfe_import.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
# Copyright 2020 Akretion - Raphael Valyi <raphael.valyi@akretion.com>
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0.en.html).
# flake8: noqa: C901

import re
from datetime import datetime

import nfelib
import pkg_resources
from nfelib.mdfe.bindings.v3_0.mdfe_v3_00 import Tmdfe

from odoo import api, fields, models
from odoo.fields import Command
from odoo.models import BaseModel, NewId
from odoo.tests import TransactionCase
from odoo.tools import OrderedSet

from ..models import spec_mixin

tz_datetime = re.compile(r".*[-+]0[0-9]:00$")


@api.model
def build_fake(self, node, create=False):
attrs = self.build_attrs_fake(node, create_m2o=True)
return self.new(attrs)


# flake8: noqa: C901
@api.model
def build_attrs_fake(self, node, create_m2o=False):
"""
Similar to build_attrs from spec_driven_model but simpler: assuming
generated abstract mixins are not injected into concrete Odoo models.
"""
fields = self.fields_get()
vals = self.default_get(fields.keys())
for fname, fspec in node.__dataclass_fields__.items():
if fname == "any_element": # FIXME in spec_driven_model
continue
value = getattr(node, fname)
if value is None:
continue
key = f"mdfe30_{fspec.metadata.get('name', fname)}"
if (
fspec.type == str or not any(["." in str(i) for i in fspec.type.__args__])
) and not str(fspec.type).startswith("typing.List"):
# SimpleType
if fields[key]["type"] == "datetime":
if "T" in value:
if tz_datetime.match(value):
old_value = value
value = old_value[:19]
# TODO see python3/pysped/xml_sped/base.py#L692
value = datetime.strptime(value, "%Y-%m-%dT%H:%M:%S")
vals[key] = value

else:
if hasattr(fspec.type.__args__[0], "__name__"):
binding_type = fspec.type.__args__[0].__name__
else:
binding_type = fspec.type.__args__[0].__forward_arg__

# ComplexType
if fields.get(key) and fields[key].get("related"):
key = fields[key]["related"][0]
comodel_name = fields[key]["relation"]
else:
clean_type = binding_type.lower()
comodel_name = f"mdfe.30.{clean_type.split('.')[-1]}"
comodel = self.env.get(comodel_name)
if comodel is None: # example skip ICMS100 class
continue

if not str(fspec.type).startswith("typing.List"):
# m2o
new_value = comodel.build_attrs_fake(
value,
create_m2o=create_m2o,
)
if new_value is None:
continue
if comodel._name == self._name: # stacked m2o
vals.update(new_value)
else:
vals[key] = self.match_or_create_m2o_fake(
comodel, new_value, create_m2o
)
else: # if attr.get_container() == 1:
# o2m
lines = []
for line in [li for li in value if li]:
line_vals = comodel.build_attrs_fake(line, create_m2o=create_m2o)
lines.append((0, 0, line_vals))
vals[key] = lines

for k, v in fields.items():
if (
v.get("related") is not None
and len(v["related"]) == 1
and vals.get(k) is not None
):
vals[v["related"][0]] = vals.get(k)

return vals


@api.model
def match_or_create_m2o_fake(self, comodel, new_value, create_m2o=False):
return comodel.new(new_value)._ids[0]


spec_mixin.MdfeSpecMixin.build_fake = build_fake
spec_mixin.MdfeSpecMixin.build_attrs_fake = build_attrs_fake
spec_mixin.MdfeSpecMixin.match_or_create_m2o_fake = match_or_create_m2o_fake


# in version 12, 13 and 14, the code above would properly allow loading NFe XMLs
# as an Odoo AbstractModel structure for minimal testing of these structures.
# However in version Odoo 15 and 16 (at least), the ORM has trouble when
# doing env["some.model"].new(vals) if some.model is an AbstractModel like the
# models in this module. This is strange as new is available for AbstractModel...
# Anyway, only 2 methods are problematic for what we want to test here so
# a workaround is to monkey patch them as done in the next lines.
# Note that we only want test loading the XML data structure here,
# we remove the monkey patch after the tests and even if it's a dirty
# workaround it doesn't matter much because in the more completes tests in l10n_br_nfe
# we the models are made concrete so this problem does not occur anymore.
def fields_convert_to_cache(self, value, record, validate=True):
"""
A monkey patched version of convert_to_cache that works with
new instances of AbstractModel. Look at the lines after
# THE NEXT LINE WAS PATCHED:
and # THE NEXT 4 LINES WERE PATCHED:
to see the change.
"""
# cache format: tuple(ids)
if isinstance(value, BaseModel):
if validate and value._name != self.comodel_name:
raise ValueError("Wrong value for %s: %s" % (self, value))
ids = value._ids
if record and not record.id:
# x2many field value of new record is new records
ids = tuple(it and NewId(it) for it in ids)
return ids
elif isinstance(value, (list, tuple)):
# value is a list/tuple of commands, dicts or record ids
comodel = record.env[self.comodel_name]
# if record is new, the field's value is new records
# THE NEXT LINE WAS PATCHED:
if record and hasattr(record, "id") and not record.id:
browse = lambda it: comodel.browse([it and NewId(it)])
else:
browse = comodel.browse
# determine the value ids
ids = OrderedSet(record[self.name]._ids if validate else ())
# modify ids with the commands
for command in value:
if isinstance(command, (tuple, list)):
if command[0] == Command.CREATE:
# THE NEXT 4 LINES WERE PATCHED:
if hasattr(comodel.new(command[2], ref=command[1]), "id"):
ids.add(comodel.new(command[2], ref=command[1]).id)
else:
ids.add(comodel.new(command[2], ref=command[1])._ids[0])
elif command[0] == Command.UPDATE:
line = browse(command[1])
if validate:
line.update(command[2])
else:
line._update_cache(command[2], validate=False)
ids.add(line.id)
elif command[0] in (Command.DELETE, Command.UNLINK):
ids.discard(browse(command[1]).id)
elif command[0] == Command.LINK:
ids.add(browse(command[1]).id)
elif command[0] == Command.CLEAR:
ids.clear()
elif command[0] == Command.SET:
ids = OrderedSet(browse(it).id for it in command[2])
elif isinstance(command, dict):
ids.add(comodel.new(command).id)
else:
ids.add(browse(command).id)
# return result as a tuple
return tuple(ids)
elif not value:
return ()
raise ValueError("Wrong value for %s: %s" % (self, value))


fields_convert_to_cache._original_method = fields._RelationalMulti.convert_to_cache
fields._RelationalMulti.convert_to_cache = fields_convert_to_cache


def models_update_cache(self, values, validate=True):
"""
A monkey patched version of _update_cache that works with
new instances of AbstractModel. Look at the lines after
# THE NEXT LINE WAS PATCHED:
to see the change.
"""
self.ensure_one()
cache = self.env.cache
fields = self._fields
try:
field_values = [(fields[name], value) for name, value in values.items()]
except KeyError as e:
raise ValueError("Invalid field %r on model %r" % (e.args[0], self._name))
# convert monetary fields after other columns for correct value rounding
for field, value in sorted(field_values, key=lambda item: item[0].write_sequence):
cache.set(self, field, field.convert_to_cache(value, self, validate))
# set inverse fields on new records in the comodel
if field.relational:
# THE NEXT LINE WAS PATCHED:
inv_recs = self[field.name].filtered(
lambda r: hasattr(r, "id") and not r.id
)
if not inv_recs:
continue
for invf in self.pool.field_inverses[field]:
# DLE P98: `test_40_new_fields`
# /home/dle/src/odoo/master-nochange-fp/odoo/addons/test_new_api/tests/test_new_fields.py
# Be careful to not break `test_onchange_taxes_1`, `test_onchange_taxes_2`, `test_onchange_taxes_3`
# If you attempt to find a better solution
for inv_rec in inv_recs:
if not cache.contains(inv_rec, invf):
val = invf.convert_to_cache(self, inv_rec, validate=False)
cache.set(inv_rec, invf, val)
else:
invf._update(inv_rec, self)


models_update_cache._original_method = models.BaseModel._update_cache
models.BaseModel._update_cache = models_update_cache


class NFeImportTest(TransactionCase):
@classmethod
def tearDownClass(cls):
fields._RelationalMulti.convert_to_cache = (
fields_convert_to_cache._original_method
)
models.BaseModel._update_cache = models_update_cache._original_method
super().tearDownClass()

def test_import_mdfe(self):
res_items = (
"mdfe",
"samples",
"v3_0",
"41190876676436000167580010000500001000437558-mdfe.xml",
)
resource_path = "/".join(res_items)
mdfe_stream = pkg_resources.resource_stream(nfelib.__name__, resource_path)
binding = Tmdfe.from_xml(mdfe_stream.read().decode())
mdfe = (
self.env["mdfe.30.tmdfe_infmdfe"]
.with_context(tracking_disable=True, edoc_type="in", lang="pt_BR")
.build_fake(binding.infMDFe, create=False)
)
self.assertEqual(mdfe.mdfe30_emit.mdfe30_CNPJ, "76676436000167")

def test_import_mdfe2(self):
res_items = (
"mdfe",
"samples",
"v3_0",
"50170876063965000276580010000011311421039568-mdfe.xml",
)
resource_path = "/".join(res_items)
mdfe_stream = pkg_resources.resource_stream(nfelib.__name__, resource_path)
binding = Tmdfe.from_xml(mdfe_stream.read().decode())
mdfe = (
self.env["mdfe.30.tmdfe_infmdfe"]
.with_context(tracking_disable=True, edoc_type="in", lang="pt_BR")
.build_fake(binding.infMDFe, create=False)
)
self.assertEqual(mdfe.mdfe30_emit.mdfe30_xNome, "TESTE")
1 change: 1 addition & 0 deletions setup/l10n_br_mdfe_spec/odoo/addons/l10n_br_mdfe_spec
6 changes: 6 additions & 0 deletions setup/l10n_br_mdfe_spec/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import setuptools

setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)