Skip to Content
Menu
This question has been flagged
220 Views

Espero se en

Solución: Generación correcta del Catálogo de Cuentas XML SAT en Odoo 16.0+ (l10n_mx_reports)

Problema resuelto

Esta solución corrige el error de validación del catálogo de cuentas XML SAT que muestra:


Primero: este error se soluciona definiendo si la cuenta es de debito o crédito en la etiqueta de la cuenta:

Debe de verse asi:


Segundo error, este es por que hay cuentas que están como crédito pero de acuerdo al tipo que esta configurada debería de ser débito o viceversa, tambien puede ser por que existen 2 cuentas con exactamente el mismo código agrupador


Tercero, parece que el addon tiene algunos errores para la versión 16, por lo que hay que ralizar algunas modificaciones, aqui tienes el error que muestra:

 Requisitos previos

  • Odoo 16.0+ Enterprise con módulo l10n_mx_reports
  • Permisos de administrador del sistema
  • Acceso SSH al servidor
  • Las cuentas contables deben tener etiquetas de naturaleza (Deudora/Acreedora)

Archivos a modificar

1. Archivo Python del manejador

Ruta: /usr/lib/python3/dist-packages/odoo/addons/l10n_mx_reports/models/trial_balance.py

Respalde el archivo original:

bash

sudo cp /usr/lib/python3/dist-packages/odoo/addons/l10n_mx_reports/models/trial_balance.py \
        /usr/lib/python3/dist-packages/odoo/addons/l10n_mx_reports/models/trial_balance.py.backup

Reemplacelo con el siguiente codigo:
# coding: utf-8

# Part of Odoo. See LICENSE file for full copyright and licensing details.


import re

from lxml import etree

from collections import defaultdict


from odoo import models, fields, _

from odoo.exceptions import UserError, RedirectWarning



class TrialBalanceCustomHandler(models.AbstractModel):

    _inherit = 'account.trial.balance.report.handler'


    def _custom_options_initializer(self, report, options, previous_options=None):

        super()._custom_options_initializer(report, options, previous_options)

        if self.env.company.account_fiscal_country_id.code == 'MX':

            options['buttons'] += [

                {'name': _("SAT (XML)"), 'action': 'export_file', 'action_param': 'action_l10n_mx_generate_sat_xml', 'file_export_type': _("SAT (XML)"), 'sequence': 15},

                {'name': _("COA SAT (XML)"), 'action': 'export_file', 'action_param': 'action_l10n_mx_generate_coa_sat_xml', 'file_export_type': _("COA SAT (XML)"), 'sequence': 16},

            ]


    def action_l10n_mx_generate_sat_xml(self, options):

        if self.env.company.account_fiscal_country_id.code != 'MX':

            raise UserError(_("Only Mexican company can generate SAT report."))


        sat_values = self._l10n_mx_get_sat_values(options)

        file_name = f"{sat_values['vat']}{sat_values['year']}{sat_values['month']}BN"

        sat_report = etree.fromstring(self.env['ir.qweb']._render('l10n_mx_reports.cfdibalance', sat_values))


        self.env['ir.attachment'].l10n_mx_reports_validate_xml_from_attachment(sat_report, 'xsd_mx_cfdibalance_1_3.xsd')


        return {

            'file_name': f"{file_name}.xml",

            'file_content': etree.tostring(sat_report, pretty_print=True, xml_declaration=True, encoding='utf-8'),

            'file_type': 'xml',

        }


    def _l10n_mx_get_sat_values(self, options):

        report = self.env['account.report'].browse(options['report_id'])

        sat_options = self._l10n_mx_get_sat_options(options)

        report_lines = report._get_lines(sat_options)


        # The SAT code has to be of the form XXX.YY . Any additional suffixes are allowed, but if the line starts

        # with anything else it should not be included in the SAT report.

        sat_code = re.compile(r'((\d{3})\.\d{2})')


        account_lines = []

        parents = defaultdict(lambda: defaultdict(int))

        for line in [line for line in report_lines if line.get('level') == 4]:

            dummy, res_id = report._get_model_info_from_id(line['id'])

            account = self.env['account.account'].browse(res_id)

            is_credit_account = any([account.account_type.startswith(acc_type) for acc_type in ['liability', 'equity', 'income']])

            balance_sign = -1 if is_credit_account else 1

            cols = line.get('columns', [])

            # Initial Debit - Initial Credit = Initial Balance

            initial = balance_sign * (cols[0].get('no_format', 0.0) - cols[1].get('no_format', 0.0))

            # Debit and Credit of the selected period

            debit = cols[2].get('no_format', 0.0)

            credit = cols[3].get('no_format', 0.0)

            # End Debit - End Credit = End Balance

            end = balance_sign * (cols[4].get('no_format', 0.0) - cols[5].get('no_format', 0.0))

            pid_match = sat_code.match(line['name'])

            if not pid_match:

                raise UserError(_("Invalid SAT code: %s", line['name']))

            for pid in pid_match.groups():

                parents[pid]['initial'] += initial

                parents[pid]['debit'] += debit

                parents[pid]['credit'] += credit

                parents[pid]['end'] += end

        for pid in sorted(parents.keys()):

            account_lines.append({

                'number': pid,

                'initial': '%.2f' % parents[pid]['initial'],

                'debit': '%.2f' % parents[pid]['debit'],

                'credit': '%.2f' % parents[pid]['credit'],

                'end': '%.2f' % parents[pid]['end'],

            })


        report_date = fields.Date.to_date(sat_options['date']['date_from'])

        return {

            'vat': self.env.company.vat or '',

            'month': str(report_date.month).zfill(2),

            'year': report_date.year,

            'type': 'N',

            'accounts': account_lines,

        }


    def action_l10n_mx_generate_coa_sat_xml(self, options):

        if self.env.company.account_fiscal_country_id.code != 'MX':

            raise UserError(_("Only Mexican company can generate SAT report."))


        coa_values = self._l10n_mx_get_coa_values(options)

        file_name = f"{coa_values['vat']}{coa_values['year']}{coa_values['month']}CT"

       

        # Generar el XML con orden específico de atributos

        root = etree.Element(

            "{http://www.sat.gob.mx/esquemas/ContabilidadE/1_3/CatalogoCuentas}Catalogo",

            nsmap={

                None: "http://www.sat.gob.mx/esquemas/ContabilidadE/1_3/CatalogoCuentas",

                'xsi': "http://www.w3.org/2001/XMLSchema-instance",

                'catalogocuentas': "http://www.sat.gob.mx/esquemas/ContabilidadE/1_3/CatalogoCuentas"

            }

        )

       

        # Establecer atributos en orden específico

        root.set("{http://www.w3.org/2001/XMLSchema-instance}schemaLocation",

                 "http://www.sat.gob.mx/esquemas/ContabilidadE/1_3/CatalogoCuentas http://www.sat.gob.mx/esquemas/ContabilidadE/1_3/CatalogoCuentas/CatalogoCuentas_1_3.xsd")

        root.set("Version", "1.3")

        root.set("RFC", coa_values['vat'])

        root.set("Mes", coa_values['month'])

        root.set("Anio", str(coa_values['year']))

       

        # Agregar cuentas con atributos en orden específico

        for account in coa_values['accounts']:

            if all([account.get('number'), account.get('code'), account.get('name'),

                    account.get('nature'), account.get('level')]):

                cta = etree.SubElement(root, "{http://www.sat.gob.mx/esquemas/ContabilidadE/1_3/CatalogoCuentas}Ctas")

                # Orden específico: CodAgrup, NumCta, Desc, Nivel, Natur

                cta.set("CodAgrup", account.get('number'))

                cta.set("NumCta", account.get('code'))

                cta.set("Desc", account.get('name'))

                cta.set("Nivel", str(account.get('level')))

                cta.set("Natur", account.get('nature'))


        self.env['ir.attachment'].l10n_mx_reports_validate_xml_from_attachment(root, 'xsd_mx_cfdicoa_1_3.xsd')


        return {

            'file_name': f"{file_name}.xml",

            'file_content': etree.tostring(root, pretty_print=True, xml_declaration=True, encoding='utf-8'),

            'file_type': 'xml',

        }


    def _l10n_mx_get_coa_values(self, options):

        # Checking if debit/credit tags are installed

        debit_balance_account_tag = self.env.ref('l10n_mx.tag_debit_balance_account', raise_if_not_found=False)

        credit_balance_account_tag = self.env.ref('l10n_mx.tag_credit_balance_account', raise_if_not_found=False)

        if not debit_balance_account_tag or not credit_balance_account_tag:

            raise UserError(_("Missing Debit or Credit balance account tag in database."))


        coa_options = self._l10n_mx_get_sat_options(options)

        accounts = self.env['account.account'].search([

            ('company_id', '=', self.env.company.id),

            ('account_type', '!=', 'equity_unaffected'),

            ('group_id', '!=', False),

        ])

       

        accounts_template_data = []

        processed_groups = set()  # Para evitar duplicados

        no_tag_accounts = self.env['account.account']  # Cuentas sin etiquetas

        multi_tag_accounts = self.env['account.account']  # Cuentas con múltiples etiquetas

       

        for account in accounts:

            # Determinar la naturaleza de la cuenta

            nature = ''

            if debit_balance_account_tag in account.tag_ids:

                nature += 'D'

            if credit_balance_account_tag in account.tag_ids:

                nature += 'A'

           

            # Validar etiquetas

            if not nature:

                no_tag_accounts |= account

                continue  # Saltar cuentas sin etiqueta

            elif len(nature) > 1:

                multi_tag_accounts |= account

                continue  # Saltar cuentas con múltiples etiquetas conflictivas

           

            # Obtener y limpiar el código de agrupación del grupo

            cod_agrup_raw = account.group_id.code_prefix_start or ''

           

            # Limpiar el código para cumplir con formato SAT

            if '.' in cod_agrup_raw:

                parts = cod_agrup_raw.split('.')

                # Tomar solo los primeros dos niveles

                clean_parts = [p for p in parts[:2] if p and p != '000']

                if len(clean_parts) == 2 and clean_parts[1] != '00':

                    cod_agrup = '.'.join(clean_parts)

                    nivel = 2

                elif len(clean_parts) >= 1:

                    cod_agrup = clean_parts[0]

                    nivel = 1

                else:

                    continue  # Saltar si no hay código válido

            else:

                cod_agrup = cod_agrup_raw

                nivel = 1 if len(cod_agrup_raw) == 3 else 2

           

            # Agregar primero las cuentas de grupo si no se han procesado

            if cod_agrup not in processed_groups and account.group_id:

                processed_groups.add(cod_agrup)

               

                # Si es nivel 2, también agregar el nivel 1 si no existe

                if nivel == 2 and '.' in cod_agrup:

                    parent_code = cod_agrup.split('.')[0]

                    if parent_code not in processed_groups and account.group_id.parent_id:

                        processed_groups.add(parent_code)

                        accounts_template_data.append({

                            'number': parent_code,  # CodAgrup (plantilla invierte los campos)

                            'code': parent_code,    # NumCta

                            'name': account.group_id.parent_id.name,

                            'level': 1,

                            'nature': nature,  # Heredar naturaleza

                        })

               

                # Agregar el grupo actual

                accounts_template_data.append({

                    'number': cod_agrup,  # CodAgrup (plantilla invierte los campos)

                    'code': cod_agrup,    # NumCta

                    'name': account.group_id.name,

                    'level': nivel,

                    'nature': nature,

                })

           

            # Agregar la cuenta individual

            accounts_template_data.append({

                'number': cod_agrup,      # CodAgrup (plantilla invierte los campos)

                'code': account.code,     # NumCta - código real de la cuenta

                'name': account.name,     # Desc

                'level': 3,               # Las cuentas individuales son nivel 3

                'nature': nature,

            })

       

        # Validaciones y mensajes de error

        if no_tag_accounts:

            raise RedirectWarning(

                _("Some accounts present in your trial balance don't have a Debit or a Credit balance account tag."),

                {

                    'name': _("Accounts without tag"),

                    'type': 'ir.actions.act_window',

                    'views': [(False, 'list'), (False, 'form')],

                    'res_model': 'account.account',

                    'target': 'current',

                    'domain': [('id', 'in', no_tag_accounts.ids)],

                },

                _('Show list')

            )

       

        if multi_tag_accounts:

            raise RedirectWarning(

                _("Some accounts have both Debit and Credit balance account tags. This is not allowed."),

                {

                    'name': _("Accounts with conflicting tags"),

                    'type': 'ir.actions.act_window',

                    'views': [(False, 'list'), (False, 'form')],

                    'res_model': 'account.account',

                    'target': 'current',

                    'domain': [('id', 'in', multi_tag_accounts.ids)],

                },

                _('Show list')

            )

       

        # Ordenar por código para mejor presentación

        accounts_template_data.sort(key=lambda x: (x['number'], x['level'], x['code']))

       

        report_date = fields.Date.to_date(coa_options['date']['date_from'])

        return {

            'vat': self.env.company.vat or '',

            'month': str(report_date.month).zfill(2),

            'year': report_date.year,

            'accounts': accounts_template_data,

        }


    def _l10n_mx_get_sat_options(self, options):

        sat_options = options.copy()

        del sat_options['comparison']

        return self.env['account.report'].browse(options['report_id'])._get_options(

            previous_options={

                **sat_options,

                'hierarchy': True,  # We need the hierarchy activated to get group lines

            }

        )


2. Archivo de plantilla XML (opcional)

Ruta: /usr/lib/python3/dist-packages/odoo/addons/l10n_mx_reports/data/cfdicoa.xml o similar

Si desea corregir el orden de los atributos en el XML, actualice la plantilla para incluir el t-foreach:

xml

<t t-foreach="accounts" t-as="account">
    <t t-if="account.get('code') and account.get('number') and account.get('name') and account.get('nature') and account.get('level')">
        <catalogocuentas:Ctas
            t-att-CodAgrup="account.get('number')"
            t-att-NumCta="account.get('code')"
            t-att-Desc="account.get('name')"
            t-att-Nivel="account.get('level')"
            t-att-Natur="account.get('nature')"/>
    </t>
</t>

 Instalación

  1. Detener el servicio de Odoo:

    bash

    sudo systemctl stop odoo
  2. Aplicar los cambios (copiar el código actualizado)
  3. Limpiar el caché de Python:

    bash

    sudo find /usr/lib/python3/dist-packages/odoo -name "*.pyc" -delete
    sudo find /usr/lib/python3/dist-packages/odoo -name "__pycache__" -type d -exec rm -rf {} +
  4. Reiniciar Odoo:

    bash

    sudo systemctl start odoo

 

 Cambios principales implementados

  1. Limpieza de códigos de agrupación SAT:
    • Elimina el tercer nivel .000 que no acepta el SAT
    • Convierte 100.01.000 → 100.01
    • Mantiene solo los formatos XXX o XXX.YY
  2. Generación de estructura completa:
    • Nivel 1: Grupos principales (ej: 101, 102)
    • Nivel 2: Subgrupos (ej: 101.01, 102.01)
    • Nivel 3: Cuentas individuales con sus códigos reales
  3. Validaciones agregadas:
    • Detecta cuentas sin etiquetas de naturaleza (D/A)
    • Detecta cuentas con etiquetas conflictivas
    • Muestra ventanas emergentes para corregir problemas
  4. Control del orden de atributos XML:
    • Genera XML con atributos en orden correcto
    • CodAgrup, NumCta, Desc, Nivel, Natur

Consideraciones importantes

  1. Configuración de etiquetas:
    • Todas las cuentas DEBEN tener una etiqueta: "Cuenta de balance deudora" O "Cuenta de balance acreedora"
    • NO pueden tener ambas etiquetas
    • Sin etiqueta = error al generar
  2. Grupos contables:
    • Los grupos deben tener code_prefix_start configurado
    • El formato debe seguir el estándar SAT (XXX.YY.ZZZ)
  3. Respaldos:
    • SIEMPRE respaldar antes de modificar
    • Guardar una copia del XML generado para validación

Verificación

  1. Generar el XML desde: Contabilidad → Informes → Balance de comprobación → COA SAT (XML)
  2. Validar en: https://ceportalvalidacionprod.clouda.sat.gob.mx/
  3. El XML debe mostrar estructura como:

    xml

    <catalogocuentas:Ctas CodAgrup="101" NumCta="101" Desc="Caja" Nivel="1" Natur="D"/>
    <catalogocuentas:Ctas CodAgrup="101.01" NumCta="101.01" Desc="Caja y efectivo" Nivel="2" Natur="D"/>
    <catalogocuentas:Ctas CodAgrup="101.01" NumCta="101.01.001" Desc="Efectivo" Nivel="3" Natur

cuentren bien, me encontré con algunos errores 

Avatar
Discard