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

321 lines
15 KiB
Python

# -*- coding: utf-8 -*-
from odoo import fields, models, api, _
from odoo.exceptions import ValidationError
from odoo.tools.sql import column_exists, create_column
from odoo.tools import float_round
import re
from collections import defaultdict
CUSTOM_NUMBERS_PATTERN = re.compile(r'[0-9]{2} [0-9]{2} [0-9]{4} [0-9]{7}')
class AccountMove(models.Model):
_inherit = 'account.move'
l10n_mx_edi_external_trade = fields.Boolean(
string="Need external trade?",
readonly=False, store=True,
compute='_compute_l10n_mx_edi_external_trade',
help="If this field is active, the CFDI that generates this invoice will include the complement "
"'External Trade'.")
l10n_mx_edi_external_trade_type = fields.Selection(
selection=[
('02', 'Definitive'),
('03', 'Temporary'),
],
string="External Trade",
readonly=False, store=True,
compute='_compute_l10n_mx_edi_external_trade_type',
help="If this field is 02, the CFDI will include the complement.")
def _auto_init(self):
"""
Create compute stored field l10n_mx_edi_external_trade
here to avoid MemoryError on large databases.
"""
if not column_exists(self.env.cr, 'account_move', 'l10n_mx_edi_external_trade'):
create_column(self.env.cr, 'account_move', 'l10n_mx_edi_external_trade', 'boolean')
# _compute_l10n_mx_edi_external_trade uses res_partner.l10n_mx_edi_external_trade,
# which is a new field in this module hence all values set to False.
self.env.cr.execute("UPDATE account_move set l10n_mx_edi_external_trade=FALSE;")
if not column_exists(self.env.cr, 'account_move', 'l10n_mx_edi_external_trade_type'):
create_column(self.env.cr, 'account_move', 'l10n_mx_edi_external_trade_type', 'varchar')
return super()._auto_init()
# -------------------------------------------------------------------------
# CFDI: HELPERS
# -------------------------------------------------------------------------
@api.depends('l10n_mx_edi_external_trade_type')
def _compute_l10n_mx_edi_external_trade(self):
for move in self:
move.l10n_mx_edi_external_trade = move.l10n_mx_edi_external_trade_type == '02'
@api.depends('partner_id', 'partner_id.l10n_mx_edi_external_trade_type')
def _compute_l10n_mx_edi_external_trade_type(self):
for move in self:
move.l10n_mx_edi_external_trade_type = move.partner_id.l10n_mx_edi_external_trade_type
# -------------------------------------------------------------------------
# CFDI
# -------------------------------------------------------------------------
def _l10n_mx_edi_add_invoice_cfdi_values(self, cfdi_values, percentage_paid=None, global_invoice=False):
# EXTENDS 'l10n_mx_edi'
self.ensure_one()
if self.journal_id.l10n_mx_address_issued_id:
cfdi_values['issued_address'] = self.journal_id.l10n_mx_address_issued_id
super()._l10n_mx_edi_add_invoice_cfdi_values(cfdi_values, percentage_paid=percentage_paid, global_invoice=global_invoice)
if cfdi_values.get('errors'):
return
cfdi_values['exportacion'] = self.l10n_mx_edi_external_trade_type or '01'
# External Trade
ext_trade_values = cfdi_values['comercio_exterior'] = {}
if self.l10n_mx_edi_external_trade_type == '02':
# Customer.
customer_values = cfdi_values['receptor']
customer = customer_values['customer']
if customer_values['rfc'] == 'XEXX010101000':
cfdi_values['receptor']['num_reg_id_trib'] = customer.vat
# A value must be registered in the ResidenciaFiscal field when information is registered in the
# NumRegIdTrib field.
cfdi_values['receptor']['residencia_fiscal'] = customer.country_id.l10n_mx_edi_code
ext_trade_values['receptor'] = {
**cfdi_values['receptor'],
'curp': customer.l10n_mx_edi_curp,
'calle': customer.street_name,
'numero_exterior': customer.street_number,
'numero_interior': customer.street_number2,
'colonia': customer.l10n_mx_edi_colony_code,
'localidad': customer.l10n_mx_edi_locality_id.code,
'municipio': customer.city_id.l10n_mx_edi_code,
'estado': customer.state_id.code,
'pais': customer.country_id.l10n_mx_edi_code,
'codigo_postal': customer.zip,
}
# Supplier.
supplier_values = cfdi_values['emisor']
supplier = supplier_values['supplier']
ext_trade_values['emisor'] = {
'curp': supplier.l10n_mx_edi_curp,
'calle': supplier.street_name,
'numero_exterior': supplier.street_number,
'numero_interior': supplier.street_number2,
'colonia': supplier.l10n_mx_edi_colony_code,
'localidad': supplier.l10n_mx_edi_locality_id.code,
'municipio': supplier.city_id.l10n_mx_edi_code,
'estado': supplier.state_id.code,
'pais': supplier.country_id.l10n_mx_edi_code,
'codigo_postal': supplier.zip,
}
# Shipping.
shipping = self.partner_shipping_id
if shipping != customer:
shipping_cfdi_values = dict(cfdi_values)
# In case of COMEX we need to fill "NumRegIdTrib" with the real tax id of the customer
# but let the generic RFC.
self.env['l10n_mx_edi.document']._add_customer_cfdi_values(
shipping_cfdi_values,
customer=shipping,
usage=cfdi_values['receptor']['uso_cfdi'],
to_public=self.l10n_mx_edi_cfdi_to_public,
)
shipping_values = shipping_cfdi_values['receptor']
if (
shipping.country_id == shipping.commercial_partner_id.country_id
and shipping_values['rfc'] == 'XEXX010101000'
):
shipping_vat = shipping.vat.strip() if shipping.vat else None
else:
shipping_vat = None
if shipping.country_id.l10n_mx_edi_code == 'MEX':
colony = shipping.l10n_mx_edi_colony_code
locality = shipping.l10n_mx_edi_locality_id.code
city = shipping.city_id.l10n_mx_edi_code
else:
colony = shipping.l10n_mx_edi_colony
locality = shipping.l10n_mx_edi_locality
city = shipping.city
if shipping.country_id.l10n_mx_edi_code in ('MEX', 'USA', 'CAN') or shipping.state_id.code:
state = shipping.state_id.code
else:
state = 'NA'
ext_trade_values['destinario'] = {
'num_reg_id_trib': shipping_vat,
'nombre': shipping.name,
'calle': shipping.street_name,
'numero_exterior': shipping.street_number,
'numero_interior': shipping.street_number2,
'colonia': colony,
'localidad': locality,
'municipio': city,
'estado': state,
'pais': shipping.country_id.l10n_mx_edi_code,
'codigo_postal': shipping.zip,
}
# Certificate.
ext_trade_values['certificado_origen'] = '1' if self.l10n_mx_edi_cer_source else '0'
ext_trade_values['num_certificado_origen'] = self.l10n_mx_edi_cer_source
# Rate.
mxn = self.env["res.currency"].search([('name', '=', 'MXN')], limit=1)
usd = self.env["res.currency"].search([('name', '=', 'USD')], limit=1)
ext_trade_values['tipo_cambio_usd'] = usd._get_conversion_rate(usd, mxn, self.company_id, self.date)
if ext_trade_values['tipo_cambio_usd']:
to_usd_rate = (cfdi_values['tipo_cambio'] or 1.0) / ext_trade_values['tipo_cambio_usd']
else:
to_usd_rate = 0.0
# Misc.
if customer.country_id in self.env.ref('base.europe').country_ids:
ext_trade_values['numero_exportador_confiable'] = self.company_id.l10n_mx_edi_num_exporter
else:
ext_trade_values['numero_exportador_confiable'] = None
ext_trade_values['incoterm'] = self.invoice_incoterm_id.code
ext_trade_values['observaciones'] = self.narration
# Details per product.
product_values_map = defaultdict(lambda: {
'quantity': 0.0,
'price_unit': 0.0,
'total': 0.0,
})
for line_vals in cfdi_values['conceptos_list']:
line = line_vals['line']['record']
product_values_map[line.product_id]['quantity'] += line.l10n_mx_edi_qty_umt
product_values_map[line.product_id]['price_unit'] += line.l10n_mx_edi_price_unit_umt
product_values_map[line.product_id]['total'] += line_vals['importe']
ext_trade_values['total_usd'] = 0.0
ext_trade_values['mercancia_list'] = []
for product, product_values in product_values_map.items():
total_usd = float_round(product_values['total'] * to_usd_rate, precision_digits=4)
ext_trade_values['mercancia_list'].append({
'no_identificacion': product.default_code,
'fraccion_arancelaria': product.l10n_mx_edi_tariff_fraction_id.code,
'cantidad_aduana': product_values['quantity'],
'unidad_aduana': product.l10n_mx_edi_umt_aduana_id.l10n_mx_edi_code_aduana,
'valor_unitario_udana': float_round(product_values['price_unit'] * to_usd_rate, precision_digits=6),
'valor_dolares': total_usd,
})
ext_trade_values['total_usd'] += total_usd
else:
# Invoice lines.
for line_vals in cfdi_values['conceptos_list']:
line_vals['informacion_aduanera_list'] = line_vals['line']['record']._l10n_mx_edi_get_custom_numbers()
class AccountMoveLine(models.Model):
_inherit = "account.move.line"
l10n_mx_edi_customs_number = fields.Char(
help='Optional field for entering the customs information in the case '
'of first-hand sales of imported goods or in the case of foreign trade'
' operations with goods or services.\n'
'The format must be:\n'
' - 2 digits of the year of validation followed by two spaces.\n'
' - 2 digits of customs clearance followed by two spaces.\n'
' - 4 digits of the serial number followed by two spaces.\n'
' - 1 digit corresponding to the last digit of the current year, '
'except in case of a consolidated customs initiated in the previous '
'year of the original request for a rectification.\n'
' - 6 digits of the progressive numbering of the custom.',
string='Customs number',
copy=False)
l10n_mx_edi_umt_aduana_id = fields.Many2one(
comodel_name='uom.uom',
string="UMT Aduana",
readonly=True, store=True, compute_sudo=True,
related='product_id.l10n_mx_edi_umt_aduana_id',
help="Used in complement 'Comercio Exterior' to indicate in the products the TIGIE Units of Measurement. "
"It is based in the SAT catalog.")
l10n_mx_edi_qty_umt = fields.Float(
string="Qty UMT",
digits=(16, 3),
readonly=False, store=True,
compute='_compute_l10n_mx_edi_qty_umt',
help="Quantity expressed in the UMT from product. It is used in the attribute 'CantidadAduana' in the CFDI")
l10n_mx_edi_price_unit_umt = fields.Float(
string="Unit Value UMT",
readonly=True, store=True,
compute='_compute_l10n_mx_edi_price_unit_umt',
help="Unit value expressed in the UMT from product. It is used in the attribute 'ValorUnitarioAduana' in the "
"CFDI")
def _auto_init(self):
if not column_exists(self.env.cr, "account_move_line", "l10n_mx_edi_umt_aduana_id"):
create_column(self.env.cr, "account_move_line", "l10n_mx_edi_umt_aduana_id", "int4")
# Since l10n_mx_edi_umt_aduana_id columns does not exist we can assume the columns
# l10n_mx_edi_qty_umt and l10n_mx_edi_price_unit_umt do not exist either
create_column(self.env.cr, "account_move_line", "l10n_mx_edi_qty_umt", "numeric")
create_column(self.env.cr, "account_move_line", "l10n_mx_edi_price_unit_umt", "float8")
return super()._auto_init()
# -------------------------------------------------------------------------
# HELPERS
# -------------------------------------------------------------------------
def _l10n_mx_edi_get_custom_numbers(self):
self.ensure_one()
if self.l10n_mx_edi_customs_number:
return [num.strip() for num in self.l10n_mx_edi_customs_number.split(',')]
else:
return []
# -------------------------------------------------------------------------
# COMPUTE METHODS
# -------------------------------------------------------------------------
@api.depends('l10n_mx_edi_umt_aduana_id', 'product_uom_id', 'quantity')
def _compute_l10n_mx_edi_qty_umt(self):
for line in self:
product_aduana_code = line.l10n_mx_edi_umt_aduana_id.l10n_mx_edi_code_aduana
uom_aduana_code = line.product_uom_id.l10n_mx_edi_code_aduana
if product_aduana_code == uom_aduana_code:
line.l10n_mx_edi_qty_umt = line.quantity
elif '01' in (product_aduana_code or ''):
line.l10n_mx_edi_qty_umt = line.product_id.weight * line.quantity
else:
line.l10n_mx_edi_qty_umt = None
@api.depends('quantity', 'price_unit', 'l10n_mx_edi_qty_umt')
def _compute_l10n_mx_edi_price_unit_umt(self):
for line in self:
if line.l10n_mx_edi_qty_umt:
line.l10n_mx_edi_price_unit_umt = line.quantity * line.price_unit / line.l10n_mx_edi_qty_umt
else:
line.l10n_mx_edi_price_unit_umt = line.price_unit
# -------------------------------------------------------------------------
# CONSTRAINT METHODS
# -------------------------------------------------------------------------
@api.constrains('l10n_mx_edi_customs_number')
def _check_l10n_mx_edi_customs_number(self):
invalid_lines = self.env['account.move.line']
for line in self:
custom_numbers = line._l10n_mx_edi_get_custom_numbers()
if any(not CUSTOM_NUMBERS_PATTERN.match(custom_number) for custom_number in custom_numbers):
invalid_lines |= line
if not invalid_lines:
return
raise ValidationError(_(
"Custom numbers set on invoice lines are invalid and should have a pattern like: 15 48 3009 0001234:\n%(invalid_message)s",
invalid_message='\n'.join('%s (id=%s)' % (line.l10n_mx_edi_customs_number, line.id) for line in invalid_lines),
))