From 5bc42e853f9a3882c06bbb86b01d8e07b2a6a3eb Mon Sep 17 00:00:00 2001 From: Pablo Montenegro Date: Fri, 19 May 2023 15:08:06 -0300 Subject: [PATCH] [IMP] l10n_uy_edi: Creacion de facturas de proveedor automaticamente Task: 28036 --- .../views/account_journal_view.xml | 2 +- l10n_uy_edi/__manifest__.py | 2 +- l10n_uy_edi/data/ir_cron.xml | 10 + l10n_uy_edi/models/account_move.py | 1 + l10n_uy_edi/models/l10n_uy_cfe.py | 245 +++++++++++++++++- l10n_uy_edi/models/res_company.py | 3 +- l10n_uy_edi/views/account_move_views.xml | 2 + 7 files changed, 260 insertions(+), 5 deletions(-) diff --git a/l10n_uy_account/views/account_journal_view.xml b/l10n_uy_account/views/account_journal_view.xml index 0b5930ee..6d7195e8 100644 --- a/l10n_uy_account/views/account_journal_view.xml +++ b/l10n_uy_account/views/account_journal_view.xml @@ -8,7 +8,7 @@ - + {'invisible': ['|', ('type', 'not in', ['sale', 'purchase']), ('l10n_latam_use_documents', '=', True), ('country_code', '=', 'UY')]} diff --git a/l10n_uy_edi/__manifest__.py b/l10n_uy_edi/__manifest__.py index 61f7d57d..577353b7 100644 --- a/l10n_uy_edi/__manifest__.py +++ b/l10n_uy_edi/__manifest__.py @@ -7,7 +7,7 @@ 'author': 'ADHOC SA', 'category': 'Localization', 'license': 'LGPL-3', - 'version': "16.0.1.6.0", + 'version': "16.0.1.7.0", 'depends': [ 'l10n_uy_account', 'account_debit_note', diff --git a/l10n_uy_edi/data/ir_cron.xml b/l10n_uy_edi/data/ir_cron.xml index 5ce6e8b7..8d66e65a 100755 --- a/l10n_uy_edi/data/ir_cron.xml +++ b/l10n_uy_edi/data/ir_cron.xml @@ -13,6 +13,16 @@ + + UY: Consult vendor bills received + 10 + minutes + -1 + + code + model.l10n_uy_action_get_l10n_uy_received_invoices() + + diff --git a/l10n_uy_edi/models/account_move.py b/l10n_uy_edi/models/account_move.py index 13e69a0b..fbd14693 100644 --- a/l10n_uy_edi/models/account_move.py +++ b/l10n_uy_edi/models/account_move.py @@ -11,6 +11,7 @@ class AccountMove(models.Model): _inherit = ['account.move', 'l10n.uy.cfe'] l10n_uy_journal_type = fields.Selection(related='journal_id.l10n_uy_type') + l10n_uy_idreq = fields.Text('idReq', copy=False, readonly=True, groups="base.group_system", help="We add this field to the vendor bills that are created from Uruware because if we need to consult them again through the consult of notifications we need to pass that idReq to consult 600 to get the response it again.") # This is required to be able to save defaults taking into account the document type selected l10n_latam_document_type_id = fields.Many2one(change_default=True) diff --git a/l10n_uy_edi/models/l10n_uy_cfe.py b/l10n_uy_edi/models/l10n_uy_cfe.py index b5d0efd5..f3cb0c89 100644 --- a/l10n_uy_edi/models/l10n_uy_cfe.py +++ b/l10n_uy_edi/models/l10n_uy_cfe.py @@ -4,7 +4,8 @@ import base64 import stdnum.uy import re - +import xml.etree.ElementTree as ET +# from lxml import etree from odoo import _, fields, models, api from odoo.exceptions import UserError from odoo.tools.safe_eval import safe_eval @@ -578,6 +579,243 @@ def _l10n_uy_get_cfe_serie(self): }) return res + #def l10n_uy_xml_to_dict(self, xml_string): + # """ We use this method when we make a '610 - Solicitud de datos de Notificacion' request to make the information we obtain from the notification more readable. + # """ + #test = etree.fromstring(xml_string.encode('utf-8')) + # root = ET.fromstring(xml_string) + # + # return root + + #def l10n_uy_element_to_dict(self, element): + # import pdb; pdb.set_trace() + # if len(element) == 0: + # return element.text +# + # result = {} + # for child in element: + # child_data = self.l10n_uy_element_to_dict(child) + # key = child.tag.replace('xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.0" xmlns="http://cfe.dgi.gub.uy"', '') + # if key in result: + # if type(result[key]) is list: + # result[key].append(child_data) + # else: + # result[key] = [result[key], child_data] + # else: + # result[key] = child_data +# + # return result + + def l10n_uy_verify_codrta(self, company, response): + """ Verify response code from notifications (vendor bills). If response code is != 0 return True (can`t create new vendor bill), else return False (continue the process and create vendor bill). """ + if response.Resp.CodRta == '01': + _logger.info('There are no notifications available in the UCFE for the company %s (id: %d)' % (company.name, company.id)) + return True + elif response.Resp.CodRta != '00': + _logger.info(_('ERROR: This is what we receive when requesting notification data (610) %s') % response) + return True + else: + return False + + def l10n_uy_dismiss_notification(self, company, response): + """ This is implemented for vendor bills. Is needed to dismiss the last notification if the last vendor bill was created in Odoo from Uruware. """ + try: + response3 = company._l10n_uy_ucfe_inbox_operation('620', { + 'IdReq': response.Resp.IdReq, + 'TipoNotificacion': response.Resp.TipoNotificacion}) + except: + _logger.warning('We found an error when dismissing the notification: id: %d' % (response.Resp.IdReq)) + if response3.Resp.CodRta != '00': + raise UserError(_('ERROR: the notification could not be dismissed %s') % response) + + def l10n_uy_create_partner_from_notification(self, value, partner_vat_RUC): + """ In case we need to create vendor bills synchronized with Uruware through notifications and the partner from the bill does not exist id Odoo, then we create it in this method. """ + partner_name = root.findtext('.//*RznSoc') + partner_city = root.findtext('.//*Ciudad') + partner_state_id = root.findtext('.//*Departamento') + partner_street = root.findtext('.//*DomFiscal') + state_id = self.env['res.country.state'].search([('name', 'ilike', partner_state_id)], limit=1) + country_id = state_id.country_id + ruc = self.env.ref('l10n_uy_account.it_rut').id + # + partner_vals = {'name': partner_name, + 'vat': partner_vat_RUC, + 'city': partner_city, + 'street': partner_street, + 'state_id': state_id.id, + 'country_id': country_id.id, + 'l10n_latam_identification_type_id': ruc, + 'is_company': True} + partner_id = self.env['res.partner'].create(partner_vals) + return partner_id + + def l10n_uy_create_vendor_line_ids(self, company, invoice_line_ids): + """ Here are created the lines of vendor bills that are synchronized through the Uruware notification request. """ + line_ids = [] + for value in invoice_line_ids: + #IndFac + # agregar + #7: Producto o servicio no + #facturable negativo. . No + #existe validación, excepto si + #A-C20= 1, B-C4=6 o 7. + domain_tax = [('country_code', '=', 'UY'), ('company_id.id', '=', company.id)] + ind_fact = value.find(".//IndFact").text + if ind_fact == '1': + # Exento de IVA + domain_tax += [('tax_group_id.l10n_uy_vat_code', '=', 'vat_exempt'), ('name', '=', 'IVA Compras Exento')] + elif ind_fact == '2': + # Gravado a Tasa Mínima + domain_tax += (('tax_group_id.l10n_uy_vat_code', '=', 'vat_10'), ('name', '=', 'IVA Compras 10%')) + elif ind_fact == '3': + # Gravado a Tasa Básica + domain_tax += (('tax_group_id.l10n_uy_vat_code', '=', 'vat_22'), ('name', '=', 'IVA Compras 22%')) + price_unit = value.find(".//PrecioUnitario").text + price_unit_signed = float(price_unit) if ind_fact != '7' else -1*float(price_unit) + tax_item = self.env['account.tax'].search(domain_tax, limit=1) + line_vals = {'move_type': 'in_invoice', + 'name': value.find(".//NomItem").text, + 'quantity': float(value.find(".//Cantidad").text), + 'price_unit': price_unit_signed, + 'tax_ids': [(6, 0, tax_item.ids)] if ind_fact not in ['6', '7'] else []} + line_ids.append((0, 0, line_vals)) + return line_ids + + def l10n_uy_create_vendor_invoice(self, company, response2, transport, l10n_uy_idreq): + """ Here the vendor bills are created and synchronized through the Uruware notification request. """ + response2_to_string = str(response2) + xml_string = response2.Resp.XmlCfeFirmado + if not xml_string: + raise UserError('There is no information to create the vendor invoice in the notification %d consulted' % (l10n_uy_idreq)) + xml_string = re.sub(r']*>', '', xml_string) + #xml_string = xml_string.replace('xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.0" xmlns="http://cfe.dgi.gub.uy"', '') + root = ET.fromstring(xml_string) + invoice_line_ids = [] + invoice_date_due = False + l10n_latam_document_type_id = root.findtext('.//*TipoCFE') + # Facturas + if l10n_latam_document_type_id in ['101', '111', '181', '182', '121', '124', '131', '141', '151']: + move_type = 'in_invoice' + # Notas de crédito + if l10n_latam_document_type_id in ['102', '112', '122', '132', '142', '152']: + move_type = 'in_refund' + if l10n_latam_document_type_id in ['103', '113', '123', '133', '143', '153']: + move_type = 'in_refund' + new_invoice = self.env['account.move'].create({'l10n_uy_idreq': l10n_uy_idreq, + 'move_type': move_type, + }) + try: + partner_vat_RUC = root.findtext('.//*RUCEmisor') + serieCfe = root.findtext('.//*Serie') + l10n_latam_document_number = root.findtext('.//*Nro') + req_data_pdf = {'rut': company.vat, + 'rutRecibido': partner_vat_RUC, + 'tipoCfe': l10n_latam_document_type_id, + 'serieCfe': serieCfe, + 'numeroCfe': l10n_latam_document_number} + self.l10n_uy_create_pdf_vendor_invoice(company, new_invoice, req_data_pdf) + # Mejorar esto '>\n<'.join([item for item in xml_string.split('><')]) para que se muestre mejor parseado, ver los links que dejé en la tarea nota 18/10/2023 09:54:57 + l10n_uy_cfe_xml = '>\n<'.join([item for item in xml_string.split('><')]) + import pdb; pdb.set_trace() + new_invoice.write({ + 'l10n_uy_dgi_xml_response': transport.xml_response, + 'l10n_uy_cfe_xml': l10n_uy_cfe_xml, + 'l10n_uy_dgi_xml_request': transport.xml_request, + }) + invoice_date = datetime.strptime(root.findtext('.//*FchEmis'), '%Y-%m-%d').date() + fecha_vto = root.findtext('.//*FchVenc') + if fecha_vto: + invoice_date_due = datetime.strptime(fecha_vto, '%Y-%m-%d').date() + invoice_currency = root.findtext('.//*TpoMoneda') + amount_total = root.findtext('.//*MntTotal') + cant_lineas = root.findtext('.//*CantLinDet') + if invoice_currency == 'USD': + currency_id = self.env['res.currency'].search([('l10n_uy_bcu_code', '=', 2225), ('symbol', '=', 'USD')], limit=1) + elif invoice_currency == 'UYI': + currency_id = self.env['res.currency'].search([('l10n_uy_bcu_code', '=', 9800), ('symbol', '=', 'UYI')], limit=1) + elif invoice_currency == 'EUR': + currency_id = self.env['res.currency'].search([('l10n_uy_bcu_code', '=', 1111), ('symbol', '=', 'EUR')], limit=1) + elif invoice_currency == 'ARS': + currency_id = self.env['res.currency'].search([('l10n_uy_bcu_code', '=', 501), ('symbol', '=', 'ARS')], limit=1) + else: + currency_id = company.currency_id + # que identificador único uso para el comprobante? identificador de lado de lo que + # recibimos de uruware + # l10n_uy_cfe_uuid = value['Encabezado']['IdDoc']['NroInterno'] + partner_id = self.env['res.partner'].search([('commercial_partner_id.vat', '=', partner_vat_RUC)], limit=1) + # Si no existe el partner lo creamos + if not partner_id: + partner_id = self.l10n_uy_create_partner_from_notification(value, partner_vat_RUC) + # para iterar los items usar findall + invoice_line_ids = root.findall('.//*Item') + line_ids = self.l10n_uy_create_vendor_line_ids(company, invoice_line_ids) + # este if era para verificar que el comprobante que estamos por crear no exista en odoo pero creo que se podría quitar + #if not self.env['account.move'].search([('l10n_uy_cfe_uuid', '=', l10n_uy_cfe_uuid)]): + document_type = self.env['l10n_latam.document.type'].search([('code', '=', l10n_latam_document_type_id)], limit=1).id + move_vals = { + 'partner_id': partner_id.id, + 'invoice_date': invoice_date, + 'l10n_latam_document_type_id': document_type, + 'l10n_latam_document_number': l10n_latam_document_number, + 'line_ids': line_ids, + 'currency_id': currency_id.id,} + if invoice_date_due: + move_vals['invoice_date_due'] = invoice_date_due + new_invoice.write(move_vals) + if len(new_invoice.invoice_line_ids) != int(cant_lineas): + raise UserError('The number of invoice lines %s (id:%d) is invalid' % (new_invoice.name, new_invoice.id)) + except: + message = _("We found an error when loading information in this invoice") + new_invoice.message_post(body=message) + _logger.warning('We found an error when loading information on the vendor invoice: %s (id: %d)' % (new_invoice.name, new_invoice.id)) + + def l10n_uy_create_pdf_vendor_invoice(self, company, new_invoice, req_data_pdf): + """ The vendor invoice pdb is created and syncronized through the Uruware notification request. """ + response_reporte_pdf = self.env.company._l10n_uy_ucfe_query('ObtenerPdfCfeRecibido', req_data_pdf) + new_invoice.l10n_uy_cfe_pdf = self.env['ir.attachment'].create({ + 'name': (new_invoice.name or prefix.get(new_invoice._name, 'OBJ')).replace('/', '_') + '.pdf', + 'res_model': new_invoice._name, 'res_id': new_invoice.id, + 'type': 'binary', 'datas': base64.b64encode(response_reporte_pdf) + }) + + def action_l10n_uy_complete_vendor_invoice(self): + """ Sync with Uruware and complete vendor invoice information. """ + l10n_uy_idreq = self.l10n_uy_idreq + company = self.company_id + response2, transport = company._l10n_uy_ucfe_inbox_operation('610', {'IdReq': l10n_uy_idreq}, return_transport=1) + self.l10n_uy_create_vendor_invoice(company, response2, transport, l10n_uy_idreq) + + def l10n_uy_get_l10n_uy_received_invoices(self): + # TODO test it + + # 600 - Consulta de Notificacion Disponible + for company in self.env['res.company'].search([('account_fiscal_country_id.code', '=', 'UY'), ('l10n_uy_ucfe_commerce_code', '!=', False)]): + response = company._l10n_uy_ucfe_inbox_operation('600') + # Si guardo idReq de la solicitud 600 luego más adelante puedo volver a consultar la notificación y no hace falta descartarla + # Por lo tanto conviene guardar el idReq y adjuntarlo a la factura + l10n_uy_idreq = response.Resp.IdReq + if self.l10n_uy_verify_codrta(company,response): + continue + # If there is notifications + while response.Resp.CodRta == '00': + try: + # response.Resp.TipoNotificacion + # 610 - Solicitud de datos de Notificacion + l10n_uy_idreq = response.Resp.IdReq + response2, transport = company._l10n_uy_ucfe_inbox_operation('610', {'IdReq': l10n_uy_idreq}, return_transport=1) + except: + import pdb; pdb.set_trace() + _logger.warning('Encontramos un error al momento de sincronizar comprobantes de proveedor de la compañía: %s (id: %d)' % (company.name, company.id)) + break + self.l10n_uy_create_vendor_invoice(company, response2, transport, l10n_uy_idreq) + #if new_invoice.amount_total != float(amount_total): + # No puedo implementar este control aún porque hay un bug en odoo. Ver task: 34023 + # raise UserError('El monto total de la factura %s (id:%d)' % (new_invoice.name, new_invoice.id)) + self.l10n_uy_dismiss_notification(company, response) + response = company._l10n_uy_ucfe_inbox_operation('600') + if self.l10n_uy_verify_codrta(company,response): + continue + @api.model def l10n_uy_get_ucfe_notif(self): # TODO test it @@ -1360,3 +1598,8 @@ def _l10n_uy_dgi_post(self): # TODO comprobar. este devolvera un campo clave llamado UUID que permite identificar el comprobante, si es enviando dos vence sno genera otro CFE firmado return response + + def l10n_uy_action_get_l10n_uy_received_invoices(self): + """UY: Consult vendor bills received""" + self.l10n_uy_get_l10n_uy_received_invoices() + diff --git a/l10n_uy_edi/models/res_company.py b/l10n_uy_edi/models/res_company.py index c1f437ff..63088739 100644 --- a/l10n_uy_edi/models/res_company.py +++ b/l10n_uy_edi/models/res_company.py @@ -118,7 +118,7 @@ def _l10n_uy_ucfe_inbox_operation(self, msg_type, extra_req={}, return_transport """ Call Operation get in msg_type for UCFE inbox webservice """ self.ensure_one() # TODO consumir secuencia creada en Odoo - id_req = 1 + id_req = extra_req.get('IdReq') or 1 now = datetime.utcnow() company = self.sudo() data = {'Req': {'TipoMensaje': msg_type, 'CodComercio': company.l10n_uy_ucfe_commerce_code, @@ -129,7 +129,6 @@ def _l10n_uy_ucfe_inbox_operation(self, msg_type, extra_req={}, return_transport 'Tout': '30000'} if extra_req: data.get('Req').update(extra_req) - res = company._uy_get_client(company.l10n_uy_ucfe_inbox_url, return_transport=return_transport) client = res[0] if isinstance(res, tuple) else res transport = res[1] if isinstance(res, tuple) else False diff --git a/l10n_uy_edi/views/account_move_views.xml b/l10n_uy_edi/views/account_move_views.xml index 69cea97a..1e78e89c 100644 --- a/l10n_uy_edi/views/account_move_views.xml +++ b/l10n_uy_edi/views/account_move_views.xml @@ -37,6 +37,7 @@ +