forked from Mapan/odoo17e
280 lines
14 KiB
Python
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)
|