355 lines
20 KiB
Python
355 lines
20 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 datetime import datetime
|
|
from unicodedata import normalize
|
|
|
|
from odoo import _, models
|
|
from odoo.exceptions import RedirectWarning, UserError
|
|
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, get_lang, SQL
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
def diot_country_adapt(values):
|
|
# 2025 SAT classification uses 3-letter country codes
|
|
# https://www.fiscalia.com/archivos/2025-02-06-DIOT_2025_Ayuda_Carga_Masiva.pdf
|
|
cc = values.get('country_code')
|
|
diot_country_dict = {
|
|
'MX': 'MEX', None: '',
|
|
'AF': 'AFG', 'AX': 'ALA', 'AL': 'ALB', 'DE': 'DEU', 'AD': 'AND', 'AO': 'AGO', 'AI': 'AIA', 'AQ': 'ATA', 'AG': 'ATG', 'SA': 'SAU',
|
|
'DZ': 'DZA', 'AR': 'ARG', 'AM': 'ARM', 'AW': 'ABW', 'AU': 'AUS', 'AT': 'AUT', 'AZ': 'AZE', 'BS': 'BHS', 'BD': 'BGD', 'BB': 'BRB',
|
|
'BH': 'BHR', 'BE': 'BEL', 'BZ': 'BLZ', 'BJ': 'BEN', 'BM': 'BMU', 'BY': 'BLR', 'MM': 'MMR', 'BO': 'BOL', 'BA': 'BIH', 'BW': 'BWA',
|
|
'BR': 'BRA', 'BN': 'BRN', 'BG': 'BGR', 'BF': 'BFA', 'BI': 'BDI', 'BT': 'BTN', 'CV': 'CPV', 'KH': 'KHM', 'CM': 'CMR', 'CA': 'CAN',
|
|
'QA': 'QAT', 'TD': 'TCD', 'CL': 'CHL', 'CN': 'CHN', 'CY': 'CYP', 'CO': 'COL', 'KM': 'COM', 'KP': 'PRK', 'KR': 'KOR', 'CI': 'CIV',
|
|
'CR': 'CRI', 'HR': 'HRV', 'CU': 'CUB', 'CW': 'CUW', 'DK': 'DNK', 'DM': 'DMA', 'EC': 'ECU', 'EG': 'EGY', 'SV': 'SLV', 'AE': 'ARE',
|
|
'ER': 'ERI', 'SK': 'SVK', 'SI': 'SVN', 'ES': 'ESP', 'US': 'USA', 'EE': 'EST', 'ET': 'ETH', 'PH': 'PHL', 'FI': 'FIN', 'FJ': 'FJI',
|
|
'FR': 'FRA', 'GA': 'GAB', 'GB': 'GBR', 'GM': 'GMB', 'GE': 'GEO', 'GH': 'GHA', 'GI': 'GIB', 'GD': 'GRD', 'GR': 'GRC', 'GL': 'GRL', 'GP': 'GLP',
|
|
'GU': 'GUM', 'GT': 'GTM', 'GF': 'GUF', 'GG': 'GGY', 'GN': 'GIN', 'GW': 'GNB', 'GQ': 'GNY', 'HT': 'HTI', 'HN': 'HND', 'HK': 'HKG',
|
|
'HU': 'HUN', 'IN': 'IND', 'IQ': 'IRQ', 'IR': 'IRN', 'IE': 'IRL', 'BV': 'BVT', 'IM': 'IMN', 'CX': 'CXR', 'NF': 'NFK', 'IS': 'ISL',
|
|
'KY': 'CYM', 'CC': 'CCK', 'CK': 'COK', 'FO': 'FRO', 'GS': 'SGS', 'HM': 'HMD', 'FK': 'FLK', 'MP': 'MNP', 'MH': 'MHL', 'PN': 'PCN',
|
|
'SB': 'SLB', 'TC': 'TCA', 'UM': 'UMI', 'VG': 'VGB', 'VI': 'VIR', 'IL': 'ISR', 'IT': 'ITA', 'JM': 'JAM', 'JP': 'JPN', 'JE': 'JEY',
|
|
'JO': 'JOR', 'KZ': 'KAZ', 'KE': 'KEN', 'KG': 'KGZ', 'KI': 'KIR', 'KW': 'KWT', 'LA': 'LAO', 'LS': 'LSO', 'LV': 'LVA', 'LB': 'LBA',
|
|
'LR': 'LBR', 'LY': 'LBY', 'LI': 'LIE', 'LT': 'LTU', 'LU': 'LUX', 'MO': 'MAC', 'MG': 'MDG', 'MY': 'MYS', 'MW': 'MWI', 'MV': 'MDV',
|
|
'ML': 'MLI', 'MT': 'MLT', 'MA': 'MAR', 'MQ': 'MTQ', 'MU': 'MUS', 'MR': 'MRT', 'YT': 'MYT', 'FM': 'FSM', 'MD': 'MDA', 'MC': 'MCO',
|
|
'MN': 'MNG', 'ME': 'MNE', 'MS': 'MSR', 'MZ': 'MOZ', 'NA': 'NAM', 'NR': 'NRU', 'NP': 'NPL', 'NI': 'NIC', 'NE': 'NER', 'NG': 'NGA',
|
|
'NU': 'NIU', 'NO': 'NOR', 'NC': 'NCL', 'NZ': 'NZL', 'OM': 'OMN', 'NL': 'NLD', 'PK': 'PAK', 'PW': 'PLW', 'PS': 'PSE', 'PA': 'PAN',
|
|
'PG': 'PNG', 'PY': 'PRY', 'PE': 'PER', 'PF': 'PYF', 'PL': 'POL', 'PT': 'PRT', 'PR': 'PRI', 'CF': 'CAF', 'CZ': 'CZE', 'MK': 'MKD',
|
|
'CG': 'COG', 'CD': 'COD', 'DR': 'DOM', 'RE': 'REU', 'RW': 'RWA', 'RO': 'ROU', 'RU': 'RUS', 'EH': 'ESH', 'WS': 'WSM', 'AS': 'ASM',
|
|
'BL': 'BLM', 'KN': 'KNA', 'SM': 'SMR', 'MF': 'MAF', 'PM': 'SPM', 'VC': 'VCT', 'SH': 'SHN', 'LC': 'LCA', 'ST': 'STP', 'SN': 'SEN',
|
|
'RS': 'SRB', 'SC': 'SYC', 'SL': 'SLE', 'SG': 'SGP', 'SX': 'SXM', 'SY': 'SYR', 'SO': 'SOM', 'LK': 'LKA', 'SZ': 'SWZ', 'ZA': 'ZAF',
|
|
'SD': 'SDN', 'SS': 'SSD', 'SE': 'SWE', 'CH': 'CHE', 'SR': 'SUR', 'SJ': 'SJM', 'TH': 'THA', 'TW': 'TWA', 'TZ': 'TZA', 'TJ': 'TJK',
|
|
'IO': 'IOT', 'TF': 'ATF', 'TL': 'TLS', 'TG': 'TGO', 'TK': 'TKL', 'TO': 'TON', 'TT': 'TTO', 'TN': 'TUN', 'TM': 'TKM', 'TR': 'TUR',
|
|
'TV': 'TUV', 'UA': 'UKR', 'UG': 'UGA', 'UY': 'URY', 'UZ': 'UZB', 'VU': 'VUT', 'VA': 'VAT', 'VE': 'VEN', 'VN': 'VNM', 'WF': 'WLF',
|
|
'YE': 'YEM', 'DJ': 'DJY', 'ZM': 'ZMB', 'ZW': 'ZWE'
|
|
}
|
|
|
|
if cc not in diot_country_dict:
|
|
# Country not in DIOT catalog, so we use the special code 'ZZZ'
|
|
# for 'Other' countries
|
|
values['country_code'] = 'ZZZ'
|
|
else:
|
|
# Map the standard country_code to the SAT standard
|
|
values['country_code'] = diot_country_dict.get(cc, cc)
|
|
return values
|
|
|
|
|
|
class L10n_MxReportHandler(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):
|
|
super()._custom_options_initializer(report, options, previous_options)
|
|
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 []
|
|
|
|
query = report._get_report_query(options, 'strict_range', domain=[
|
|
('parent_state', '=', 'posted'),
|
|
])
|
|
country_demonym = self.env['res.country']._field_to_sql('country', 'demonym')
|
|
tags = report.line_ids.expression_ids._get_matching_tags()
|
|
|
|
tail_query = report._get_engine_query_tail(offset, limit)
|
|
self.env.cr.execute(SQL(
|
|
"""
|
|
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,
|
|
%(country_demonym)s AS partner_nationality
|
|
FROM %(table_references)s
|
|
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 %(tags)s
|
|
JOIN res_partner AS partner ON partner.id = account_move_line.partner_id
|
|
LEFT JOIN res_country AS country ON country.id = partner.country_id
|
|
WHERE %(search_condition)s
|
|
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)s
|
|
ORDER BY
|
|
%(groupby_sql)s
|
|
%(tail_query)s
|
|
""",
|
|
country_demonym=country_demonym,
|
|
table_references=query.from_clause,
|
|
tags=tuple(tags.ids),
|
|
search_condition=query.where_clause,
|
|
groupby_sql=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_query,
|
|
))
|
|
|
|
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(list(partner_and_values_to_report))
|
|
|
|
lines = []
|
|
data_87 = [0] * 54
|
|
for partner, values in partner_and_values_to_report.items():
|
|
if not sum(values.get(x, 0) for x in (
|
|
'paid_8', 'paid_8_non_cred', 'paid_8_tax', 'paid_8_non_cred_tax', 'refunds_8_n',
|
|
'paid_8_s', 'paid_8_s_nc', 'paid_8_s_tax', 'paid_8_s_nc_tax', 'refunds_8_s',
|
|
'paid_16', 'paid_16_non_cred', 'paid_16_tax', 'paid_16_non_cred_tax', 'refunds_16',
|
|
'importation_16', 'paid_16_imp_nc', 'importation_16_tax', 'paid_16_imp_nc_tax', 'refunds_16_imp',
|
|
'paid_16_imp_int', 'paid_16_imp_int_nc', 'paid_16_imp_int_tax', 'paid_16_imp_int_nc_tax', 'refunds_16_imp_int',
|
|
'withheld', 'exempt', 'exempt_imp', 'paid_0', 'no_obj'
|
|
)):
|
|
# don't report if there isn't any amount to report
|
|
continue
|
|
|
|
data = [0] * 54
|
|
if values.get('operation_type_code') != '87':
|
|
self.l10n_mx_diot_get_values(values, data, partner)
|
|
for i in range(7, 53):
|
|
if not data[i]:
|
|
data[i] = ''
|
|
if not isinstance(data[i], str):
|
|
data[i] = str(round(data[i]))
|
|
lines.append('|'.join(str(d) for d in data))
|
|
else:
|
|
self.l10n_mx_diot_get_values(values, data_87, partner)
|
|
# Global Operations
|
|
if any(data_87):
|
|
for i in range(7, 53):
|
|
if not data_87[i]:
|
|
data_87[i] = ''
|
|
if not isinstance(data_87[i], str):
|
|
data_87[i] = str(round(data_87[i]))
|
|
lines.append('|'.join(str(d) for d in data_87))
|
|
|
|
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 l10n_mx_diot_get_values(self, values, data, partner):
|
|
is_foreign_partner = values.get('third_party_code') != '04'
|
|
is_global = values.get('operation_type_code') == '87'
|
|
# Non-numerical
|
|
data[0] = values.get('third_party_code') # Supplier Type
|
|
data[1] = values.get('operation_type_code') # Operation Type
|
|
data[2] = 'XAXX010101000' if is_global else (values.get('partner_vat_number') if not is_foreign_partner else '') # Tax Number
|
|
data[3] = '' if is_global else (values.get('partner_vat_number') if is_foreign_partner else '') # Tax Number for Foreigners
|
|
data[4] = '' if is_global else (''.join(self.str_format(partner.name)).encode('utf-8').strip().decode('utf-8') if is_foreign_partner else '') # Name
|
|
data[5] = '' if is_global else (values.get('country_code') if is_foreign_partner else '') # Country
|
|
data[6] = '' # Fiscal Jurisdiction specified (for manual entry)
|
|
# Sums and refunds
|
|
data[7] += float(values.get('paid_8_n_wnc', 0)) # 8% Northern +NC paid
|
|
data[8] += float(values.get('refunds_8_n', 0)) # 8% Northern refunds
|
|
data[9] += float(values.get('paid_8_s_wnc', 0)) # 8% Southern +NC paid
|
|
data[10] += float(values.get('refunds_8_s', 0)) # 8% Southern refunds
|
|
data[11] += float(values.get('paid_16_wnc', 0)) # 16% +NC paid
|
|
data[12] += float(values.get('refunds_16', 0)) # 16% refunds
|
|
data[13] += float(values.get('paid_16_imp_wnc', 0)) # 16% imports +NC paid
|
|
data[14] += float(values.get('refunds_16_imp', 0)) # 16% imports refunds
|
|
data[15] += float(values.get('paid_16_imp_int_wnc', 0)) # 16% intangible imports +NC paid
|
|
data[16] += float(values.get('refunds_16_imp_int', 0)) # 16% int imp refunds
|
|
# Creditable VAT
|
|
data[17] += float(values.get('paid_8_tax', 0)) # 8% Northern paid
|
|
data[19] += float(values.get('paid_8_s_tax', 0)) # 8% Southern paid
|
|
data[21] += float(values.get('paid_16_tax', 0)) # 16% VAT exclusive base
|
|
data[23] += float(values.get('importation_16_tax', 0)) # 16% import VAT exclusive base
|
|
data[25] += float(values.get('paid_16_imp_int_tax', 0)) # 16% int imp VAT exclusive base
|
|
# Non-creditable VAT
|
|
data[27] += float(values.get('paid_8_non_cred_tax', 0)) # 8% Northern NC paid
|
|
data[31] += float(values.get('paid_8_s_nc_tax', 0)) # 8% Southern NC paid
|
|
data[35] += float(values.get('paid_16_non_cred_tax', 0)) # 16% NC paid
|
|
data[39] += float(values.get('paid_16_imp_nc_tax', 0)) # 16% imports NC paid
|
|
data[43] += float(values.get('paid_16_imp_int_nc_tax', 0)) # 16% non tangible imports NC paid
|
|
# Additional data
|
|
data[47] += float(values.get('withheld', 0)) # DIOT:Retention base
|
|
data[48] += float(values.get('exempt_imp', 0)) # DIOT:Import Exempt base
|
|
data[49] += float(values.get('exempt', 0)) # DIOT:Exempt base
|
|
data[50] += float(values.get('paid_0', 0)) # 0% payments
|
|
data[51] += float(values.get('no_obj', 0)) # No tax object
|
|
# Declaration
|
|
data[53] = '02' if is_global else '01' # "Hereby I declare that I rightfully credited VAT"
|
|
|
|
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 = 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_tax', 'paid_16_non_cred_tax', 'paid_8_tax', 'paid_8_non_cred_tax', 'importation_16_tax',
|
|
'paid_0', 'exempt', 'withheld', 'refunds_8_n', 'refunds_16', 'refunds_16_imp'
|
|
)):
|
|
# 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_tax', 0))) or '' # 16%
|
|
data[36] = round(float(values.get('paid_8_tax', 0))) or '' # 8%
|
|
data[39] = round(float(values.get('importation_16_tax', 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_8_n', 0)) + float(values.get('refunds_16', 0)) + float(values.get('refunds_16_imp', 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:
|
|
if not partner_id:
|
|
raise UserError(_("The report cannot be generated because there are entries with tax amounts but no partner assigned."))
|
|
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)
|