806 lines
38 KiB
Python
806 lines
38 KiB
Python
import json
|
|
import uuid
|
|
|
|
import requests
|
|
from werkzeug.urls import url_quote, url_quote_plus
|
|
from datetime import datetime
|
|
|
|
from odoo import api, models, fields, _
|
|
from odoo.addons.base.models.ir_qweb import keep_query
|
|
from odoo.addons.l10n_mx_edi.models.l10n_mx_edi_document import CANCELLATION_REASON_SELECTION, CFDI_DATE_FORMAT
|
|
from odoo.exceptions import UserError
|
|
from odoo.fields import Domain
|
|
|
|
from .product_template import MX_PACKAGING_CATALOG
|
|
|
|
MAPBOX_GEOCODE_URL = 'https://api.mapbox.com/geocoding/v5/mapbox.places/'
|
|
MAPBOX_MATRIX_URL = 'https://api.mapbox.com/directions-matrix/v1/mapbox/driving/'
|
|
|
|
|
|
class StockPicking(models.Model):
|
|
_inherit = 'stock.picking'
|
|
|
|
l10n_mx_edi_is_delivery_guide_needed = fields.Boolean(
|
|
compute='_compute_l10n_mx_edi_is_delivery_guide_needed'
|
|
)
|
|
l10n_mx_edi_is_cfdi_needed = fields.Boolean(
|
|
compute='_compute_l10n_mx_edi_is_cfdi_needed',
|
|
store=True,
|
|
)
|
|
l10n_mx_edi_document_ids = fields.One2many(
|
|
comodel_name='l10n_mx_edi.document',
|
|
inverse_name='picking_id',
|
|
copy=False,
|
|
readonly=True,
|
|
)
|
|
l10n_mx_edi_cfdi_state = fields.Selection(
|
|
string="CFDI status",
|
|
selection=[
|
|
('sent', 'Signed'),
|
|
('cancel', 'Cancelled'),
|
|
],
|
|
store=True,
|
|
copy=False,
|
|
tracking=True,
|
|
compute="_compute_l10n_mx_edi_cfdi_state_and_attachment",
|
|
)
|
|
l10n_mx_edi_cfdi_sat_state = fields.Selection(
|
|
string="SAT status",
|
|
selection=[
|
|
('valid', "Validated"),
|
|
('not_found', "Not Found"),
|
|
('not_defined', "Not Defined"),
|
|
('cancelled', "Cancelled"),
|
|
('error', "Error"),
|
|
],
|
|
store=True,
|
|
copy=False,
|
|
tracking=True,
|
|
compute="_compute_l10n_mx_edi_cfdi_state_and_attachment",
|
|
)
|
|
l10n_mx_edi_cfdi_attachment_id = fields.Many2one(
|
|
comodel_name='ir.attachment',
|
|
store=True,
|
|
copy=False,
|
|
compute='_compute_l10n_mx_edi_cfdi_state_and_attachment',
|
|
)
|
|
l10n_mx_edi_update_sat_needed = fields.Boolean(compute='_compute_l10n_mx_edi_update_sat_needed')
|
|
l10n_mx_edi_external_trade = fields.Char(compute='_compute_l10n_mx_edi_external_trade')
|
|
l10n_mx_edi_cfdi_uuid = fields.Char(
|
|
string="Fiscal Folio",
|
|
compute='_compute_l10n_mx_edi_cfdi_uuid',
|
|
copy=False,
|
|
store=True,
|
|
help="Folio in electronic invoice, is returned by SAT when send to stamp.",
|
|
)
|
|
l10n_mx_edi_cfdi_origin = fields.Char(
|
|
string='CFDI Origin',
|
|
copy=False,
|
|
help="Specify the existing Fiscal Folios to replace. Prepend with '04|'",
|
|
)
|
|
l10n_mx_edi_cfdi_cancel_picking_id = fields.Many2one(
|
|
comodel_name='stock.picking',
|
|
string="Substituted By",
|
|
compute='_compute_l10n_mx_edi_cfdi_cancel_picking_id',
|
|
)
|
|
|
|
l10n_mx_edi_src_lat = fields.Float(
|
|
string='Source Latitude',
|
|
related='picking_type_id.warehouse_id.partner_id.partner_latitude')
|
|
l10n_mx_edi_src_lon = fields.Float(
|
|
string='Source Longitude',
|
|
related='picking_type_id.warehouse_id.partner_id.partner_longitude')
|
|
l10n_mx_edi_des_lat = fields.Float(
|
|
string='Destination Latitude',
|
|
related='partner_id.partner_latitude')
|
|
l10n_mx_edi_des_lon = fields.Float(
|
|
string='Destination Longitude',
|
|
related='partner_id.partner_longitude')
|
|
l10n_mx_edi_distance = fields.Integer('Distance to Destination (KM)', copy=False)
|
|
|
|
l10n_mx_edi_transport_type = fields.Selection(
|
|
selection=[
|
|
('00', 'No Federal Highways'),
|
|
('01', 'Federal Transport'),
|
|
],
|
|
string='Transport Type',
|
|
copy=False,
|
|
help='Specify the transportation method. The Delivery Guide will contain the Complemento Carta Porte only when'
|
|
' federal transport is used')
|
|
l10n_mx_edi_vehicle_id = fields.Many2one(
|
|
comodel_name='fleet.vehicle',
|
|
string='Vehicle Setup',
|
|
domain="[('l10n_mx_is_freight_vehicle', '=', True)]",
|
|
ondelete='restrict',
|
|
copy=False,
|
|
help='The vehicle used for Federal Transport')
|
|
l10n_mx_edi_idccp = fields.Char(
|
|
string="IdCCP",
|
|
help="Additional UUID for the Delivery Guide.",
|
|
compute='_compute_l10n_mx_edi_idccp',
|
|
)
|
|
l10n_mx_edi_gross_vehicle_weight = fields.Float(
|
|
string="Gross Vehicle Weight",
|
|
digits=(16, 2),
|
|
compute="_compute_l10n_mx_edi_gross_vehicle_weight",
|
|
help='The weight is computed in accordance with the Carta Porte filling guide. '
|
|
'This means that the reported weight in tons includes the sum of the vehicle weight, the freight weight, '
|
|
'and the "extra weight" used to account for the drivers and their luggage.'
|
|
)
|
|
l10n_mx_edi_extra_weight = fields.Float(
|
|
string="Extra Weight",
|
|
help='Used to report the weight of the drivers, the luggage and any other extras.'
|
|
)
|
|
l10n_mx_edi_customs_regime_ids = fields.Many2many(
|
|
string="Customs Regimes",
|
|
help="Regimes associated to the good's transfer (import or export).",
|
|
comodel_name='l10n_mx_edi.customs.regime',
|
|
ondelete='restrict',
|
|
)
|
|
l10n_mx_edi_customs_document_type_id = fields.Many2one(
|
|
string="Customs Document Type",
|
|
help="Type of customs document associated with the transport of the goods.",
|
|
comodel_name='l10n_mx_edi.customs.document.type',
|
|
ondelete="restrict",
|
|
)
|
|
l10n_mx_edi_customs_document_type_code = fields.Char(related='l10n_mx_edi_customs_document_type_id.code')
|
|
l10n_mx_edi_pedimento_number = fields.Char(
|
|
string="Pedimento Number",
|
|
help="Pedimento number associated with the import of the goods.",
|
|
)
|
|
l10n_mx_edi_customs_doc_identification = fields.Char(
|
|
string="Customs Document Identification",
|
|
help="Folio of the customs document associated with the import of the goods.",
|
|
)
|
|
l10n_mx_edi_importer_id = fields.Many2one(
|
|
string="Importer",
|
|
help="Importer registered in the customs documentation.",
|
|
comodel_name='res.partner',
|
|
ondelete="restrict",
|
|
)
|
|
l10n_mx_edi_delivery_date = fields.Datetime(
|
|
string="Delivery Date",
|
|
help="Date of arrival at destination, required for the Carta Porte",
|
|
)
|
|
|
|
def _l10n_mx_edi_get_cartaporte_pdf_values(self):
|
|
self.ensure_one()
|
|
|
|
existing_cfdi_values = self.env['l10n_mx_edi.document']._decode_cfdi_attachment(self.l10n_mx_edi_cfdi_attachment_id.raw)
|
|
cfdi_values = self.env['l10n_mx_edi.document']._get_company_cfdi_values(self.company_id)
|
|
self.env['l10n_mx_edi.document']._add_certificate_cfdi_values(cfdi_values)
|
|
self._l10n_mx_edi_add_picking_cfdi_values(cfdi_values)
|
|
if existing_cfdi_values.get('carta_porte_idccp'):
|
|
cfdi_values['idccp'] = existing_cfdi_values['carta_porte_idccp']
|
|
|
|
warehouse_partner = self.picking_type_id.warehouse_id.partner_id
|
|
|
|
figure_types_dict = dict(self.env['l10n_mx_edi.figure']._fields['type'].selection)
|
|
vehicle_configs_dict = dict(self.env['fleet.vehicle']._fields['l10n_mx_vehicle_config'].selection)
|
|
transport_types_dict = dict(self.env['stock.picking']._fields['l10n_mx_edi_transport_type'].selection)
|
|
|
|
ubicacion_fields = (
|
|
'id_ubicacion',
|
|
'rfc_remitente_destinatario',
|
|
'num_reg_id_trib',
|
|
'residencia_fiscal',
|
|
'fecha_hora_salida_llegada'
|
|
)
|
|
|
|
origin_partner = self.partner_id if self.picking_type_code == 'incoming' else warehouse_partner
|
|
destination_partner = self.partner_id if self.picking_type_code == 'outgoing' else warehouse_partner
|
|
|
|
origen_domicilio = {
|
|
field: value or "-" for field, value in cfdi_values['origen']['domicilio'].items()
|
|
}
|
|
destino_domicilio = {
|
|
field: value or "-" for field, value in cfdi_values['destino']['domicilio'].items()
|
|
}
|
|
|
|
# Add legible data to the origin and destination addresses
|
|
if origen_domicilio['municipio'] != origin_partner.city:
|
|
origen_domicilio['municipio'] += f" - {origin_partner.city}"
|
|
if origen_domicilio['estado'] != origin_partner.state_id.name:
|
|
origen_domicilio['estado'] += f" - {origin_partner.state_id.name}"
|
|
if origen_domicilio['pais'] != origin_partner.country_id.name:
|
|
origen_domicilio['pais'] += f" - {origin_partner.country_id.name}"
|
|
|
|
if destino_domicilio['municipio'] != destination_partner.city:
|
|
destino_domicilio['municipio'] += f" - {destination_partner.city}"
|
|
if destino_domicilio['estado'] != destination_partner.state_id.name:
|
|
destino_domicilio['estado'] += f" - {destination_partner.state_id.name}"
|
|
if destino_domicilio['pais'] != destination_partner.country_id.name:
|
|
destino_domicilio['pais'] += f" - {destination_partner.country_id.name}"
|
|
|
|
contains_hazardous_materials = any(self.move_ids.product_id.mapped('l10n_mx_edi_hazardous_material_code_id'))
|
|
|
|
if self.l10n_mx_edi_external_trade:
|
|
external_trade_vals = {
|
|
'external_trade': True,
|
|
'transp_internac': "Sí",
|
|
'entrada_salida_merc': cfdi_values.get('entrada_salida_merc', "-"),
|
|
'regimenes_aduanero': ", ".join(cfdi_values.get('regimenes_aduanero', ["-"])),
|
|
'pais_origen_destino': f"{self.partner_id.country_id.l10n_mx_edi_code} - {self.partner_id.country_id.name}",
|
|
'via_entrada_salida': f"{self.l10n_mx_edi_transport_type} - {transport_types_dict.get(self.l10n_mx_edi_transport_type, '')}",
|
|
}
|
|
else:
|
|
external_trade_vals = {
|
|
'external_trade': False,
|
|
'transp_internac': "No",
|
|
}
|
|
|
|
# Get stamp date in ISO 8601 Format for the QR code
|
|
cfdi_infos = self.env['l10n_mx_edi.document']._decode_cfdi_attachment(self.l10n_mx_edi_cfdi_attachment_id.raw)
|
|
stamp_date = datetime.strptime(cfdi_infos['stamp_date'], "%Y-%m-%d %H:%M:%S").isoformat()
|
|
|
|
# Generate QR code of the URL to access the service regarding the current guide document (legal requirement)
|
|
barcode_value = url_quote_plus(f"https://verificacfdi.facturaelectronica.sat.gob.mx/verificaccp/default.aspx?"
|
|
f"IdCCP={cfdi_values['idccp']}&"
|
|
f"FechaOrig={cfdi_values['cfdi_date']}&"
|
|
f"FechaTimb={stamp_date}")
|
|
barcode_src = f'/report/barcode/?barcode_type=QR&value={barcode_value}&width=180&height=180&quiet=0'
|
|
|
|
return {
|
|
**external_trade_vals,
|
|
'idccp': cfdi_values['idccp'] or "-",
|
|
'total_dist_recorrida': self.l10n_mx_edi_distance or "-",
|
|
'peso_bruto_total': cfdi_values['format_float'](sum(self.move_ids.mapped('weight')), 3),
|
|
'unidad_peso': cfdi_values['weight_uom'].unspsc_code_id.code or "-",
|
|
'num_total_mercancias': len(self.move_ids),
|
|
'origen_domicilio': origen_domicilio,
|
|
'destino_domicilio': destino_domicilio,
|
|
'origen_ubicacion': {
|
|
field: cfdi_values['origen'][field] or "-" for field in ubicacion_fields
|
|
},
|
|
'destino_ubicacion': {
|
|
field: cfdi_values['destino'][field] or "-" for field in (*ubicacion_fields, 'distancia_recorrida')
|
|
},
|
|
'l10n_mx_transport_perm_sct': self.l10n_mx_edi_vehicle_id.l10n_mx_transport_perm_sct or "-",
|
|
'num_permiso_sct': self.l10n_mx_edi_vehicle_id.l10n_mx_transport_perm_number or "-",
|
|
'config_vehicular': f"{self.l10n_mx_edi_vehicle_id.l10n_mx_vehicle_config} - {vehicle_configs_dict.get(self.l10n_mx_edi_vehicle_id.l10n_mx_vehicle_config, '')}",
|
|
'peso_bruto_vehicular': cfdi_values['peso_bruto_vehicular'] or "-",
|
|
'placa_vm': self.l10n_mx_edi_vehicle_id.license_plate or "-",
|
|
'anio_modelo_vm': self.l10n_mx_edi_vehicle_id.model_year or "-",
|
|
'asegura_resp_civil': self.l10n_mx_edi_vehicle_id.l10n_mx_transport_insurer or "-",
|
|
'poliza_resp_civil': self.l10n_mx_edi_vehicle_id.l10n_mx_transport_insurance_policy or "-",
|
|
'asegura_med_ambiente': self.l10n_mx_edi_vehicle_id.l10n_mx_environment_insurer if contains_hazardous_materials else "-",
|
|
'poliza_med_ambiente': self.l10n_mx_edi_vehicle_id.l10n_mx_environment_insurance_policy if contains_hazardous_materials else "-",
|
|
'figures': [
|
|
{
|
|
'tipo_figura': f"{figure.type} - {figure_types_dict.get(figure.type, '')}",
|
|
'num_licencia': (figure.type == '01' and figure.operator_id.l10n_mx_edi_operator_licence) or "-",
|
|
'num_reg_id_trib_figura': figure.operator_id.vat or "-"
|
|
if figure.operator_id.country_id.l10n_mx_edi_code != 'MEX'
|
|
else "-",
|
|
'residencia_fiscal_figura': f"{figure.operator_id.country_id.l10n_mx_edi_code} - {figure.operator_id.country_id.name}" or "-"
|
|
if figure.operator_id.country_id.l10n_mx_edi_code != 'MEX'
|
|
else "-",
|
|
}
|
|
for figure in self.l10n_mx_edi_vehicle_id.l10n_mx_figure_ids.sorted('type')
|
|
],
|
|
'barcode_src': barcode_src,
|
|
}
|
|
|
|
def _l10n_mx_edi_get_extra_picking_report_values(self):
|
|
self.ensure_one()
|
|
return self.env['l10n_mx_edi.document']._l10n_mx_edi_get_extra_common_report_values(self.l10n_mx_edi_cfdi_attachment_id)
|
|
|
|
def _get_mail_thread_data_attachments(self):
|
|
# EXTENDS 'stock'
|
|
return super()._get_mail_thread_data_attachments() \
|
|
- self.l10n_mx_edi_document_ids.attachment_id \
|
|
+ self.l10n_mx_edi_cfdi_attachment_id
|
|
|
|
@api.model
|
|
def _l10n_mx_edi_get_packaging_desc(self, code):
|
|
return dict(MX_PACKAGING_CATALOG).get(code, None)
|
|
|
|
# -------------------------------------------------------------------------
|
|
# COMPUTE METHODS
|
|
# -------------------------------------------------------------------------
|
|
|
|
@api.depends('company_id', 'picking_type_code')
|
|
def _compute_l10n_mx_edi_is_delivery_guide_needed(self):
|
|
for picking in self:
|
|
picking.l10n_mx_edi_is_delivery_guide_needed = (
|
|
picking.country_code == 'MX'
|
|
and picking.picking_type_code in ('incoming', 'outgoing')
|
|
)
|
|
|
|
@api.depends('company_id', 'state', 'picking_type_code')
|
|
def _compute_l10n_mx_edi_is_cfdi_needed(self):
|
|
for picking in self:
|
|
picking.l10n_mx_edi_is_cfdi_needed = (
|
|
picking.l10n_mx_edi_is_delivery_guide_needed
|
|
and picking.state == 'done'
|
|
)
|
|
|
|
@api.depends('l10n_mx_edi_document_ids.state', 'l10n_mx_edi_document_ids.sat_state')
|
|
def _compute_l10n_mx_edi_cfdi_state_and_attachment(self):
|
|
for picking in self:
|
|
picking.l10n_mx_edi_cfdi_sat_state = picking.l10n_mx_edi_cfdi_sat_state
|
|
picking.l10n_mx_edi_cfdi_state = None
|
|
picking.l10n_mx_edi_cfdi_attachment_id = None
|
|
for doc in picking.l10n_mx_edi_document_ids.sorted():
|
|
if doc.state == 'picking_sent':
|
|
picking.l10n_mx_edi_cfdi_sat_state = doc.sat_state
|
|
picking.l10n_mx_edi_cfdi_state = 'sent'
|
|
picking.l10n_mx_edi_cfdi_attachment_id = doc.attachment_id
|
|
break
|
|
elif doc.state == 'picking_cancel':
|
|
picking.l10n_mx_edi_cfdi_sat_state = doc.sat_state
|
|
picking.l10n_mx_edi_cfdi_state = 'cancel'
|
|
picking.l10n_mx_edi_cfdi_attachment_id = doc.attachment_id
|
|
break
|
|
|
|
@api.depends('l10n_mx_edi_document_ids.state')
|
|
def _compute_l10n_mx_edi_update_sat_needed(self):
|
|
for picking in self:
|
|
picking.l10n_mx_edi_update_sat_needed = bool(
|
|
picking.l10n_mx_edi_document_ids.filtered_domain(
|
|
Domain.OR(self.env['l10n_mx_edi.document']._get_update_sat_status_domains(from_cron=False))
|
|
)
|
|
)
|
|
|
|
@api.depends('l10n_mx_edi_cfdi_attachment_id')
|
|
def _compute_l10n_mx_edi_cfdi_uuid(self):
|
|
for picking in self:
|
|
if picking.l10n_mx_edi_cfdi_attachment_id:
|
|
cfdi_infos = self.env['l10n_mx_edi.document']._decode_cfdi_attachment(picking.l10n_mx_edi_cfdi_attachment_id.raw)
|
|
picking.l10n_mx_edi_cfdi_uuid = cfdi_infos.get('uuid')
|
|
else:
|
|
picking.l10n_mx_edi_cfdi_uuid = None
|
|
|
|
@api.depends('partner_id')
|
|
def _compute_l10n_mx_edi_external_trade(self):
|
|
for picking in self:
|
|
picking.l10n_mx_edi_external_trade = picking.partner_id.country_code != 'MX'
|
|
|
|
@api.depends('l10n_mx_edi_cfdi_uuid')
|
|
def _compute_l10n_mx_edi_cfdi_cancel_picking_id(self):
|
|
for picking in self:
|
|
if picking.company_id and picking.l10n_mx_edi_cfdi_uuid:
|
|
picking.l10n_mx_edi_cfdi_cancel_picking_id = picking.search(
|
|
[
|
|
('l10n_mx_edi_cfdi_origin', '=like', f'04|{picking.l10n_mx_edi_cfdi_uuid}%'),
|
|
('company_id', '=', picking.company_id.id)
|
|
],
|
|
limit=1,
|
|
)
|
|
else:
|
|
picking.l10n_mx_edi_cfdi_cancel_picking_id = None
|
|
|
|
@api.depends('l10n_mx_edi_is_cfdi_needed')
|
|
def _compute_l10n_mx_edi_idccp(self):
|
|
for picking in self:
|
|
if picking.l10n_mx_edi_is_cfdi_needed and not picking.l10n_mx_edi_idccp:
|
|
# The IdCCP must be a 36 characters long RFC 4122 identifier starting with 'CCC'.
|
|
picking.l10n_mx_edi_idccp = f'CCC{str(uuid.uuid4())[3:]}'
|
|
else:
|
|
picking.l10n_mx_edi_idccp = False
|
|
|
|
@api.depends('l10n_mx_edi_vehicle_id.l10n_mx_gross_vehicle_weight', 'weight', 'l10n_mx_edi_extra_weight')
|
|
def _compute_l10n_mx_edi_gross_vehicle_weight(self):
|
|
weight_uom = self.env['product.template']._get_weight_uom_id_from_ir_config_parameter()
|
|
ton_uom = self.env.ref('uom.product_uom_ton')
|
|
|
|
for picking in self:
|
|
picking.l10n_mx_edi_gross_vehicle_weight = (
|
|
picking.l10n_mx_edi_vehicle_id.l10n_mx_gross_vehicle_weight +
|
|
weight_uom._compute_quantity(picking.weight, ton_uom, round=False) +
|
|
weight_uom._compute_quantity(picking.l10n_mx_edi_extra_weight, ton_uom, round=False)
|
|
)
|
|
|
|
# -------------------------------------------------------------------------
|
|
# CFDI: Generation
|
|
# -------------------------------------------------------------------------
|
|
|
|
def _l10n_mx_edi_cfdi_check_external_trade_config(self):
|
|
""" Check the configuration of the partner's address. """
|
|
self.ensure_one()
|
|
errors = []
|
|
if not self.partner_id.zip or not self.partner_id.state_id:
|
|
errors.append(_("A zip code and state are required to generate a delivery guide"))
|
|
return errors
|
|
|
|
def _l10n_mx_edi_cfdi_check_picking_config(self):
|
|
""" Check the configuration of the picking. """
|
|
self.ensure_one()
|
|
errors = []
|
|
if not self.l10n_mx_edi_transport_type:
|
|
errors.append(_("You must select a transport type to generate the delivery guide"))
|
|
if self.move_line_ids.product_id.filtered(lambda product: not product.unspsc_code_id):
|
|
errors.append(_("All products require a UNSPSC Code"))
|
|
if self.l10n_mx_edi_transport_type == '01' and not self.l10n_mx_edi_distance:
|
|
errors.append(_("Distance in KM must be specified when using federal transport"))
|
|
if self.l10n_mx_edi_vehicle_id and not self.l10n_mx_edi_gross_vehicle_weight:
|
|
errors.append(_("Please define a gross vehicle weight."))
|
|
|
|
if self.l10n_mx_edi_external_trade:
|
|
if not self.l10n_mx_edi_customs_document_type_code:
|
|
errors.append(_("Please define a customs regime."))
|
|
# Code '01' corresponds to 'Pedimento'.
|
|
if self.l10n_mx_edi_customs_document_type_code != '01' and not self.l10n_mx_edi_customs_doc_identification:
|
|
errors.append(_("Please define a customs document identification."))
|
|
if self.l10n_mx_edi_importer_id and not self.l10n_mx_edi_importer_id.vat:
|
|
errors.append(_("Please define a VAT number for the importer '%s'.", self.l10n_mx_edi_importer_id.name))
|
|
if any(not move.product_id.l10n_mx_edi_material_type for move in self.move_ids):
|
|
errors.append(_("At least one product is missing a material type."))
|
|
|
|
return errors
|
|
|
|
def _l10n_mx_edi_add_picking_cfdi_values(self, cfdi_values):
|
|
self.ensure_one()
|
|
|
|
self.env['l10n_mx_edi.document']._add_base_cfdi_values(cfdi_values)
|
|
self.env['l10n_mx_edi.document']._add_currency_cfdi_values(cfdi_values, cfdi_values['company'].currency_id)
|
|
self.env['l10n_mx_edi.document']._add_document_name_cfdi_values(cfdi_values, self.name)
|
|
self.env['l10n_mx_edi.document']._add_document_origin_cfdi_values(cfdi_values, self.l10n_mx_edi_cfdi_origin)
|
|
self.env['l10n_mx_edi.document']._add_customer_cfdi_values(cfdi_values, self.partner_id)
|
|
|
|
receptor = cfdi_values['receptor']
|
|
emisor = cfdi_values['emisor']
|
|
|
|
warehouse_partner = self.picking_type_id.warehouse_id.partner_id
|
|
mx_tz = warehouse_partner._l10n_mx_edi_get_cfdi_timezone()
|
|
delivery_date = self.l10n_mx_edi_delivery_date or self.date_done
|
|
|
|
cfdi_values.update({
|
|
'record': self,
|
|
'cfdi_date': self.date_done.astimezone(mx_tz).strftime(CFDI_DATE_FORMAT),
|
|
'scheduled_date': delivery_date.astimezone(mx_tz).strftime(CFDI_DATE_FORMAT),
|
|
'lugar_expedicion': warehouse_partner.zip,
|
|
'moves': self.move_ids.filtered(lambda ml: ml.quantity > 0),
|
|
'weight_uom': self.env['product.template']._get_weight_uom_id_from_ir_config_parameter(),
|
|
})
|
|
|
|
cfdi_values['issued_address'] = warehouse_partner
|
|
cfdi_values['idccp'] = self.l10n_mx_edi_idccp
|
|
cfdi_values['origen'] = {
|
|
'id_ubicacion': f"OR{str(self.location_id.id).rjust(6, '0')}",
|
|
'fecha_hora_salida_llegada': cfdi_values['cfdi_date'],
|
|
'num_reg_id_trib': None,
|
|
'residencia_fiscal': None,
|
|
}
|
|
cfdi_values['destino'] = {
|
|
'id_ubicacion': f"DE{str(self.location_dest_id.id).rjust(6, '0')}",
|
|
'fecha_hora_salida_llegada': cfdi_values['scheduled_date'],
|
|
'num_reg_id_trib': None,
|
|
'residencia_fiscal': None,
|
|
'distancia_recorrida': self.l10n_mx_edi_distance,
|
|
}
|
|
|
|
if self.l10n_mx_edi_vehicle_id:
|
|
cfdi_values['peso_bruto_vehicular'] = self.l10n_mx_edi_gross_vehicle_weight
|
|
else:
|
|
cfdi_values['peso_bruto_vehicular'] = None
|
|
|
|
if self.picking_type_code == 'outgoing':
|
|
cfdi_values['destino']['rfc_remitente_destinatario'] = receptor['rfc']
|
|
if warehouse_partner.country_id.l10n_mx_edi_code != 'MEX':
|
|
cfdi_values['origen']['rfc_remitente_destinatario'] = 'XEXX010101000'
|
|
cfdi_values['origen']['num_reg_id_trib'] = emisor['supplier'].vat
|
|
cfdi_values['origen']['residencia_fiscal'] = warehouse_partner.country_id.l10n_mx_edi_code
|
|
else:
|
|
cfdi_values['origen']['rfc_remitente_destinatario'] = emisor['rfc']
|
|
self._l10n_mx_edi_add_domicilio_cfdi_values(cfdi_values['origen'], warehouse_partner)
|
|
self._l10n_mx_edi_add_domicilio_cfdi_values(cfdi_values['destino'], self.partner_id)
|
|
else:
|
|
cfdi_values['origen']['rfc_remitente_destinatario'] = receptor['rfc']
|
|
if warehouse_partner.country_id.l10n_mx_edi_code != 'MEX':
|
|
cfdi_values['destino']['rfc_remitente_destinatario'] = 'XEXX010101000'
|
|
cfdi_values['destino']['num_reg_id_trib'] = emisor['supplier'].vat
|
|
cfdi_values['destino']['residencia_fiscal'] = warehouse_partner.country_id.l10n_mx_edi_code
|
|
else:
|
|
cfdi_values['destino']['rfc_remitente_destinatario'] = emisor['rfc']
|
|
self._l10n_mx_edi_add_domicilio_cfdi_values(cfdi_values['origen'], self.partner_id)
|
|
self._l10n_mx_edi_add_domicilio_cfdi_values(cfdi_values['destino'], warehouse_partner)
|
|
|
|
if self.l10n_mx_edi_external_trade:
|
|
cfdi_values['tipo_documento'] = self.l10n_mx_edi_customs_document_type_code
|
|
if self.picking_type_code == 'outgoing':
|
|
cfdi_values['destino']['num_reg_id_trib'] = receptor['customer'].vat
|
|
cfdi_values['destino']['residencia_fiscal'] = receptor['customer'].country_id.l10n_mx_edi_code
|
|
else: # picking_type_code in ('incoming', 'internal')
|
|
cfdi_values['origen']['num_reg_id_trib'] = receptor['customer'].vat
|
|
cfdi_values['origen']['residencia_fiscal'] = receptor['customer'].country_id.l10n_mx_edi_code
|
|
if self.picking_type_code in ('outgoing', 'incoming'):
|
|
cfdi_values['entrada_salida_merc'] = 'Salida' if self.picking_type_code == 'outgoing' else 'Entrada'
|
|
cfdi_values['regimenes_aduanero'] = self.l10n_mx_edi_customs_regime_ids.mapped('code')
|
|
|
|
# Code '01' corresponds to 'Pedimento'.
|
|
if self.l10n_mx_edi_customs_document_type_code == '01':
|
|
if self.picking_type_code == 'incoming':
|
|
cfdi_values['num_pedimento'] = self.l10n_mx_edi_pedimento_number
|
|
cfdi_values['rfc_impo'] = self.l10n_mx_edi_importer_id.vat
|
|
else:
|
|
cfdi_values['ident_doc_aduanero'] = self.l10n_mx_edi_customs_doc_identification
|
|
|
|
@api.model
|
|
def _l10n_mx_edi_add_domicilio_cfdi_values(self, cfdi_values, partner):
|
|
cfdi_values['domicilio'] = {
|
|
'calle': partner.street,
|
|
'codigo_postal': partner.zip,
|
|
'colonia': partner.l10n_mx_edi_colony_code,
|
|
'estado': partner.state_id.code,
|
|
'pais': partner.country_id.l10n_mx_edi_code,
|
|
'municipio': partner.city_id.l10n_mx_edi_code or partner.city,
|
|
'localidad': partner.l10n_mx_edi_locality,
|
|
}
|
|
|
|
@api.model
|
|
def _l10n_mx_edi_prepare_picking_cfdi_template(self):
|
|
return 'l10n_mx_edi_stock.cfdi_cartaporte'
|
|
|
|
# -------------------------------------------------------------------------
|
|
# CFDI: DOCUMENTS
|
|
# -------------------------------------------------------------------------
|
|
|
|
def _l10n_mx_edi_cfdi_document_sent_failed(self, error, cfdi_filename=None, cfdi_str=None):
|
|
""" Create/update the invoice document for 'sent_failed'.
|
|
The parameters are provided by '_l10n_mx_edi_prepare_picking_cfdi'.
|
|
|
|
:param error: The error.
|
|
:param cfdi_filename: The optional filename of the cfdi.
|
|
:param cfdi_str: The optional content of the cfdi.
|
|
"""
|
|
self.ensure_one()
|
|
|
|
document_values = {
|
|
'picking_id': self.id,
|
|
'state': 'picking_sent_failed',
|
|
'sat_state': None,
|
|
'message': error,
|
|
}
|
|
if cfdi_filename and cfdi_str:
|
|
document_values['attachment_id'] = {
|
|
'name': cfdi_filename,
|
|
'raw': cfdi_str,
|
|
}
|
|
return self.env['l10n_mx_edi.document']._create_update_picking_document(self, document_values)
|
|
|
|
def _l10n_mx_edi_cfdi_document_sent(self, cfdi_filename, cfdi_str):
|
|
""" Create/update the invoice document for 'sent'.
|
|
The parameters are provided by '_l10n_mx_edi_prepare_picking_cfdi'.
|
|
|
|
:param cfdi_filename: The filename of the cfdi.
|
|
:param cfdi_str: The content of the cfdi.
|
|
"""
|
|
self.ensure_one()
|
|
|
|
document_values = {
|
|
'picking_id': self.id,
|
|
'state': 'picking_sent',
|
|
'sat_state': 'not_defined',
|
|
'message': None,
|
|
'attachment_id': {
|
|
'name': cfdi_filename,
|
|
'raw': cfdi_str,
|
|
'res_model': self._name,
|
|
'res_id': self.id,
|
|
'description': "CFDI",
|
|
},
|
|
}
|
|
return self.env['l10n_mx_edi.document']._create_update_picking_document(self, document_values)
|
|
|
|
def _l10n_mx_edi_cfdi_document_cancel_failed(self, error, cfdi, cancel_reason):
|
|
""" Create/update the invoice document for 'cancel_failed'.
|
|
|
|
:param error: The error.
|
|
:param cfdi: The source cfdi attachment to cancel.
|
|
:param cancel_reason: The reason for this cancel.
|
|
:return: The created/updated document.
|
|
"""
|
|
self.ensure_one()
|
|
|
|
document_values = {
|
|
'picking_id': self.id,
|
|
'state': 'picking_cancel_failed',
|
|
'sat_state': None,
|
|
'message': error,
|
|
'attachment_id': cfdi.attachment_id.id,
|
|
'cancellation_reason': cancel_reason,
|
|
}
|
|
return self.env['l10n_mx_edi.document']._create_update_picking_document(self, document_values)
|
|
|
|
def _l10n_mx_edi_cfdi_document_cancel(self, cfdi, cancel_reason):
|
|
""" Create/update the invoice document for 'cancel'.
|
|
|
|
:param cfdi: The source cfdi attachment to cancel.
|
|
:param cancel_reason: The reason for this cancel.
|
|
:return: The created/updated document.
|
|
"""
|
|
self.ensure_one()
|
|
|
|
document_values = {
|
|
'picking_id': self.id,
|
|
'state': 'picking_cancel',
|
|
'sat_state': 'not_defined',
|
|
'message': None,
|
|
'attachment_id': cfdi.attachment_id.id,
|
|
'cancellation_reason': cancel_reason,
|
|
}
|
|
return self.env['l10n_mx_edi.document']._create_update_picking_document(self, document_values)
|
|
|
|
# -------------------------------------------------------------------------
|
|
# CFDI: FLOWS
|
|
# -------------------------------------------------------------------------
|
|
|
|
def l10n_mx_edi_cfdi_try_send(self):
|
|
""" Try to generate and send the CFDI for the current picking. """
|
|
self.ensure_one()
|
|
|
|
# == Check the config ==
|
|
errors = self._l10n_mx_edi_cfdi_check_external_trade_config() \
|
|
+ self._l10n_mx_edi_cfdi_check_picking_config()
|
|
if errors:
|
|
self._l10n_mx_edi_cfdi_document_sent_failed("\n".join(errors))
|
|
return
|
|
|
|
# == Lock ==
|
|
self.env['res.company']._with_locked_records(self)
|
|
|
|
# == Send ==
|
|
def on_populate(cfdi_values):
|
|
self._l10n_mx_edi_add_picking_cfdi_values(cfdi_values)
|
|
|
|
def on_failure(error, cfdi_filename=None, cfdi_str=None):
|
|
self._l10n_mx_edi_cfdi_document_sent_failed(error, cfdi_filename=cfdi_filename, cfdi_str=cfdi_str)
|
|
|
|
def on_success(_cfdi_values, cfdi_filename, cfdi_str, populate_return=None):
|
|
document = self._l10n_mx_edi_cfdi_document_sent(cfdi_filename, cfdi_str)
|
|
self.message_post(
|
|
body=_("The CFDI document was successfully created and signed by the government."),
|
|
attachment_ids=document.attachment_id.ids,
|
|
)
|
|
|
|
qweb_template = self._l10n_mx_edi_prepare_picking_cfdi_template()
|
|
cfdi_filename = f'CFDI_DeliveryGuide_{self.name}.xml'.replace('/', '')
|
|
self.env['l10n_mx_edi.document']._send_api(
|
|
self.company_id,
|
|
qweb_template,
|
|
cfdi_filename,
|
|
on_populate,
|
|
on_failure,
|
|
on_success,
|
|
)
|
|
|
|
def _l10n_mx_edi_cfdi_post_cancel(self):
|
|
""" Cancel the current picking and drop a message in the chatter.
|
|
This method is only there to unify the flows since they are multiple
|
|
ways to cancel a picking:
|
|
- The user can request a cancellation from Odoo.
|
|
- The user can cancel the picking from the SAT, then update the SAT state in Odoo.
|
|
"""
|
|
self.ensure_one()
|
|
self.message_post(body=_("The CFDI document has been successfully cancelled."))
|
|
|
|
def _l10n_mx_edi_cfdi_try_cancel(self, document):
|
|
""" Try to cancel the CFDI for the current picking.
|
|
|
|
:param document: The source payment document to cancel.
|
|
"""
|
|
self.ensure_one()
|
|
if self.l10n_mx_edi_cfdi_state != 'sent':
|
|
return
|
|
|
|
# == Lock ==
|
|
self.env['res.company']._with_locked_records(self)
|
|
|
|
# == Cancel ==
|
|
substitution_doc = document._get_substitution_document()
|
|
cancel_uuid = substitution_doc.attachment_uuid
|
|
cancel_reason = '01' if cancel_uuid else '02'
|
|
|
|
def on_failure(error):
|
|
self._l10n_mx_edi_cfdi_document_cancel_failed(error, document, cancel_reason)
|
|
|
|
def on_success():
|
|
self._l10n_mx_edi_cfdi_document_cancel(document, cancel_reason)
|
|
self.l10n_mx_edi_cfdi_origin = f'04|{self.l10n_mx_edi_cfdi_uuid}'
|
|
self._l10n_mx_edi_cfdi_post_cancel()
|
|
|
|
document._cancel_api(self.company_id, cancel_reason, on_failure, on_success)
|
|
|
|
def l10n_mx_edi_cfdi_try_cancel(self):
|
|
""" Try to cancel the CFDI for the current picking. """
|
|
self.ensure_one()
|
|
source_document = self.l10n_mx_edi_document_ids.filtered(lambda x: x.state == 'picking_sent')[:1]
|
|
self._l10n_mx_edi_cfdi_try_cancel(source_document)
|
|
|
|
def _l10n_mx_edi_cfdi_update_sat_state(self, document, sat_state, error=None):
|
|
""" Update the SAT state of the document for the current picking.
|
|
|
|
:param document: The CFDI document to be updated.
|
|
:param sat_state: The newly fetched state from the SAT
|
|
:param error: In case of error, the message returned by the SAT.
|
|
"""
|
|
self.ensure_one()
|
|
|
|
# The user manually cancelled the document in the SAT portal.
|
|
if document.state == 'picking_sent' and sat_state == 'cancelled':
|
|
if document.sat_state not in ('valid', 'cancelled', 'skip'):
|
|
document.sat_state = 'skip'
|
|
|
|
document = self._l10n_mx_edi_cfdi_document_cancel(
|
|
document,
|
|
CANCELLATION_REASON_SELECTION[1][0], # Force '02'.
|
|
)
|
|
document.sat_state = sat_state
|
|
self._l10n_mx_edi_cfdi_post_cancel()
|
|
else:
|
|
document.sat_state = sat_state
|
|
|
|
document.message = None
|
|
if sat_state == 'error' and error:
|
|
document.message = error
|
|
self.message_post(body=error)
|
|
|
|
def l10n_mx_edi_cfdi_try_sat(self):
|
|
self.ensure_one()
|
|
documents = self.l10n_mx_edi_document_ids
|
|
|
|
# sudo: pos_order_ids might appear in the domain and the user might not have access to PoS
|
|
documents = documents.sudo().filtered_domain(documents._get_update_sat_status_domain(from_cron=False)).sudo(flag=False)
|
|
for document in documents:
|
|
document._update_sat_state()
|
|
|
|
# -------------------------------------------------------------------------
|
|
# MAPBOX
|
|
# -------------------------------------------------------------------------
|
|
|
|
def _l10n_mx_edi_request_mapbox(self, url, params):
|
|
try:
|
|
fetched_data = requests.get(url, params=params, timeout=10)
|
|
except Exception:
|
|
raise UserError(_('Unable to connect to mapbox'))
|
|
return fetched_data
|
|
|
|
def l10n_mx_edi_action_set_partner_coordinates(self):
|
|
mb_token = self.env['ir.config_parameter'].sudo().get_param('web_map.token_map_box', False)
|
|
if not mb_token:
|
|
raise UserError(_('Please configure MapBox to use this feature'))
|
|
for record in self:
|
|
src = record.picking_type_id.warehouse_id.partner_id.contact_address_complete
|
|
dest = record.partner_id.contact_address_complete
|
|
if not (src and dest):
|
|
raise UserError(_('The warehouse address and the delivery address are required'))
|
|
src_address = url_quote(src)
|
|
url = f'{MAPBOX_GEOCODE_URL}{src_address}.json?'
|
|
fetched_data = record._l10n_mx_edi_request_mapbox(url, {'access_token': mb_token})
|
|
res = json.loads(fetched_data.content)
|
|
if 'features' in res:
|
|
record.picking_type_id.warehouse_id.partner_id.partner_latitude = res['features'][0]['geometry']['coordinates'][0]
|
|
record.picking_type_id.warehouse_id.partner_id.partner_longitude = res['features'][0]['geometry']['coordinates'][1]
|
|
dest_address = url_quote(dest)
|
|
url = f'{MAPBOX_GEOCODE_URL}{dest_address}.json?'
|
|
fetched_data = record._l10n_mx_edi_request_mapbox(url, {'access_token': mb_token})
|
|
res = json.loads(fetched_data.content)
|
|
if 'features' in res:
|
|
record.partner_id.partner_latitude = res['features'][0]['geometry']['coordinates'][0]
|
|
record.partner_id.partner_longitude = res['features'][0]['geometry']['coordinates'][1]
|
|
|
|
def l10n_mx_edi_action_calculate_distance(self):
|
|
mb_token = self.env['ir.config_parameter'].sudo().get_param('web_map.token_map_box', False)
|
|
if not mb_token:
|
|
raise UserError(_('Please configure MapBox to use this feature'))
|
|
params = {
|
|
'sources': 0,
|
|
'destinations': 'all',
|
|
'annotations': 'distance',
|
|
'access_token': mb_token,
|
|
}
|
|
for record in self:
|
|
if record.l10n_mx_edi_src_lat and record.l10n_mx_edi_src_lon \
|
|
and record.l10n_mx_edi_des_lat and record.l10n_mx_edi_des_lon:
|
|
url = f'{MAPBOX_MATRIX_URL}{record.l10n_mx_edi_src_lat},{record.l10n_mx_edi_src_lon};{record.l10n_mx_edi_des_lat},{record.l10n_mx_edi_des_lon}'
|
|
fetched_data = record._l10n_mx_edi_request_mapbox(url, params)
|
|
res = json.loads(fetched_data.content)
|
|
if 'distances' in res:
|
|
record.l10n_mx_edi_distance = res['distances'][0][1] // 1000
|
|
else:
|
|
raise UserError(_('Distance calculation requires both the source and destination coordinates'))
|
|
|
|
def l10n_mx_edi_action_print_cartaporte(self):
|
|
return self.env.ref('l10n_mx_edi_stock.action_report_cartaporte').report_action(self)
|