akretion / nfelib

nfelib - bindings Python para e ler e gerir XML de NF-e, NFS-e nacional, CT-e, MDF-e, BP-e
MIT License
144 stars 59 forks source link

xsdata and --compound-field #40

Open rvalyi opened 2 years ago

rvalyi commented 2 years ago

Na nova branch master-xsdata que usa xsdata, temos um pequeno problema com o elemento IPI da tag Imposto. Da forma como é o xsd, ou precisa de um patch de 1 linha no xsdata, ou precisa usar a opçâo --compound-field do xsdata. Eu detalhei o problema aqui: https://github.com/tefra/xsdata/issues/666

De inicio, pode parecer uma boa usar a opção --compound-field já que resolve o problema no binding gerido como aqui. Porem tem um outro lado da moeda: quando usar essa opção, tem pelo menos 2 campos (IPI do Imposto e um outro no Transp) que viram então um campo composto com uma hierarquia mais funda:

Sem a opção --compound-field e com meu patch https://github.com/tefra/xsdata/issues/666:

            @dataclass
            class Imposto:

                v_tot_trib: Optional[str] = field(
                    default=None,
                    metadata={
                        "name": "vTotTrib",
                        "type": "Element",
                        "namespace": "http://www.portalfiscal.inf.br/nfe",
                        "white_space": "preserve",
                        "pattern": r"0|0\.[0-9]{2}|[1-9]{1}[0-9]{0,12}(\.[0-9]{2})?",
                    }
                )
                icms: Optional["Tnfe.InfNfe.Det.Imposto.Icms"] = field(
                    default=None,
                    metadata={
                        "name": "ICMS",
                        "type": "Element",
                        "namespace": "http://www.portalfiscal.inf.br/nfe",
                    }
                )
                ipi: Optional[Tipi] = field(
                    default=None,
                    metadata={
                        "name": "IPI",
                        "type": "Element",
                        "namespace": "http://www.portalfiscal.inf.br/nfe",
                    }
                )

Agora com a opção --compound-field e sem meu patch:

            @dataclass
            class Imposto:
                v_tot_trib: Optional[str] = field(
                    default=None,
                    metadata={
                        "name": "vTotTrib",
                        "type": "Element",
                        "namespace": "http://www.portalfiscal.inf.br/nfe",
                        "white_space": "preserve",
                        "pattern": r"0|0\.[0-9]{2}|[1-9]{1}[0-9]{0,12}(\.[0-9]{2})?",
                    }   
                )  
                icms: Optional["Tnfe.InfNfe.Det.Imposto.Icms"] = field(
                    default=None,
                    metadata={
                        "name": "ICMS",
                        "type": "Element",
                        "namespace": "http://www.portalfiscal.inf.br/nfe",
                    }
                )  
                choice: List[object] = field(
                    default_factory=list,
                    metadata={
                        "type": "Elements",
                        "choices": (
                            {
                                "name": "IPI",
                                "type": Tipi,
                                "namespace": "http://www.portalfiscal.inf.br/nfe",
                            },
                            {
                                "name": "II",
                                "type": Type["Tnfe.InfNfe.Det.Imposto.Ii"],
                                "namespace": "http://www.portalfiscal.inf.br/nfe",
                            },
                            {
                                "name": "ISSQN",
                                "type": Type["Tnfe.InfNfe.Det.Imposto.Issqn"],
                                "namespace": "http://www.portalfiscal.inf.br/nfe",
                            },
                            {
                                "name": "PIS",
                                "type": Type["Tnfe.InfNfe.Det.Imposto.Pis"],
                                "namespace": "http://www.portalfiscal.inf.br/nfe",
                            },
                            {
                                "name": "PISST",
                                "type": Type["Tnfe.InfNfe.Det.Imposto.Pisst"],
                                "namespace": "http://www.portalfiscal.inf.br/nfe",
                            },
                            {
                                "name": "COFINS",
                                "type": Type["Tnfe.InfNfe.Det.Imposto.Cofins"],
                                "namespace": "http://www.portalfiscal.inf.br/nfe",
                            },
                            {
                                "name": "COFINSST",
                                "type": Type["Tnfe.InfNfe.Det.Imposto.Cofinsst"],
                                "namespace": "http://www.portalfiscal.inf.br/nfe",
                            },
                            {
                                "name": "ICMSUFDest",
                                "type": Type["Tnfe.InfNfe.Det.Imposto.Icmsufdest"],
                                "namespace": "http://www.portalfiscal.inf.br/nfe",
                            },
                        ),
                        "max_occurs": 9,
                    }
                )

Nesse caso o xsdata tem uma espece de gestão de choice.

O grande ponto é que nos temos um interesso não apenas nos bindings Python mas tb nos modelos Odoo geridos pelo plugin https://github.com/akretion/xsdata-odoo Esses modelos Odoo são de persistência e é interessante que seja muito mais "planos" do que os modelos super fundos do XSD que servem apenas para validação XSD. E usar a opção --compound-field iria atrapalhar nisso porque nos obrigaria a repensar o mapeamento entre um campo Odoo e um campo de um binding dessa lib, criaria mais complexidade no mapping. Se essa opção tb lidava com as verdadeiras tags choice do xsd como fazia o generateDS (que nos interessa apenas nas visões automáticas, ou seja muito pouco hoje), eu bancaria essa complexidade. Mas não é o caso, seria uma complexidade meio inútil.

Eu fiz uma busca exaustiva e o problema acontece apenas com essa tag IPI na dezena de esquemas de NFe que temos nessa branch. Com tudo o meu fix aqui https://github.com/tefra/xsdata/issues/666 resolve isso perfeitamente (eu gerei tudo de novo e vi que o impacto era apenas o esperado aqui).

Nisso minha escolha é de usar esse patch de uma linha no xsdata ou então no código Python gerido apenas pela tag IPI.

cc @mbcosta @renatonlima @marcelsavegnago @netosjb @felipemotter

rvalyi commented 2 years ago

O fix ainda é necessario, mas no xsdata o nome do arquivo mudou. Eu refiz o commit xsdata aqui então: https://github.com/akretion/xsdata/commit/933ddc0b23264fec9910b535cd9199a11aff4eb4

rvalyi commented 1 year ago

atualização: o método process foi ré-escrito na futura versão do xsdata: https://github.com/tefra/xsdata/commit/4dc9b41f6e85616927f74bddead80ebee33a995b

agora tem que aplicar um patch no metodo: merge_duplicate_attrs

@classmethod
def merge_duplicate_attrs(self, target: Class):
    result: List[Attr] = []
    for attr in target.attrs:
        pos = collections.find(result, attr)
        existing = result[pos] if pos > -1 else None

        if not existing:
            result.append(attr)
        elif not (attr.is_attribute or attr.is_enumeration):
            existing.help = existing.help or attr.help

            e_res = existing.restrictions
            a_res = attr.restrictions

            min_occurs = e_res.min_occurs or 0
            max_occurs = e_res.max_occurs or 1
            attr_min_occurs = a_res.min_occurs or 0
            attr_max_occurs = a_res.max_occurs or 1

            e_res.min_occurs = min(min_occurs, attr_min_occurs)
            e_res.max_occurs = min(max_occurs, attr_max_occurs)  # this is the patch

            if a_res.sequence is not None:
                e_res.sequence = a_res.sequence

            existing.fixed = False
            existing.types.extend(attr.types)

    target.attrs = result
    ClassUtils.cleanup_class(target)