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..6dec327f 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,108 +579,295 @@ 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('No hay notificaciones disponibles en el UCFE para la compañía %s (id: %d)' % (company.name, company.id))
+ return True
+ elif response.Resp.CodRta != '00':
+ _logger.info(_('ERROR: esto es lo que recibimos al solicitar datos de notificación (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. """
+ 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)
+
+ 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('XML Empty')
+ 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')
+ invoice_date = datetime.strptime(root.findtext('.//*FchEmis'), '%Y-%m-%d').date()
+ l10n_latam_document_number = root.findtext('.//*Nro')
+ serieCfe = root.findtext('.//*Serie')
+ 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_vat_RUC = root.findtext('.//*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_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)]):
+ # 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
+ # 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('><')])
+ 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': transport.xml_response,
+ 'l10n_uy_cfe_xml': l10n_uy_cfe_xml,
+ 'l10n_uy_dgi_xml_request': transport.xml_request,
+ 'l10n_uy_idreq': l10n_uy_idreq,
+ 'currency_id': currency_id.id,}
+ if invoice_date_due:
+ move_vals['invoice_date_due'] = invoice_date_due
+ req_data_pdf = {'rut': company.vat,
+ 'rutRecibido': partner_vat_RUC,
+ 'tipoCfe': l10n_latam_document_type_id,
+ 'serieCfe': serieCfe,
+ 'numeroCfe': l10n_latam_document_number}
+ return req_data_pdf, self.env['account.move'].create(move_vals), cant_lineas
+
+ def l10n_uy_create_pdf_vendor_invoice(self, company, new_invoice, req_data_pdf):
+ """ Aquí se crea el pdf de comprobante de proveedor que es sincronizado a través de solicitud de notificaciones de Uruware. """
+ 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)
+ })
+
@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.company_id._l10n_uy_ucfe_inbox_operation('610', {'idReq': response.Resp.idReq})
-
- # ('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 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)
+ #if self.l10n_uy_verify_codrta(company, response2):
+ # continue
+ req_data_pdf, new_invoice, cant_lineas = 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))
+ 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
+ self.l10n_uy_create_pdf_vendor_invoice(company, new_invoice, req_data_pdf)
+ # ('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
+ self.l10n_uy_dismiss_notification(company, response)
+ response = company._l10n_uy_ucfe_inbox_operation('600')
+ if self.l10n_uy_verify_codrta(company,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
@@ -1360,3 +1548,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_ucfe_notif()
+
diff --git a/l10n_uy_edi/models/res_company.py b/l10n_uy_edi/models/res_company.py
index c1f437ff..0891af3e 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,
diff --git a/l10n_uy_edi/views/account_move_views.xml b/l10n_uy_edi/views/account_move_views.xml
index 69cea97a..e4e394d1 100644
--- a/l10n_uy_edi/views/account_move_views.xml
+++ b/l10n_uy_edi/views/account_move_views.xml
@@ -37,6 +37,7 @@
+