forked from Mapan/odoo17e
232 lines
11 KiB
Python
232 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 models, fields, _
|
|
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 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"
|
|
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)
|
|
|
|
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': 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 = self.env.company.sudo().l10n_mx_edi_certificate_ids._get_valid_certificate()
|
|
if not certificate:
|
|
return tree
|
|
cadena = certificate._get_cadena_chain(tree, path_xslt)
|
|
sello = certificate.sudo()._get_encrypted_cadena(cadena)
|
|
tree.attrib['Sello'] = sello
|
|
tree.attrib['noCertificado'] = certificate.serial_number
|
|
tree.attrib['Certificado'] = certificate.sudo()._get_data()[0]
|
|
return tree
|
|
|
|
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"
|
|
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)
|
|
|
|
self.env['ir.attachment'].l10n_mx_reports_validate_xml_from_attachment(coa_report, 'xsd_mx_cfdicoa_1_3.xsd')
|
|
|
|
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),
|
|
('account_type', '!=', 'equity_unaffected'),
|
|
('group_id', '!=', False),
|
|
])
|
|
accounts_groups_by_parent = defaultdict(lambda: defaultdict(lambda: self.env['account.account']))
|
|
accounts_template_data = []
|
|
for account in accounts:
|
|
accounts_groups_by_parent[account.group_id.parent_id][account.group_id] |= account
|
|
no_tag_accounts = self.env['account.account']
|
|
multi_tag_accounts = self.env['account.account']
|
|
parent_nature = ''
|
|
for parent, accounts_by_group in accounts_groups_by_parent.items():
|
|
if not parent:
|
|
continue
|
|
parent_nature = set()
|
|
group_lines = []
|
|
for group, accounts in accounts_by_group.items():
|
|
group_nature = set()
|
|
for account in accounts:
|
|
nature = ''
|
|
if debit_balance_account_tag in account.tag_ids:
|
|
nature += 'D'
|
|
if credit_balance_account_tag in account.tag_ids:
|
|
nature += 'A'
|
|
if not nature:
|
|
no_tag_accounts |= account
|
|
elif len(nature) > 1:
|
|
multi_tag_accounts |= account
|
|
else:
|
|
group_nature.add(nature)
|
|
if len(group_nature) > 1:
|
|
multi_tag_accounts |= accounts
|
|
|
|
group_nature = len(group_nature) and group_nature.pop()
|
|
group_lines.append({
|
|
'code': group.code_prefix_start,
|
|
'number': group.code_prefix_start,
|
|
'name': group.name,
|
|
'level': 2,
|
|
'nature': group_nature,
|
|
})
|
|
if group_nature:
|
|
parent_nature.add(group_nature)
|
|
|
|
if len(parent_nature) > 1:
|
|
for accounts in accounts_by_group.values():
|
|
multi_tag_accounts |= accounts
|
|
parent_nature = len(parent_nature) and parent_nature.pop()
|
|
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
|
|
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 account prefixes used in your trial balance use both Debit and Credit balance account tags. This is not allowed."),
|
|
{
|
|
'name': _("Accounts with too much 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')
|
|
)
|
|
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
|
|
}
|
|
)
|