Files
enterprise-addons/l10n_mx_reports/models/trial_balance.py
T

231 lines
11 KiB
Python

# coding: utf-8
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import re
from lxml import etree
from lxml.objectify import fromstring
from collections import defaultdict
from odoo import _, fields, models, tools
from odoo.exceptions import UserError, RedirectWarning
CFDIBCE_XSLT_CADENA = 'l10n_mx_reports/data/xslt/1.3/BalanzaComprobacion_1_2.xslt'
CFDICATALOGO_XSLT_CADENA = 'l10n_mx_reports/data/xslt/1.3/CatalogoCuentas_1_2.xslt'
class AccountTrialBalanceReportHandler(models.AbstractModel):
_inherit = 'account.trial.balance.report.handler'
def _custom_options_initializer(self, report, options, previous_options):
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 _check_accounts_code_sat_validity(self, accounts, options, report_action):
if options.get('l10n_mx_sat_ignore_errors'):
return
report = self.env['account.report'].browse(options['report_id'])
incorrect_code_accounts = accounts.filtered('l10n_mx_is_sat_invalid')
if incorrect_code_accounts:
account_names = '\n'.join(_('\t- %(name)s', name=account.name) for account in incorrect_code_accounts)
error_msg = _("Some of your accounts do not respect Odoo's code guidelines.\n\n%(account_names)s", account_names=account_names),
action_vals = report.export_file({**options, 'l10n_mx_sat_ignore_errors': True}, report_action)
raise RedirectWarning(error_msg, action_vals, _("Generate report"))
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"
cfdi = self.env['ir.qweb']._render('l10n_mx_reports.cfdibalance', sat_values)
sat_report = self._l10n_mx_edi_add_digital_stamp(CFDIBCE_XSLT_CADENA, cfdi)
return {
'file_name': f"{file_name}.xml",
'file_content': self.env['l10n_mx_edi.document']._convert_xml_to_attachment_data(sat_report),
'file_type': 'xml',
}
def _l10n_mx_edi_add_digital_stamp(self, path_xslt, cfdi):
"""Add digital stamp certificate attributes in XML report"""
tree = fromstring(cfdi)
certificate_sudo = self.env.company.sudo().l10n_mx_edi_certificate_ids.filtered('is_valid')[:1]
if not certificate_sudo:
return tree
with tools.file_open(path_xslt) as f:
cadena_transformer = etree.parse(f)
cadena = str(etree.XSLT(cadena_transformer)(tree))
tree.attrib['Sello'] = certificate_sudo._sign(cadena)
tree.attrib['noCertificado'] = ('%x' % int(certificate_sudo.serial_number))[1::2]
tree.attrib['Certificado'] = certificate_sudo.pem_certificate
return tree
def _l10n_mx_get_sat_values(self, options):
def get_column(column_type, external_label):
col_group_key = next(group for group, group_vals in options['column_groups'].items() if group_vals['forced_options']['trial_balance_column_type'] == column_type)
return next(col for col in cols if col['column_group_key'] == col_group_key and col['expression_label'] == external_label)
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})')
report_lines = [line for line in report_lines if line.get('level') >= 4]
account_ids = [report._get_res_id_from_line_id(line['id'], 'account.account') for line in report_lines]
accounts = self.env['account.account'].browse(account_ids)
self._check_accounts_code_sat_validity(accounts, options, 'action_l10n_mx_generate_sat_xml')
account_lines = []
parents = defaultdict(lambda: defaultdict(int))
for account, line in zip(accounts, report_lines):
if not account.id or account.account_type == 'equity_unaffected':
continue
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 = balance_sign * (get_column('initial_balance', 'balance').get('no_format', 0.0))
debit = get_column('period', 'debit').get('no_format', 0.0)
credit = get_column('period', 'credit').get('no_format', 0.0)
end = balance_sign * (get_column('end_balance', 'balance').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"
cfdi = self.env['ir.qweb']._render('l10n_mx_reports.cfdicoa', coa_values)
coa_report = self._l10n_mx_edi_add_digital_stamp(CFDICATALOGO_XSLT_CADENA, cfdi)
return {
'file_name': f"{file_name}.xml",
'file_content': self.env['l10n_mx_edi.document']._convert_xml_to_attachment_data(coa_report),
'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([
*self.env['account.account']._check_company_domain(self.env.company),
])
self._check_accounts_code_sat_validity(accounts, options, 'action_l10n_mx_generate_coa_sat_xml')
accounts_groups_by_parent = defaultdict(lambda: defaultdict(lambda: self.env['account.account']))
accounts_template_data = []
for account in accounts:
if account.group_id:
accounts_groups_by_parent[account.group_id.parent_id][account.group_id] |= account
no_tag_accounts = self.env['account.account']
for parent, accounts_by_group in accounts_groups_by_parent.items():
if not parent:
continue
parent_natures = set()
group_lines = defaultdict(list)
for group, accounts in accounts_by_group.items():
natures = set()
for account in accounts:
if debit_balance_account_tag in account.tag_ids:
natures.add('D')
if credit_balance_account_tag in account.tag_ids:
natures.add('A')
if not natures:
no_tag_accounts |= account
if not natures:
continue
for nature in sorted(natures):
group_lines[nature].append({
'code': group.code_prefix_start,
'number': group.code_prefix_start,
'name': group.name,
'level': 2,
'nature': nature,
})
parent_natures.add(nature)
if not parent_natures:
continue
for parent_nature in sorted(parent_natures):
parent_line = {
'code': parent.code_prefix_start,
'number': parent.code_prefix_start,
'name': parent.name,
'level': 1,
'nature': parent_nature,
}
accounts_template_data += [parent_line] + group_lines[parent_nature]
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')
)
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
}
)