From 6ea94b6945af1a1ca54d5a4641e5e47dd4da122f Mon Sep 17 00:00:00 2001 From: Pablo Montenegro Date: Mon, 29 May 2023 16:08:40 -0300 Subject: [PATCH] [ADD] l10n_uy_edi: se crea factura y cliente si corresponde --- .../data/l10n_latam.document.type.csv | 3 + l10n_uy_edi/data/ir_cron.xml | 10 + l10n_uy_edi/models/account_move.py | 2 + l10n_uy_edi/models/l10n_uy_cfe.py | 380 +++++++++++------- l10n_uy_edi/models/res_company.py | 5 +- l10n_uy_edi/views/account_move_views.xml | 4 +- 6 files changed, 263 insertions(+), 141 deletions(-) diff --git a/l10n_uy_account/data/l10n_latam.document.type.csv b/l10n_uy_account/data/l10n_latam.document.type.csv index 21a21e04..3456d606 100644 --- a/l10n_uy_account/data/l10n_latam.document.type.csv +++ b/l10n_uy_account/data/l10n_latam.document.type.csv @@ -44,3 +44,6 @@ dc_nota_de_crédito_de_e_factura_venta_por_cuenta_ajena_contingencia,242,Nota de dc_nota_de_débito_de_e_factura_venta_por_cuenta_ajena_contingencia,243,Nota de Débito de e-Factura Venta por Cuenta Ajena Contingencia,debit_note,ND e-Fac.Cta.Aj.Cotg,base.uy dc_ e_remito_contingencia,281, e-Remito Contingencia,stock_picking,e-Rem.Cotg,base.uy dc_ e_resguardo_contingencia,282, e-Resguardo Contingencia,,E-Resguardo.Cotg,base.uy +dc_e_boleta_de_entrada,151,e-Boleta de entrada,invoice,e-Boleta-entrada,base.uy +dc_cn_e_boleta_de_entrada,152,Nota de crédito de e-Boleta de entrada,credit_note,NC e-Boleta-entrada,base.uy +dc_dn_e_boleta_de_entrada,153,Nota de débito de e-Boleta de entrada,debit_note,ND e-Boleta-entrada,base.uy diff --git a/l10n_uy_edi/data/ir_cron.xml b/l10n_uy_edi/data/ir_cron.xml index 7fa03728..68f3b63d 100755 --- a/l10n_uy_edi/data/ir_cron.xml +++ b/l10n_uy_edi/data/ir_cron.xml @@ -24,6 +24,16 @@ + + Conciliar comprobantes recibidos + 15 + minutes + -1 + + code + model.action_conciliate_received_invoices() + + diff --git a/l10n_uy_edi/models/account_move.py b/l10n_uy_edi/models/account_move.py index 13e69a0b..1d507468 100644 --- a/l10n_uy_edi/models/account_move.py +++ b/l10n_uy_edi/models/account_move.py @@ -11,6 +11,8 @@ class AccountMove(models.Model): _inherit = ['account.move', 'l10n.uy.cfe'] l10n_uy_journal_type = fields.Selection(related='journal_id.l10n_uy_type') + l10n_uy_journal_use_documents = fields.Boolean('Journal use documents', related='journal_id.l10n_latam_use_documents') + l10n_uy_idReq = fields.Text('idReq', copy=False, readonly=True, groups="base.group_system", help="Este campo lo agregamos a los comprobantes de proveedor que se crean desde Uruware porque si necesitamos consultarlos nuevamente a través de consulta de notificaciones necesitamos pasar ese idReq a la consulta 600 para consultarlo nuevamente.") # 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 1ea8a169..0d3d1c1b 100644 --- a/l10n_uy_edi/models/l10n_uy_cfe.py +++ b/l10n_uy_edi/models/l10n_uy_cfe.py @@ -4,7 +4,7 @@ import base64 import stdnum.uy import re - +import xml.etree.ElementTree as ET from odoo import _, fields, models, api from odoo.exceptions import UserError from odoo.tools.safe_eval import safe_eval @@ -578,148 +578,241 @@ def _l10n_uy_get_cfe_serie(self): }) return res + def xml_to_dict(self, xml_string): + """ Este método lo usamos cuando hacemos una petición '610 - Solicitud de datos de Notificacion' para hacer más legible la información que obtenemos de la notificación. """ + root = ET.fromstring(xml_string) + return self.element_to_dict(root) + + def element_to_dict(self, element): + if len(element) == 0: + return element.text + + result = {} + for child in element: + child_data = self.element_to_dict(child) + key = child.tag.replace('{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 + @api.model def l10n_uy_get_ucfe_notif(self): # TODO test it # 600 - Consulta de Notificacion Disponible - response = self.env.company._l10n_uy_ucfe_inbox_operation('600') - # import pdb; pdb.set_trace() - # If there is notifications - if response.Resp.CodRta == '00': - # response.Resp.TipoNotificacion - - # 610 - Solicitud de datos de Notificacion - response2 = self.env.company._l10n_uy_ucfe_inbox_operation('610', {'IdReq': response.Resp.IdReq}) - import xml.etree.ElementTree as ET - #response_parsed = etree.fromstring(response2.Resp.XmlCfeFirmado) - xml_response = response2.Resp.XmlCfeFirmado - root = ET.fromstring(xml_response) - print(xml_response) - print("Tipo de CFE:", root.find(".//{http://cfe.dgi.gub.uy}IdDoc")) - # Es necesario indicar el namespace de la etiqueta - factura = {'TipoCfe': 'l10n_latam_document_type_id'} - partner = {'RUCEmisor': 'partner_id.vat'} - res = {} - for key, value in factura.items(): - res.update({value: root.find(".//{http://cfe.dgi.gub.uy}"+key).text}) - - # revisar si el ruc emisor existe en odoo - for key, value in partner.items(): - res.update({value: root.find(".//{http://cfe.dgi.gub.uy}"+key).text}) - - root.find(".//{http://cfe.dgi.gub.uy}TmstFirma").text - factura["TmstFirma"] = root.find(".//{http://cfe.dgi.gub.uy}TmstFirma").text - factura["Encabezado"] = {} - factura["Encabezado"]["IdDoc"] = {} - factura["Encabezado"]["Emisor"] = {} - factura["Encabezado"]["Totales"] = {} - factura["Detalle"] = {} - factura["Detalle"]["Item"] = {} - factura["CAEData"] = {} - for child in root.findall('.//{http://cfe.dgi.gub.uy}Item/*'): - factura["Detalle"]["Item"][child.tag.split('}')[1]] = child.text - for child in root.findall('.//{http://cfe.dgi.gub.uy}CAEData/*'): - factura["CAEData"][child.tag.split('}')[1]] = child.text - for child in root.findall('.//{http://cfe.dgi.gub.uy}IdDoc/*'): - factura["Encabezado"]["IdDoc"][child.tag.split('}')[1]] = child.text - for child in root.findall('.//{http://cfe.dgi.gub.uy}Emisor/*'): - factura["Encabezado"]["Emisor"][child.tag.split('}')[1]] = child.text - for child in root.findall('.//{http://cfe.dgi.gub.uy}Totales/*'): - factura["Encabezado"]["Totales"][child.tag.split('}')[1]] = child.text - import pdb - pdb.set_trace() - partner = factura['Encabezado']['Emisor']['RUCEmisor'] - self.env['res.partner'].search([('vat','=',partner)],limit=1) - # env['account.move'].create({'partner_id': partner_id, 'invoice_date':date, 'line_ids': [(0,0,{'product_id':31}),(0,0,{'product_id':24}),(0,0,{'product_id':231}),(0,0,{'product_id':234})]})._cr.commit() - - # ('5', 'Aviso de CFE emitido rechazado por DGI'), or - # ('6', 'Aviso de CFE emitido rechazado por el receptor electrónico'), - # Uuid - # TipoCfe - # Serie - # NumeroCfe - # MensajeRta - - # ('7', 'Aviso de CFE recibido'), - # Uuid - # TipoCfe - # Serie - # NumeroCfe - # XmlCfeFirmado - # Adenda - # RutEmisor - # Etiquetas - # EstadoEnDgiCfeRecibido - - # ('8', 'Aviso de anulación de CFE recibido'), - # ('9', 'Aviso de aceptación comercial de un CFE recibido'), - # ('10', 'Aviso de aceptación comercial de un CFE recibido en la gestión UCFE'), - # Uuid - # TipoCfe - # Serie - # NumeroCfe - # RutEmisor - - # ('11', 'Aviso de que se ha emitido un CFE'), - # ('12', 'Aviso de que se ha emitido un CFE en la gestión UCFE'), - # Uuid - # TipoCfe - # Serie - # NumeroCfe - # XmlCfeFirmado - # Adenda - # Etiquetas - - # ('13', 'Aviso de rechazo comercial de un CFE recibido'), - # Uuid - # TipoCfe - # Serie - # NumeroCfe - # MensajeRta - # RutEmisor - - # ('14', 'Aviso de rechazo comercial de un CFE recibido en la gestión UCFE'), - # Uuid - # TipoCfe - # Serie - # NumeroCfe - # RutEmisor - - # ('15', 'Aviso de CFE emitido aceptado por DGI'), - # ('16', 'Aviso de CFE emitido aceptado por el receptor electrónico'), - # Uuid - # TipoCfe - # Serie - # NumeroCfe - - # ('17', 'Aviso que a un CFE emitido se lo ha etiquetado'), - # ('18', 'Aviso que a un CFE emitido se le removió una etiqueta'), - # Uuid - # TipoCfe - # Serie - # NumeroCfe - # RutEmisor - - # ('19', 'Aviso que a un CFE recibido se lo ha etiquetado'), - # ('20', 'Aviso que a un CFE recibido se le removió una etiqueta'), - # Uuid - # TipoCfe - # Serie - # NumeroCfe - # RutEmisor - # Etiquetas - - elif response.Resp.CodRta == '01': - raise UserError(_('No hay notificaciones disponibles en el UCFE')) - else: - raise UserError(_('ERROR: esto es lo que recibimos %s') % response) - - # TODO 620 - Descartar Notificacion - # response3 = self.company_id._l10n_uy_ucfe_inbox_operation('620', { - # 'idReq': response.Resp.idReq, 'TipoNotificacion': response.Resp.TipoNotificacion}) - # if response3.Resp.CodRta != '00': - # raise UserError(_('ERROR: la notificacion no pudo descartarse %s') % response) + 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 response.Resp.CodRta == '01': + _logger.info('No hay notificaciones disponibles en el UCFE para la compañía %s (id: %d)' % (company.name, company.id)) + continue + if response.Resp.CodRta != '00': + _logger.info(_('ERROR: esto es lo que recibimos al solicitar datos de notificación (610) %s') % response) + continue + # If there is notifications + while response.Resp.CodRta == '00': + try: + # response.Resp.TipoNotificacion + # 610 - Solicitud de datos de Notificacion + response2 = company._l10n_uy_ucfe_inbox_operation('610', {'IdReq': l10n_uy_idReq}) + response2_to_string = str(response2) + import xml.etree.ElementTree as ET + xml_string = response2.Resp.XmlCfeFirmado + data_dict = self.xml_to_dict(xml_string) + invoice_line_ids = [] + for key, value in data_dict.items(): + if isinstance(value, dict): + if value.get('Encabezado'): + l10n_latam_document_type_id = value['Encabezado']['IdDoc']['TipoCFE'] + invoice_date = datetime.strptime(value['Encabezado']['IdDoc']['FchEmis'], '%Y-%m-%d').date() + l10n_latam_document_number = value['Encabezado']['IdDoc']['Nro'] + serieCfe = value['Encabezado']['IdDoc']['Serie'] + invoice_currency = value['Encabezado']['Totales']['TpoMoneda'] + amount_total = value['Encabezado']['Totales']['MntTotal'] + cant_lineas = value['Encabezado']['Totales']['CantLinDet'] + #invoice_date_due = datetime.strptime(value['Encabezado']['IdDoc']['FchVenc'], '%Y-%m-%d').date() + 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_vat_RUC = value['Encabezado']['Emisor']['RUCEmisor'] + 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_name = value['Encabezado']['Emisor']['RznSoc'] + partner_city = value['Encabezado']['Emisor']['Ciudad'] + partner_state_id = value['Encabezado']['Emisor']['Departamento'] + partner_street = value['Encabezado']['Emisor']['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_id = self.env['res.partner'].create({'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}) + if isinstance(value['Detalle']['Item'], list): + for el in value['Detalle']['Item']: + invoice_line_ids.append(el) + else: + invoice_line_ids.append(value['Detalle']['Item']) + 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)] + if value['IndFact'] == '1': + # Exento de IVA + domain_tax += [('tax_group_id.l10n_uy_vat_code', '=', 'vat_exempt'), ('name', '=', 'IVA Compras Exento')] + elif value['IndFact'] == '2': + # Gravado a Tasa Mínima + domain_tax += (('tax_group_id.l10n_uy_vat_code', '=', 'vat_10'), ('name', '=', 'IVA Compras 10%')) + elif value['IndFact'] == '3': + # Gravado a Tasa Básica + domain_tax += (('tax_group_id.l10n_uy_vat_code', '=', 'vat_22'), ('name', '=', 'IVA Compras 22%')) + tax_item = self.env['account.tax'].search(domain_tax, limit=1) + line_ids.append((0, 0,{'move_type': 'in_invoice', 'name': value['NomItem'], 'quantity': float(value['Cantidad']), 'price_unit': float(value['PrecioUnitario']), 'tax_ids': [(6, 0, tax_item.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)]): + # 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' + document_type = self.env['l10n_latam.document.type'].search([('code', '=', l10n_latam_document_type_id)], limit=1).id + move_vals = {'move_type': move_type, + '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, + 'l10n_uy_dgi_xml_response': response2_to_string, + 'l10n_uy_idReq': l10n_uy_idReq, + 'currency_id': currency_id.id,} + new_invoice = self.env['account.move'].create(move_vals) + #import pdb; pdb.set_trace() + #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)) + if len(new_invoice.invoice_line_ids) != int(cant_lineas): + raise UserError('La cantidad de líneas de la factura %s (id:%d) es inválida' % (new_invoice.name, new_invoice.id)) + # Obtener pdf del comprobante + req_data_pdf = {'rut': company.vat, + 'rutRecibido': partner_vat_RUC, + 'tipoCfe': l10n_latam_document_type_id, + 'serieCfe': serieCfe, + 'numeroCfe': l10n_latam_document_number} + 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) + }) + new_invoice.action_post() + # ('5', 'Aviso de CFE emitido rechazado por DGI'), or + # ('6', 'Aviso de CFE emitido rechazado por el receptor electrónico'), + # Uuid + # TipoCfe + # Serie + # NumeroCfe + # MensajeRta + # ('7', 'Aviso de CFE recibido'), + # Uuid + # TipoCfe + # Serie + # NumeroCfe + # XmlCfeFirmado + # Adenda + # RutEmisor + # Etiquetas + # EstadoEnDgiCfeRecibido + # ('8', 'Aviso de anulación de CFE recibido'), + # ('9', 'Aviso de aceptación comercial de un CFE recibido'), + # ('10', 'Aviso de aceptación comercial de un CFE recibido en la gestión UCFE'), + # Uuid + # TipoCfe + # Serie + # NumeroCfe + # RutEmisor + # ('11', 'Aviso de que se ha emitido un CFE'), + # ('12', 'Aviso de que se ha emitido un CFE en la gestión UCFE'), + # Uuid + # TipoCfe + # Serie + # NumeroCfe + # XmlCfeFirmado + # Adenda + # Etiquetas + # ('13', 'Aviso de rechazo comercial de un CFE recibido'), + # Uuid + # TipoCfe + # Serie + # NumeroCfe + # MensajeRta + # RutEmisor + # ('14', 'Aviso de rechazo comercial de un CFE recibido en la gestión UCFE'), + # Uuid + # TipoCfe + # Serie + # NumeroCfe + # RutEmisor + # ('15', 'Aviso de CFE emitido aceptado por DGI'), + # ('16', 'Aviso de CFE emitido aceptado por el receptor electrónico'), + # Uuid + # TipoCfe + # Serie + # NumeroCfe + # ('17', 'Aviso que a un CFE emitido se lo ha etiquetado'), + # ('18', 'Aviso que a un CFE emitido se le removió una etiqueta'), + # Uuid + # TipoCfe + # Serie + # NumeroCfe + # RutEmisor + # ('19', 'Aviso que a un CFE recibido se lo ha etiquetado'), + # ('20', 'Aviso que a un CFE recibido se le removió una etiqueta'), + # Uuid + # TipoCfe + # Serie + # NumeroCfe + # RutEmisor + # Etiquetas + # 620 - Descartar Notificacion + response3 = company._l10n_uy_ucfe_inbox_operation('620', { + 'IdReq': response.Resp.IdReq, 'TipoNotificacion': response.Resp.TipoNotificacion}) + if response3.Resp.CodRta != '00': + raise UserError(_('ERROR: la notificacion no pudo descartarse %s') % response) + response = company._l10n_uy_ucfe_inbox_operation('600') + if response.Resp.CodRta == '01': + _logger.info('No hay notificaciones disponibles en el UCFE para la compañía %s (id: %d)' % (company.name, company.id)) + continue + if response.Resp.CodRta != '00': + _logger.info(_('ERROR: esto es lo que recibimos al solicitar datos de notificación (610) %s') % response) + continue + except: + _logger.warning('Encontramos un error al momento de sincronizar comprobantes de proveedor de la compañía: %s (id: %d)' % (company.name, company.id)) + break def action_cfe_inform_commercial_status(self, rejection=False): # TODO only applies for vendor bills @@ -1381,3 +1474,14 @@ def _l10n_uy_dgi_post(self): def action_get_l10n_uy_received_invoices(self): """Consultar comprobantes recibidos""" self.l10n_uy_get_ucfe_notif() + + def action_conciliate_received_invoices(self): + """Conciliar comprobantes recibidos. Manual de integración UCFE v2_13.pdf página 140 --> 7.1.19 Ejecutar el proceso de conciliación. """ + # TODO --> aún en proceso de desarrollo. Primero lo estoy harcodeando para verificar cómo funciona + #conciliacion = 'FC--8485217265990014|111|20231002|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|' + #req_data = {'rut': '218296790015', 'fechaDesde': '2023-04-03', 'fechaHasta': '2023-04-03', 'conciliacion': '00022747', 'nombreParametros': ['received-CFEs'], 'valoresParametros': [1]} + #import pdb; pdb.set_trace() + #response = self.env.company._l10n_uy_ucfe_query('RealizarConciliacion', req_data) + #req_data2 = {'rut': '218296790015', 'identificador': response, 'formato': 'html'} + #response2 = '' + #response2 = self.env.company._l10n_uy_ucfe_query('ObtenerResultadoConciliacion', req_data2) diff --git a/l10n_uy_edi/models/res_company.py b/l10n_uy_edi/models/res_company.py index c1f437ff..24e669d7 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,8 @@ def _l10n_uy_ucfe_inbox_operation(self, msg_type, extra_req={}, return_transport 'Tout': '30000'} if extra_req: data.get('Req').update(extra_req) - + import pprint + pprint.pprint(data) 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..a2d1827b 100644 --- a/l10n_uy_edi/views/account_move_views.xml +++ b/l10n_uy_edi/views/account_move_views.xml @@ -9,6 +9,7 @@
+