From 606df1402c05b32e780d45bebb2ae640c0cf4706 Mon Sep 17 00:00:00 2001 From: Katherine Zaoral Date: Tue, 13 Aug 2024 17:28:16 -0300 Subject: [PATCH] wip3 limpieza --- l10n_uy_edi_stock/__manifest__.py | 6 +- .../models/l10n_latam_document_type.py | 2 +- l10n_uy_edi_stock/models/l10n_uy_addenda.py | 11 +- .../models/l10n_uy_edi_document.py | 43 +- l10n_uy_edi_stock/models/stock_picking.py | 503 +++++++++++++----- l10n_uy_edi_stock/views/cfe_template.xml | 76 ++- .../views/l10n_uy_edi_document_views.xml | 17 + .../views/stock_picking_views.xml | 49 +- 8 files changed, 499 insertions(+), 208 deletions(-) create mode 100644 l10n_uy_edi_stock/views/l10n_uy_edi_document_views.xml diff --git a/l10n_uy_edi_stock/__manifest__.py b/l10n_uy_edi_stock/__manifest__.py index 56a69701..563131ba 100644 --- a/l10n_uy_edi_stock/__manifest__.py +++ b/l10n_uy_edi_stock/__manifest__.py @@ -6,14 +6,16 @@ 'sequence': 12, 'author': 'Adhoc', 'depends': [ - 'l10n_uy_edi', + # 'l10n_uy_ux', 'stock_account', - # 'sale_stock', + 'sale_stock', + 'l10n_uy_edi' ], 'data': [ 'data/l10n_latam.document.type.csv', 'views/cfe_template.xml', 'views/stock_picking_views.xml', + 'views/l10n_uy_edi_document_views.xml', ], 'installable': True, 'auto_install': False, diff --git a/l10n_uy_edi_stock/models/l10n_latam_document_type.py b/l10n_uy_edi_stock/models/l10n_latam_document_type.py index 60a7c31d..b8ea925b 100644 --- a/l10n_uy_edi_stock/models/l10n_latam_document_type.py +++ b/l10n_uy_edi_stock/models/l10n_latam_document_type.py @@ -1,7 +1,7 @@ from odoo import fields, models -class L10nAccountDocumentType(models.Model): +class L10nLatamDocumentType(models.Model): _inherit = 'l10n_latam.document.type' diff --git a/l10n_uy_edi_stock/models/l10n_uy_addenda.py b/l10n_uy_edi_stock/models/l10n_uy_addenda.py index 2238cbe7..0d69d0d3 100644 --- a/l10n_uy_edi_stock/models/l10n_uy_addenda.py +++ b/l10n_uy_edi_stock/models/l10n_uy_addenda.py @@ -4,8 +4,9 @@ class L10nUyAddenda(models.Model): _inherit = "l10n_uy_edi.addenda" - - apply_on = fields.Selection(selection_add=[ - ('stock.picking', 'Delivery Guide'), - ('all', 'All CFE'), - ]) + + #Comentamos temeporalmente hasta que este mezclado l10n_uy_ux + # apply_on = fields.Selection(selection_add=[ + # ('stock.picking', 'Delivery Guide'), + # ('all', 'All CFE'), + # ]) diff --git a/l10n_uy_edi_stock/models/l10n_uy_edi_document.py b/l10n_uy_edi_stock/models/l10n_uy_edi_document.py index 4945e94d..9e463471 100644 --- a/l10n_uy_edi_stock/models/l10n_uy_edi_document.py +++ b/l10n_uy_edi_stock/models/l10n_uy_edi_document.py @@ -1,4 +1,4 @@ -from odoo import _, models +from odoo import _, models, fields from odoo.exceptions import UserError from odoo.tools.float_utils import float_repr @@ -7,29 +7,30 @@ class L10nUyEdiDocument(models.Model): _inherit = 'l10n_uy_edi.document' + picking_id = fields.Many2one("stock.picking", readonly=True) + l10n_latam_document_type_id = fields.Many2one(related="picking_id.l10n_latam_document_type_id") + l10n_latam_document_number = fields.Char(related="picking_id.l10n_latam_document_number") + def _is_uy_remito_exp(self): return self.l10n_latam_document_type_id.code == '124' def _is_uy_remito_loc(self): return self.l10n_latam_document_type_id.code == '181' - def _is_uy_remito_type_cfe(self): return self.l10n_latam_document_type_id.internal_type in ['stock_picking'] - def _uy_get_uuid(self): + def _uy_get_uuid(self, remito): """ Extend to properly return picking info Uruware UUID and also A4.1 NroInterno DGI field. Spec (V24) Nº interno que referencia - ALFA50 - Sin validación al comprobante """ - self.ensure_one() - if self.picking_id and not self.move_id: - res = self.picking_id._name + '-' + str(self.picking_id.id) - if self.company_id._l10n_uy_edi_get_environment_type() == 'testing': - res = 'sp' + str(self.picking_id.id) + '-' + self.env.cr.dbname - return res[:50] - return super()._uy_get_uuid() + remito.ensure_one() + res = remito._name + '-' + str(remito.id) + if self.company_id.l10n_uy_edi_ucfe_env == 'testing': + res = 'sp' + str(remito.id) + '-' + remito.env.cr.dbname + return res[:50] def _uy_cfe_B4_IndFact(self, line): """ B4: Indicador de facturación @@ -59,7 +60,6 @@ def _uy_cfe_B4_IndFact(self, line): if self._is_uy_remito_type_cfe(): # Solo implementamos por los momentos en el N/A res = False - else: return super()._uy_cfe_B4_IndFact() @@ -67,9 +67,6 @@ def _uy_cfe_B4_IndFact(self, line): def _uy_cfe_B8_DscItem(self, line): """ B8 Descripcion Adicional del ítem. Maximo 1000 caracteres """ - if self._is_uy_inv_type_cfe(): - return super()._uy_cfe_B8_DscItem(line) - self.ensure_one() res = [] for rec in line.l10n_uy_edi_addenda_ids: @@ -98,24 +95,6 @@ def _uy_cfe_A_receptor(self): return res - def _uy_cfe_C_totals(self): - self.ensure_one() - res = super()._uy_cfe_C_totals() - - # A110 Tipo moneda transacción: Informar para todos menos para e-Rem loc - if self._is_uy_remito_loc(): - res.pop('TpoMoneda') - - # A124 Total Monto Total (NUM 17) - # - Si tipo de CFE= 124 (e-remito de exportación), Valor numérico de 15 enteros y 2 decimales: - # - sino, Valor numérico de 15 enteros y 2 decimales, ≥0 : C124 = SUM(C112:C118) + SUM(C121:C123) - - # A111 Tipo de Cambio: Informar siempre que la moneda sea diferente al peso Uruguayo y no sea e-Rem Loc - if self._is_uy_remito_loc(): - res.pop('TpoCambio') - - return res - def _uy_get_cfe_tag(self): self.ensure_one() if self._is_uy_remito_loc(): diff --git a/l10n_uy_edi_stock/models/stock_picking.py b/l10n_uy_edi_stock/models/stock_picking.py index bca03f8a..21c6442d 100644 --- a/l10n_uy_edi_stock/models/stock_picking.py +++ b/l10n_uy_edi_stock/models/stock_picking.py @@ -1,54 +1,91 @@ +import base64 + from odoo import api, models, fields, _ -from odoo.tools import html2plaintext +from odoo.exceptions import UserError +from odoo.tools import float_repr, float_round, html2plaintext +from odoo.tools.xml_utils import cleanup_xml_node + +from markupsafe import Markup +from lxml import etree + +def format_float(amount, digits=2): + if not amount: + return None + return float_repr(float_round(amount, digits), digits) class StockPicking(models.Model): _inherit = 'stock.picking' - l10n_uy_cfe_id = fields.Many2one("l10n_uy_edi.document", string="Uruguay E-Resguardo CFE", copy=False) + # Need to make it work with document types l10n_latam_document_type_id = fields.Many2one('l10n_latam.document.type', string='Document Type (UY)', copy=False) - l10n_latam_document_number = fields.Char(string='Document Number (UY)', readonly=True, states={'draft': [('readonly', False)]}, copy=False) + l10n_latam_document_number = fields.Char(string='Document Number (UY)', readonly=True, copy=False) + l10n_latam_available_document_type_ids = fields.Many2many('l10n_latam.document.type', compute='_compute_l10n_latam_available_document_types') - # Fields that need to be fill before creating the CFE - l10n_uy_edi_cfe_uuid = fields.Char( - 'Key or UUID CFE', help="Unique identification per CFE in UCFE. Currently is formed by the concatenation of model name initials plust record id", copy=False) + # Need to make it work with EDI (simil to what we have in account.move) + l10n_uy_edi_document_id = fields.Many2one("l10n_uy_edi.document", string="Uruguay E-Invoice CFE", copy=False) + l10n_uy_edi_cfe_uuid = fields.Char(related="l10n_uy_edi_document_id.uuid") + l10n_uy_edi_cfe_state = fields.Selection(related="l10n_uy_edi_document_id.state", store=True) + l10n_uy_edi_error = fields.Text(related="l10n_uy_edi_document_id.message") + l10n_uy_is_cfe = fields.Boolean( + compute="_compute_l10n_uy_is_cfe", + help="Campo tecnico para saber si es un comprobante electronico o no y usarlo en la vista para mostrar o requerir ciertos campos." + " por los momentos lo estamos usando solo para remitos pero podemos extenderlo para otros modelos" + ) l10n_uy_edi_addenda_ids = fields.Many2many( - 'l10n_uy_edi.addenda', string="Addenda & Disclosure", - domain="[('type', 'in', ['issuer', 'receiver', 'cfe_doc', 'addenda'])]") - - l10n_latam_available_document_type_ids = fields.Many2many('l10n_latam.document.type', compute='_compute_l10n_latam_available_document_types') + "l10n_uy_edi.addenda", + string="Addenda & Disclosure", + domain="[('type', 'in', ['issuer', 'receiver', 'cfe_doc', 'addenda'])]", + help="Addendas and Mandatory Disclosure to add on the CFE. They can be added either to the issuer, receiver," + " cfe doc additional info section or to the addenda section. However, the item type should not be set in" + " this field; instead, it should be specified in the invoice lines.") + l10n_uy_edi_cfe_sale_mode = fields.Selection([ + ("1", "General Regime"), + ("2", "Consignment"), + ("3", "Reviewable Price"), + ("4", "Own goods to customs exclaves"), + ("90", "General Regime - exportation of services"), + ("99", "Other transactions"), + ], "Sales Modality", help="This field is used in the XML to create an Export e-Invoice") + l10n_uy_edi_cfe_transport_route = fields.Selection([ + ("1", "Maritime"), + ("2", "Air"), + ("3", "Ground"), + ("8", "N/A"), + ("9", "Other"), + ], "Transportation Route", help="This field is used in the XML to create an Export e-Invoice") + + # New fields only for pickings l10n_uy_transfer_of_goods = fields.Selection( - [('1', 'Venta'), ('2', 'Traslados internos')], + [('1', 'Venta'), + ('2', 'Traslados internos')], string="Traslados de Bienes", + default='1' ) - - l10n_uy_cfe_sale_mod = fields.Selection([ - ('1', 'General Regime'), - ('2', 'Consignment'), - ('3', 'Reviewable Price'), - ('4', 'Own goods to customs exclaves'), - ('90', 'General Regime - exportation of services'), - ('99', 'Other transactions'), - ], 'Sales Modality', help="This field is used in the XML to create an Export e-Delivery Guide") - l10n_uy_cfe_transport_route = fields.Selection([ - ('1', 'Maritime'), - ('2', 'Air'), - ('3', 'Ground'), - ('8', 'N/A'), - ('9', 'Other'), - ], 'Transportation Route', help="This field is used in the XML to create an Export e-Delivery Guide") - l10n_uy_place_of_delivery = fields.Char( + l10n_uy_edi_place_of_delivery = fields.Char( "Place of Delivery", size=100, - help="Indicación de donde se entrega la mercadería o se presta el servicio (Dirección, Sucursal, Puerto, etc,)") - l10n_uy_edi_place_of_delivery = fields.Boolean( - "Place of Delivery", help="CFE: Indication of where the merchandise is delivered or the service is provided" - " (Address, Branch, Port, etc.) if True then we will inform the shipping address's name and street") + " (Address, Branch, Port, etc.) if True then we will inform the shipping address's name and street" + ) + invoice_pdf_report_id = fields.Many2one( + comodel_name='ir.attachment', + string="PDF Attachment", + compute=lambda self: self._compute_linked_attachment_id('invoice_pdf_report_id', 'invoice_pdf_report_file'), + depends=['invoice_pdf_report_file'] + ) + invoice_pdf_report_file = fields.Binary( + attachment=True, + string="PDF File", + copy=False, + ) + + # TODO KZ campo similar a lo que tenemos en el UX, revisar como se llama y usarlo igual + l10n_uy_cfe_xml = fields.Text() def name_get(self): - """ Display: 'Stock Picking Internal Sequence : Remito (if defined)' """ + """ Display: 'Stock Picking Internal Sequence : Remito Number (if defined)' """ res = [] for rec in self: if rec.l10n_latam_document_number: @@ -58,31 +95,45 @@ def name_get(self): res.append((rec.id, name)) return res + def _compute_l10n_uy_is_cfe(self): + self.l10n_uy_is_cfe = False + if self.l10n_latam_document_type_id.code in ['124', '181', '224', '281']: + self.l10n_uy_is_cfe = True + @api.depends('partner_id', 'company_id', 'picking_type_code') def _compute_l10n_latam_available_document_types(self): uy_remitos = self.filtered(lambda x: x.country_code == 'UY' and x.picking_type_code == 'outgoing') - uy_remitos.l10n_latam_available_document_type_ids = self.env['l10n_latam.document.type'].search( self._get_l10n_latam_documents_domain()) (self - uy_remitos).l10n_latam_available_document_type_ids = False def _get_l10n_latam_documents_domain(self): + """ return domain """ codes = self._l10n_uy_get_remito_codes() return [('code', 'in', codes), ('active', '=', True), ('internal_type', '=', 'stock_picking')] - # TODO KZ evaluar si estaria bueno tener un boolean como este l10n_cl_draft_status - # TODO KZ evaluar si agregar una constrains de unicidad para remitos, aplicaria para: - # 1. remitos manual o preimpresos (no electronico), - # 2. remitos generados en uruware y pasados a mano luego a oodo - # 3. remitos de proveedor? no se si los necesitamos registrar - def action_cancel(self): - # The move cannot be modified once the CFE has been accepted by the DGI + """ El remito no puede ser modificado una vez que ya fue aceptado por DGI """ remitos = self.filtered(lambda x: x.country_code == 'UY' and x.picking_type_code == 'outgoing') - remitos._uy_check_state() + if remitos.filtered(lambda x: x.l10n_uy_edi_cfe_state in ['accepted', 'rejected', 'received']): + raise UserError(_('Can not cancel a Remito already process by DGI')) return super().action_cancel() - def uy_post_dgi_remito(self): + def _compute_linked_attachment_id(self, attachment_field, binary_field): + """Helper to retreive Attachment from Binary fields + This is needed because fields.Many2one('ir.attachment') makes all + attachments available to the user. + """ + attachments = self.env['ir.attachment'].search([ + ('res_model', '=', self._name), + ('res_id', 'in', self.ids), + ('res_field', '=', binary_field) + ]) + move_vals = {att.res_id: att for att in attachments} + for move in self: + move[attachment_field] = move_vals.get(move._origin.id, False) + + def l10n_uy_edi_stock_send_dgi(self): """ El E-remito tiene las siguientes partes en el xml A. Encabezado B. Detalle de los productos @@ -91,15 +142,16 @@ def uy_post_dgi_remito(self): """ # Filtrar solo los e-remitos uy_remitos = self.filtered( - lambda x: x.country_code == 'UY' and x.picking_type_code == 'outgoing' + lambda x: x.country_code == 'UY' + and x.picking_type_code == 'outgoing' and x.l10n_latam_document_type_id and int(x.l10n_latam_document_type_id.code) > 0 and x.l10n_uy_edi_cfe_state not in ['accepted', 'rejected', 'received'] ) - # If the invoice was previosly validated in Uruware and need to be link to Odoo we check that the - # l10n_uy_edi_cfe_uuid has been manually set and we consult to get the invoice information from Uruware - pre_validated_in_uruware = uy_remitos.filtered(lambda x: x.l10n_uy_edi_cfe_uuid and not x.l10n_uy_cfe_file and not x.l10n_uy_edi_cfe_state) + # If the invoice was previously validated in Uruware and need to be link to Odoo + # we check that the l10n_uy_edi_cfe_uuid has been manually set and we consult to get the invoice information from Uruware + pre_validated_in_uruware = uy_remitos.filtered(lambda x: x.l10n_uy_edi_cfe_uuid and not x.attachment_id and not x.l10n_uy_edi_cfe_state) if pre_validated_in_uruware: pre_validated_in_uruware.uy_ux_action_get_uruware_cfe() uy_remitos = uy_remitos - pre_validated_in_uruware @@ -108,40 +160,196 @@ def uy_post_dgi_remito(self): return # Send invoices to DGI and get the return info + msg = '' for remito in uy_remitos: - if remito.company_id.l10n_uy_edi_ucfe_env == "demo": - remito._uy_dummy_validation() - continue + edi_doc = self.env['l10n_uy_edi.document'].create({ + "picking_id": remito.id, + "uuid": self.env['l10n_uy_edi.document']._uy_get_uuid(remito), + }) + remito.l10n_uy_edi_document_id = edi_doc - # TODO KZ I think we can avoid this loop. review - remito._uy_dgi_post() + if remito.company_id.l10n_uy_edi_ucfe_env == "demo": + attachments = remito._l10n_uy_edi_dummy_validation() + msg = _( + "This CFE has been generated in DEMO Mode. It is considered" + " as accepted and it won\"t be sent to DGI.") + else: + request_data = remito._l10n_uy_stock_prepare_req_data() + result = edi_doc._send_dgi(request_data) + edi_doc._update_cfe_state(result) + + response = result.get("response") + + if edi_doc.message: + remito.message_post( + body=Markup("{}: {}").format(("ERROR"), edi_doc.message) + ) + elif edi_doc.state in ["received", "accepted"]: + # If everything is ok we save the return information + remito.l10n_latam_document_number = \ + response.findtext(".//{*}Serie") + "%07d" % int( + response.findtext(".//{*}NumeroCfe")) + + msg = response.findtext(".//{*}MensajeRta", "") + msg += _("The electronic invoice was created successfully") + + if response is not None: + attachments = remito._l10n_uy_stock_update_xml_and_pdf_file(response) + + remito.with_context(no_new_invoice=True).message_post( + body=msg, attachment_ids=attachments.ids if attachments else False) + + def _l10n_uy_stock_update_xml_and_pdf_file(self, response): + """ Clean up the pdf and xml fields. Create new ones with the response """ + self.ensure_one() + res_files = self.env["ir.attachment"] + edi_doc = self.l10n_uy_edi_document_id + + self.invoice_pdf_report_id.res_field = False + edi_doc.attachment_id.res_field = False + + xml_content = response.findtext(".//{*}XmlCfeFirmado") + if xml_content: + res_files = self.env["ir.attachment"].create({ + "res_model": "l10n_uy_edi.document", + "res_field": "attachment_file", + "res_id": edi_doc.id, + "name": edi_doc._get_xml_attachment_name(), + "type": "binary", + "datas": base64.b64encode( + xml_content.encode() if self.l10n_uy_edi_cfe_state in ["received", "accepted"] + else self._l10n_uy_stock_get_xml_content().encode() + ), + }) + + edi_doc.invalidate_recordset(["attachment_id", "attachment_file"]) + + # If the record has been posted automatically print and attach the legal report to the record. + if self.l10n_uy_edi_cfe_state and self.l10n_uy_edi_cfe_state != "error": + pdf_result = self._l10n_uy_edi_get_pdf() + if pdf_file := pdf_result.get("pdf_file"): + # make sure latest PDF shows to the right of the chatter + pdf_file.register_as_main_attachment(force=True) + self.invalidate_recordset(fnames=["invoice_pdf_report_id", "invoice_pdf_report_file"]) + res_files |= pdf_file + if errors := pdf_result.get("errors"): + msg = _("Error getting the PDF file: %s", errors) + self.l10n_uy_edi_error = (self.l10n_uy_edi_error or "") + msg + self.message_post(body=msg) + else: + self._l10n_uy_edi_get_preview_xml() + return res_files + + def _l10n_uy_edi_dummy_validation(self): + # COPY l10n_uy_edi (only change move_id with picking_id) + """ When we want to skip DGI and validate only in Odoo """ + edi_doc = self.l10n_uy_edi_document_id + edi_doc.state = "accepted" + self.write({ + "l10n_latam_document_number": "DE%07d" % (edi_doc.picking_id.id), + "ref": "*DEMO", + }) + + return self._l10n_uy_edi_get_preview_xml() + + def _l10n_uy_edi_get_preview_xml(self): + # COPY l10n_uy_edi + self.ensure_one() + edi_doc = self.l10n_uy_edi_document_id + edi_doc.attachment_id.res_field = False + xml_file = self.env["ir.attachment"].create({ + "res_model": "l10n_uy_edi.document", + "res_field": "attachment_file", + "res_id": edi_doc.id, + "name": edi_doc._get_xml_attachment_name(), + "type": "binary", + "datas": base64.b64encode(self._l10n_uy_stock_get_xml_content().encode()), + }) + edi_doc.invalidate_recordset(["attachment_id", "attachment_file"]) + return xml_file + + def _l10n_uy_edi_get_pdf(self): + """ Call endpoint to get PDF file from Uruware (Standard Representation) + return: dictionary with {"errors": str(): "pdf_file"attachment object } """ + res = {} + # if self.journal_id.l10n_uy_edi_type != "electronic": + # return {"errors": _("Only can get the legal representation of the CFE for customer electronic invoices")} + + result = self.l10n_uy_edi_document_id._get_pdf() + + if file_content := result.get("file_content"): + pdf_file = self.env["ir.attachment"].create({ + "res_model": "account.move", + "res_id": self.id, + "res_field": "invoice_pdf_report_file", + "name": (self.name or _("INV")).replace("/", "_") + ".pdf", + "type": "binary", + "datas": file_content, + }) + res["pdf_file"] = pdf_file - # TODO KZ buscar el metodo _l10n_cl_get_tax_amounts para ejemplos de como extraer la info de los impuestos en un picking. viene siempre de una - # factura + return res - def _uy_get_cfe_addenda(self): - """ Add Specific MOVE model fields to the CFE Addenda if they are set: + def _l10n_uy_stock_get_xml_content(self): + # COPY l10n_uy_edi + """ Create the CFE xml structure and validate it + :return: string the xml content to send to DGI """ + self.ensure_one() - * field Origin added with the prefix "Origin: ..." - * Observation - """ + values = { + "cfe": self, + "IdDoc": self._l10n_uy_stock_cfe_A_iddoc(), + "emisor": self._l10n_uy_stock_cfe_A_issuer(), + "receptor": self._l10n_uy_stock_cfe_A_receptor(), + "item_detail": self._l10n_uy_stock_cfe_B_details(), + "totals_detail": self._l10n_uy_stock_cfe_C_totals(), + # "referencia_lines": self._l10n_uy_edi_cfe_F_reference(), + #Hay que definir nuestro format_float + "format_float": format_float, + } + cfe = self.env["ir.qweb"]._render( + "l10n_uy_edi_stock." + self.l10n_uy_edi_document_id._uy_get_cfe_tag() + "_template", values) + return etree.tostring(cleanup_xml_node(cfe)).decode() + + def _l10n_uy_stock_prepare_req_data(self): + """ Creating dictionary with the request to generate a DGI EDI document """ self.ensure_one() - res = super()._uy_get_cfe_addenda() + edi_doc = self.l10n_uy_edi_document_id + xml_content = self._l10n_uy_stock_get_xml_content() + req_data = { + "Uuid": edi_doc.uuid, + "TipoCfe": int(self.l10n_latam_document_type_id.code), + "HoraReq": edi_doc.request_datetime.strftime("%H%M%S"), + "FechaReq": edi_doc.request_datetime.date().strftime("%Y%m%d"), + "CfeXmlOTexto": xml_content} + + if addenda := self._l10n_uy_edi_get_addenda(): + req_data["Adenda"] = addenda + return req_data + + def _l10n_uy_edi_get_addenda(self): + """ return string with the addenda of the remito """ + addenda = self.l10n_uy_edi_document_id._get_legends("addenda", self) if self.origin: - res += "\n\nOrigin: %s" % self.origin + addenda += "\n\nOrigin: %s" % self.origin if self.note: - res += "\n\n%s" % html2plaintext(self.note) - return res.strip() + addenda += "\n\n%s" % html2plaintext(self.note) + return addenda.strip() + + def _l10n_uy_edi_get_used_rate(self): + # COPY l10n_uy_edi + self.ensure_one() + # We need to use abs to avoid error on Credit Notes (amount_total_signed is negative) + return abs(self.amount_total_signed) / self.amount_total def _uy_get_cfe_lines(self): self.ensure_one() - if self._is_uy_remito_type_cfe(): - # TODO KZ: Toca revisar realmente cual es el line que corresponde, el que veo en la interfaz parece ser move_ids_without_package pero no se si esto siempre aplica + # TODO KZ: Toca revisar realmente cual es el line que corresponde, el que veo en la interfaz parece ser move_ids_without_package pero no se si esto siempre aplica - # move_ids_without_package Stock moves not in package (stock.move) - # move_line_ids Operations (stock.move.line) - # move_line_ids_without_package Operations without package (stock.move.line) - return self.move_ids_without_package + # move_ids_without_package Stock moves not in package (stock.move) + # move_line_ids Operations (stock.move.line) + # move_line_ids_without_package Operations without package (stock.move.line) + return self.move_ids_without_package def _l10n_uy_get_remito_codes(self): """ return list of the available document type codes for uruguayan of stock picking""" @@ -152,66 +360,115 @@ def _l10n_uy_get_remito_codes(self): def l10n_uy_edi_action_get_dgi_state(self): self.ensure_one() - self.l10n_uy_edi_cfe_id.l10n_uy_edi_action_get_dgi_state() - - # TODO KZ este metodo esta en el account.move. debemos de generarlo tal cual en stock picking - def _l10n_uy_edi_cfe_A_receptor(self): - # EXTEND l10n_uy_edi - """ Agregamos mas campos no obligatorios que no nos permitieron agregar en oficial """ - res = super()._l10n_uy_edi_cfe_A_receptor() - # A69 - LugarDestEnt - if self.l10n_uy_edi_place_of_delivery and not self._is_uy_resguardo(): - value = '' - delivery_address = self.partner_shipping_id - if delivery_address: - value = (delivery_address.name + ' ' + delivery_address.street)[:100] - res['LugarDestEnt'] = value - - def _uy_cfe_A_iddoc(self): - res = super()._uy_cfe_A_iddoc() - - return res - - def _l10n_uy_edi_cfe_A_iddoc(self): - res = self.env['account.move']._l10n_uy_edi_cfe_A_iddoc() - - if self._is_uy_remito_type_cfe(): # A6 - res.update({'TipoTraslado': self.l10n_uy_transfer_of_goods}) + self.l10n_uy_edi_document_id.action_update_dgi_state() +#Armado de xml + def _l10n_uy_stock_cfe_A_iddoc(self): + """ XML Section A (Encabezado) """ # TODO KZ A5 FchEmis - Fecha del Comprobante - # ver que fecha deberiamos de usar en caso de ser picking. opciones - # scheduled_date - Scheduled Date - # date - Creation Date - # date_deadline - Deadline - # date_done - Date of Transfer - # return res - # . self.scheduled_date.strftime('%Y-%m-%d') + # scheduled_date - Scheduled Date + # date - Creation Date + # date_deadline - Deadline + # date_done - Date of Transfer + return { + "TipoCFE": self.l10n_latam_document_type_id.code, + "FchEmis": fields.Date.to_string(self.date), + "TipoTraslado": self.l10n_uy_transfer_of_goods, # A5 + } + + def _l10n_uy_stock_cfe_A_issuer(self): + return { + "RUCEmisor": self.company_id.vat, + "RznSoc": self.company_id.name[:150], + "CdgDGISucur": self.company_id.l10n_uy_edi_branch_code, + "DomFiscal": self.company_id.partner_id._l10n_uy_edi_get_fiscal_address(), + "Ciudad": self.company_id.city[:30], + "Departamento": self.company_id.state_id.name[:30], + "InfoAdicionalEmisor": self.l10n_uy_edi_document_id._get_legends("issuer", self) or None + } + + def _l10n_uy_stock_cfe_A_receptor(self): + """ XML Section A (Encabezado / Receptor) """ + self.ensure_one() + doc_type = self.partner_id._l10n_uy_edi_get_doc_type() + + return { + "TipoDocRecep": doc_type or None, # A60 + "CodPaisRecep": self.partner_id.country_id.code or ("UY" if doc_type in [2, 3] else "99"), # A61 + "DocRecep": self.partner_id.vat if doc_type in [1, 2, 3] else None, # A62 + "DocRecepExt": self.partner_id.vat if doc_type not in [1, 2, 3] else None, # A62.1 + "RznSocRecep": self.partner_id.name[:150] or None, # A63 + "DirRecep": self.partner_id._l10n_uy_edi_get_fiscal_address() or None, # A64 + "CiudadRecep": self.partner_id.city and self.partner_id.city[:30] or None, # A65 + "DeptoRecep": self.partner_id.state_id and self.partner_id.state_id.name[:30] or None, # A66 + "PaisRecep": self.partner_id.country_id and self.partner_id.country_id.name or None, # A66.1 + "InfoAdicional": self.l10n_uy_edi_document_id._get_legends("receiver", self) or None, # A68 + } + + def _l10n_uy_stock_cfe_A_totals(self): + """ XML Section C (SUBTOTALES INFORMATIVOS) """ + self.ensure_one() + currency_name = self.company_id.currency_id.name if self.company_id.currency_id else None + + expo_doc = self.l10n_uy_edi_document_id._is_uy_remito_exp() - res.update(self._l10n_uy_get_cfe_serie()) + res = { + "TpoMoneda": currency_name if expo_doc else None, # A110 + "TpoCambio": None if currency_name == "UYU" and not expo_doc else currency_name, # A111 + #"MntExpoyAsim": sum(self.move_line_ids.mapped('quantity')) if expo_doc else None, Ver como toca desarrollar esto para e-remito-expo + #"MntTotal": self.amount_total, # A124 - Si tipo de CFE= 124 (e-remitode exportación), Valor numérico de 15 enteros y 2 decimales, + "CantLinDet": len(self.move_line_ids), # A126 + } return res + def _l10n_uy_stock_cfe_B_details(self): + self.ensure_one() + res = [] - # TODO KZ este metodo debemos adaptarlo para obtener el IndFact - def _uy_cfe_B4_IndFact(self, line): - """ B4: Indicador de facturación + #For B4 + invoice_ind = False + if self.l10n_uy_edi_document_id._is_uy_remito_exp(): + #Tome la decicion de informarlo solamentee si es e-remito-expo + invoice_ind = 10 + + for k, line in enumerate(self.move_line_ids, start=1): + res.append({ + "NroLinDet": k, # B1 + "IndFact": invoice_ind if invoice_ind else None, # B4 + "NomItem": line.display_name, # B7 + "DscItem": line.description_picking if line.description_picking and line.description_picking != line.display_name else None, # B8 + "Cantidad": line.quantity, # B9 + "UniMed": line.product_uom_id.name[:4] if line.product_uom_id else "N/A", # B10 + #"PrecioUnitario": line.price_unit, # B11 como encuentro el precio unitario para facturas de expo ? + #"MontoItem": line.price_total if tax_included else line.price_subtotal, # B24 como encuentro el precio unitario para facturas de expo ? + }) + return res + # "referencia_lines": self._l10n_uy_edi_cfe_F_reference(), - TODO KZ: Toca revisar realmente cual es el line que corresponde, el que veo en la interfaz parece ser move_ids_without_package pero no se si esto siempre aplica - move_ids_without_package Stock moves not in package (stock.move) - move_line_ids Operations (stock.move.line) - move_line_ids_without_package Operations without package (stock.move.line) - """ - # Another cases for future - # 4: Gravado a Otra Tasa/IVA sobre fictos - # 5: Entrega Gratuita. Por ejemplo docenas de trece - # 6: Producto o servicio no facturable. No existe validación, excepto si A-C20= 1, B-C4=6 o 7. - # 7: Producto o servicio no facturable negativo. . No existe validación, excepto si A-C20= 1, B-C4=6 o 7. - # 8: Sólo para remitos: Ítem a rebajar en e-remitos y en e- remitos de exportación. En área de referencia se debe indicar el N° de remito que ajusta - # 9: Sólo para resguardos: Ítem a anular en resguardos. En área de referencia se debe indicar el N° de resguardo que anular - # 11: Impuesto percibido - # 12: IVA en suspenso - # 13: Sólo para e-Boleta de entrada y sus notas de corrección: Ítem vendido por un no contribuyente (valida que A-C60≠2) - # 14: Sólo para e-Boleta de entrada y sus notas de corrección: Ítem vendido por un contribuyente IVA mínimo, Monotributo o Monotributo MIDES (valida que A-C60=2) - # 15: Sólo para e-Boleta de entrada y sus notas de corrección: Ítem vendido por un contribuyente IMEBA (valida A-C60 = 2) - # 16: Sólo para ítems vendidos por contribuyentes con obligación IVA mínimo, Monotributo o Monotributo MIDES. Si A-C10=3, no puede utilizar indicadores 1, 2, 3, 4, 11 ni 12 - return super()._uy_cfe_B4_IndFact(line) + def _l10n_uy_stock_cfe_C_totals(self): + self.ensure_one() + currency_name = self.company_id.currency_id.name if self.company_id.currency_id else None + lines = self._uy_get_cfe_lines() + res = { + 'TpoMoneda': currency_name if not self.l10n_uy_edi_document_id._is_uy_remito_loc() else None, # A110 + 'TpoCambio': None if currency_name == "UYU" else self._l10n_uy_edi_get_used_rate() or None, # A111 + 'CantLinDet': len(lines), # A126 + } + return res + #Solo informan si es NC o ND ????? + # def _l10n_uy_edi_cfe_F_reference(self): + # """ XML Section F (REFERENCE INFORMATION). If is a debit/credit note cfe then we need to inform the reference tag """ + # res = [] + # if self.l10n_latam_document_type_id.internal_type in ["credit_note", "debit_note"]: + # related_doc = self._l10n_uy_edi_found_related_cfe() + # for k, related_cfe in enumerate(related_doc, 1): + # cfe_serie, cfe_number = self.l10n_uy_edi_document_id._get_doc_parts(related_cfe) + # res.append({ + # "NroLinRef": k, # F1 + # "TpoDocRef": int(related_cfe.l10n_latam_document_type_id.code), # F3 + # "Serie": cfe_serie, # F4 + # "NroCFERef": cfe_number, # F5 + # }) + # return res diff --git a/l10n_uy_edi_stock/views/cfe_template.xml b/l10n_uy_edi_stock/views/cfe_template.xml index 23272535..a9fd2e4a 100644 --- a/l10n_uy_edi_stock/views/cfe_template.xml +++ b/l10n_uy_edi_stock/views/cfe_template.xml @@ -1,22 +1,80 @@ - -