Skip to content

Commit

Permalink
[IMP] l10n_uy_edi: Creacion de facturas de proveedor automaticamente
Browse files Browse the repository at this point in the history
Task: 28036
  • Loading branch information
pablohmontenegro committed Oct 23, 2023
1 parent a5cd0fe commit dbffe75
Show file tree
Hide file tree
Showing 7 changed files with 258 additions and 5 deletions.
2 changes: 1 addition & 1 deletion l10n_uy_account/views/account_journal_view.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<field name="inherit_id" ref="l10n_latam_invoice_document.view_account_journal_form"/>
<field name="arch" type="xml">
<field name="type" position="after">
<field name="l10n_uy_type" attrs="{'invisible':['|', '|', ('country_code', '!=', 'UY'), ('l10n_latam_use_documents', '=', False), ('type', '!=', 'sale')], 'required':[('country_code', '=', 'UY'), ('l10n_latam_use_documents', '=', True), ('type', '=', 'sale')]}"/>
<field name="l10n_uy_type" attrs="{'invisible':['|', '|', ('country_code', '!=', 'UY'), ('l10n_latam_use_documents', '=', False), ('type', 'not in', ['sale', 'purchase'])], 'required':[('country_code', '=', 'UY'), ('l10n_latam_use_documents', '=', True), ('type', 'in', ['sale', 'purchase'])]}"/>
</field>
<field name="refund_sequence" position="attributes">
<attribute name="attrs">{'invisible': ['|', ('type', 'not in', ['sale', 'purchase']), ('l10n_latam_use_documents', '=', True), ('country_code', '=', 'UY')]}</attribute>
Expand Down
2 changes: 1 addition & 1 deletion l10n_uy_edi/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
10 changes: 10 additions & 0 deletions l10n_uy_edi/data/ir_cron.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@
<field name="active" eval="True"/>
</record>

<record id="ir_cron_get_vendor_bills_received" model="ir.cron">
<field name="name">UY: Consult vendor bills received</field>
<field name="interval_number">10</field>
<field name="interval_type">minutes</field>
<field name="numbercall">-1</field>
<field name="model_id" ref="model_account_move"/>
<field name="state">code</field>
<field name="code">model.l10n_uy_action_get_l10n_uy_received_invoices()</field>
<field name="active" eval="True"/>
</record>
<!-- TODO KZ generar el reporte diario en odoo a partir del generado en ucfe
<field name="nextcall" eval="(DateTime.now().replace(hour=22, minute=0, second=0) + timedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')"/> -->

Expand Down
1 change: 1 addition & 0 deletions l10n_uy_edi/models/account_move.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
243 changes: 242 additions & 1 deletion l10n_uy_edi/models/l10n_uy_cfe.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -578,6 +579,241 @@ 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 = xml_string.replace('ns0:', '')
xml_string = re.sub(r'<CFE[^>]*>', '<CFE>', 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 --> creo que al final no haría falta
new_invoice.write({
'l10n_uy_dgi_xml_response': transport.xml_response,
'l10n_uy_cfe_xml': xml_string,
'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:
_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
Expand Down Expand Up @@ -1360,3 +1596,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()

3 changes: 1 addition & 2 deletions l10n_uy_edi/models/res_company.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down
Loading

0 comments on commit dbffe75

Please sign in to comment.