1
0
forked from Mapan/odoo17e
odoo17e-kedaikipas58/addons/l10n_mx_reports/models/account_diot.py
2024-12-10 09:04:09 +07:00

280 lines
14 KiB
Python

# coding: utf-8
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from __future__ import division
import re
import logging
from unicodedata import normalize
from odoo import _, fields, models
from odoo.exceptions import RedirectWarning, UserError
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, get_lang
_logger = logging.getLogger(__name__)
def diot_country_adapt(values):
# In SAT classification some countries have a different country code
# and others do not exists at all
# https://blueprints.launchpad.net/openerp-mexico-localization/+spec/diot-mexico
cc = values.get('country_code')
non_diot_countries = {
'CD', 'SS', 'PS', 'XK', 'SX', 'ER', 'RS', 'ME', 'TL', 'MD', 'MF',
'BL', 'BQ', 'YT', 'AZ', 'MM', 'SK', 'CW', 'GS'
}
diot_country_dict = {
'AM': 'SU', 'BZ': 'BL', 'CZ': 'CS', 'DO': 'DM', 'EE': 'SU',
'GE': 'SU', 'DE': 'DD', 'GL': 'GJ', 'GG': 'GZ', 'IM': 'IH',
'JE': 'GZ', 'KZ': 'SU', 'KG': 'SU', 'LV': 'SU', 'LT': 'SU',
'RU': 'SU', 'WS': 'EO', 'TJ': 'SU', 'TM': 'SU', 'UZ': 'SU',
'SI': 'YU', 'BA': 'YU', 'HR': 'YU', 'MK': 'YU'
}
if cc in non_diot_countries:
# Country not in DIOT catalog, so we use the special code 'XX'
# for 'Other' countries
values['country_code'] = 'XX'
else:
# Map the standard country_code to the SAT standard
values['country_code'] = diot_country_dict.get(cc, cc)
return values
class MexicanAccountReportCustomHandler(models.AbstractModel):
_name = 'l10n_mx.report.handler'
_inherit = 'account.tax.report.handler'
_description = 'Mexican Account Report Custom Handler'
def _custom_options_initializer(self, report, options, previous_options=None):
options['columns'] = [column for column in options['columns']]
options.setdefault('buttons', []).extend((
{'name': _('DIOT (txt)'), 'sequence': 40, 'action': 'export_file', 'action_param': 'action_get_diot_txt', 'file_export_type': _('DIOT')},
{'name': _('DPIVA (txt)'), 'sequence': 60, 'action': 'export_file', 'action_param': 'action_get_dpiva_txt', 'file_export_type': _('DPIVA')},
))
def _report_custom_engine_diot_report(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None):
def build_dict(report, current_groupby, query_res):
if not current_groupby:
return query_res[0] if query_res else {k: None for k in report.mapped('line_ids.expression_ids.label')}
return [(group_res["grouping_key"], group_res) for group_res in query_res]
report = self.env['account.report'].browse(options['report_id'])
query_res = self._execute_query(report, current_groupby, options, offset, limit)
return build_dict(report, current_groupby, query_res)
def _execute_query(self, report, current_groupby, options, offset, limit):
report._check_groupby_fields([current_groupby] if current_groupby else [])
# This report mixes the tax_tags and custom engine.
# The results of the custom engine are relevant
# only when we have a partner. Else, we default to ''.
if current_groupby != 'partner_id':
return []
tables, where_clause, where_params = report._query_get(options, 'strict_range', domain=[
('parent_state', '=', 'posted'),
])
lang = self.env.user.lang or get_lang(self.env).code
tags = report.line_ids.expression_ids._get_matching_tags()
groupby_sql = """
raw_results.grouping_key,
raw_results.third_party_code,
raw_results.operation_type_code,
raw_results.partner_vat_number,
raw_results.country_code,
raw_results.partner_nationality
"""
tail_query, tail_params = report._get_engine_query_tail(offset, limit)
self._cr.execute(f"""
WITH raw_results as (
SELECT
account_move_line.partner_id AS grouping_key,
CASE WHEN country.code = 'MX' THEN '04' ELSE '05' END AS third_party_code,
partner.l10n_mx_type_of_operation AS operation_type_code,
partner.vat AS partner_vat_number,
country.code AS country_code,
COALESCE(country.demonym->>'{lang}', country.demonym->>'en_US') AS partner_nationality
FROM {tables}
JOIN account_move AS move ON move.id = account_move_line.move_id
JOIN account_account_tag_account_move_line_rel AS tag_aml_rel ON account_move_line.id = tag_aml_rel.account_move_line_id
JOIN account_account_tag AS tag ON tag.id = tag_aml_rel.account_account_tag_id AND tag.id IN %s
JOIN res_partner AS partner ON partner.id = account_move_line.partner_id
JOIN res_country AS country ON country.id = partner.country_id
WHERE {where_clause}
ORDER BY partner.name, account_move_line.date, account_move_line.id
)
SELECT
raw_results.grouping_key AS grouping_key,
count(raw_results.grouping_key) AS counter,
raw_results.third_party_code AS third_party_code,
raw_results.operation_type_code AS operation_type_code,
COALESCE(raw_results.partner_vat_number, '') AS partner_vat_number,
raw_results.country_code AS country_code,
raw_results.partner_nationality AS partner_nationality
FROM raw_results
GROUP BY
{groupby_sql}
ORDER BY
{groupby_sql}
{tail_query}
""",
[tuple(tags.ids)] + [
*where_params,
*tail_params,
],
)
return [diot_country_adapt(vals) for vals in self.env.cr.dictfetchall()]
def action_get_diot_txt(self, options):
report = self.env['account.report'].browse(options['report_id'])
partner_and_values_to_report = self._get_diot_values_per_partner(report, options)
self.check_for_error_on_partner([partner for partner in partner_and_values_to_report])
lines = []
for partner, values in partner_and_values_to_report.items():
if not any([values.get(x) for x in ('paid_16', 'paid_16_non_cred', 'paid_8', 'paid_8_non_cred', 'importation_16', 'paid_0', 'exempt', 'withheld', 'refunds')]):
# don't report if there isn't any amount to report
continue
is_foreign_partner = values['third_party_code'] != '04'
data = [''] * 25
data[0] = values['third_party_code'] # Supplier Type
data[1] = values['operation_type_code'] # Operation Type
data[2] = values['partner_vat_number'] if not is_foreign_partner else '' # Tax Number
data[3] = values['partner_vat_number'] if is_foreign_partner else '' # Tax Number for Foreigners
data[4] = ''.join(self.str_format(partner.name)).encode('utf-8').strip().decode('utf-8') if is_foreign_partner else '' # Name
data[5] = values['country_code'] if is_foreign_partner else '' # Country
data[6] = ''.join(self.str_format(values['partner_nationality'])).encode('utf-8').strip().decode('utf-8') if is_foreign_partner else '' # Nationality
data[7] = round(float(values.get('paid_16', 0))) or '' # 16%
data[9] = round(float(values.get('paid_16_non_cred', 0))) or '' # 16% Non-Creditable
data[12] = round(float(values.get('paid_8', 0))) or '' # 8%
data[14] = round(float(values.get('paid_8_non_cred', 0))) or '' # 8% Non-Creditable
data[15] = round(float(values.get('importation_16', 0))) or '' # 16% - Importation
data[20] = round(float(values.get('paid_0', 0))) or '' # 0%
data[21] = round(float(values.get('exempt', 0))) or '' # Exempt
data[22] = round(float(values.get('withheld', 0))) or '' # Withheld
data[23] = round(float(values.get('refunds', 0))) or '' # Refunds
lines.append('|'.join(str(d) for d in data))
diot_txt_result = '\n'.join(lines)
return {
'file_name': report.get_default_report_filename(options, 'txt'),
'file_content': diot_txt_result.encode(),
'file_type': 'txt',
}
def action_get_dpiva_txt(self, options):
report = self.env['account.report'].browse(options['report_id'])
partner_and_values_to_report = self._get_diot_values_per_partner(report, options)
self.check_for_error_on_partner([partner for partner in partner_and_values_to_report])
date = fields.datetime.strptime(options['date']['date_from'], DEFAULT_SERVER_DATE_FORMAT)
month = {
'01': 'Enero',
'02': 'Febrero',
'03': 'Marzo',
'04': 'Abril',
'05': 'Mayo',
'06': 'Junio',
'07': 'Julio',
'08': 'Agosto',
'09': 'Septiembre',
'10': 'Octubre',
'11': 'Noviembre',
'12': 'Diciembre',
}.get(date.strftime("%m"))
lines = []
for partner, values in partner_and_values_to_report.items():
if not any([values.get(x) for x in ('paid_16', 'paid_16_non_cred', 'paid_8', 'paid_8_non_cred', 'importation_16', 'paid_0', 'exempt', 'withheld', 'refunds')]):
# don't report if there isn't any amount to report
continue
is_foreign_partner = values['third_party_code'] != '04'
data = [''] * 48
data[0] = '1.0' # Version
data[1] = f"{date.year}" # Fiscal Year
data[2] = 'MES' # Cabling value
data[3] = month # Period
data[4] = '1' # 1 Because has data
data[5] = '1' # 1 = Normal, 2 = Complementary (Not supported now).
data[8] = values['counter'] # Count the operations
for num in range(9, 26):
data[num] = '0'
data[26] = values['third_party_code'] # Supplier Type
data[27] = values['operation_type_code'] # Operation Type
data[28] = values['partner_vat_number'] if not is_foreign_partner else '' # Federal Taxpayer Registry Code
data[29] = values['partner_vat_number'] if is_foreign_partner else '' # Fiscal ID
data[30] = ''.join(self.str_format(partner.name)).encode('utf-8').strip().decode('utf-8') if is_foreign_partner else '' # Name
data[31] = values['country_code'] if is_foreign_partner else '' # Country
data[32] = ''.join(self.str_format(values['partner_nationality'])).encode('utf-8').strip().decode('utf-8') if is_foreign_partner else '' # Nationality
data[33] = round(float(values.get('paid_16', 0))) or '' # 16%
data[36] = round(float(values.get('paid_8', 0))) or '' # 8%
data[39] = round(float(values.get('importation_16', 0))) or '' # 16% - Importation
data[44] = round(float(values.get('paid_0', 0))) or '' # 0%
data[45] = round(float(values.get('exempt', 0))) or '' # Exempt
data[46] = round(float(values.get('withheld', 0))) or '' # Withheld
data[47] = round(float(values.get('refunds', 0))) or '' # Refunds
lines.append('|{}|'.format('|'.join(str(d) for d in data)))
dpiva_txt_result = '\n'.join(lines)
return {
'file_name': report.get_default_report_filename(options, 'txt'),
'file_content': dpiva_txt_result.encode(),
'file_type': 'txt',
}
def _get_diot_values_per_partner(self, report, options):
options['unfolded_lines'] = [] # This allows to only get the first groupby level: partner_id
col_group_results = report._compute_expression_totals_for_each_column_group(report.line_ids.expression_ids, options, groupby_to_expand="partner_id")
if len(col_group_results) != 1:
raise UserError(_("You can only export one period at a time with this file format!"))
expression_list = list(col_group_results.values())
label_dict = {exp.label: v['value'] for d in expression_list for exp, v in d.items()}
partner_to_label_val = {}
for label, partner_to_value_list in label_dict.items():
for partner_id, value in partner_to_value_list:
partner_to_label_val.setdefault(self.env['res.partner'].browse(partner_id), {})[label] = value
return dict(sorted(partner_to_label_val.items(), key=lambda item: item[0].name))
def check_for_error_on_partner(self, partners):
partner_missing_information = self.env['res.partner']
for partner in partners:
if partner.country_id.code == "MX" and not partner.vat:
partner_missing_information += partner
if not partner.l10n_mx_type_of_operation:
partner_missing_information += partner
if partner_missing_information:
action_error = {
'name': _('Partner missing informations'),
'type': 'ir.actions.act_window',
'res_model': 'res.partner',
'view_mode': 'list',
'views': [(False, 'list'), (False, 'form')],
'domain': [('id', 'in', partner_missing_information.ids)],
}
msg = _('The report cannot be generated because some partners are missing a valid RFC or type of operation')
raise RedirectWarning(msg, action_error, _("See the list of partners"))
@staticmethod
def str_format(text):
if not text:
return ''
trans_tab = {
ord(char): None for char in (
u'\N{COMBINING GRAVE ACCENT}',
u'\N{COMBINING ACUTE ACCENT}',
u'\N{COMBINING DIAERESIS}',
)
}
text_n = normalize('NFKC', normalize('NFKD', text).translate(trans_tab))
check_re = re.compile(r'''[^A-Za-z\d Ññ]''')
return check_re.sub('', text_n)