# -*- coding: utf-8 -*-
import base64
from email.utils import parseaddr
import operator
import uuid
import logging
from functools import reduce
import qrcode
# import cStringIO
from odoo import models, fields, api
from odoo.exceptions import ValidationError
from odoo.tools import config
from datetime import datetime, timedelta
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
import requests
import json
import re
import os
from zipfile import ZipFile
from .http_helper import HEADERS
from .http_helper import INVOICE
from .http_helper import URL_XML
from .http_helper import DIAN_INVOICE_URL_BASE
from . import xml_helper
# Constants
UTC_ADJUST = '-05:00'
UTC_TIME_CO = 5
HOUR_CHARS = 11
PRECISION = 2
EI_STATE_SELECTION = [
('pending', 'No Transferido'),
('done', 'Emitido'),
('exception', 'Excepción de Envio'),
('dian_reject', 'Rechazado DIAN'),
('dian_accept', 'Aceptado DIAN'),
('customer_reject', 'Rechazado Cliente'),
('customer_accept', 'Aceptado Cliente')
]
TAM_MAP_NAME = {
'01': 'IVA',
'02': 'IC',
'03': 'ICA',
'04': 'INC',
'05': 'ReteIVA',
'06': 'ReteFuente',
'07': 'ReteICA'
}
# Warnings
WRONG_DB_MATCH_WARNING = """
No es posible generar la Factura Electrónica, verifique que la compañía tenga
habilitado el check de Facturación Electrónica y que la base de datos sea la correcta"""
ENV_MISSING_WARNING = """
Debe configurarse en la Compañía el tipo de Ambiente para Facturación Electrónica"""
OP_TYPE_MISSING_WARNING = """
Debe configurarse en la Compañía el Tipo de Operación"""
UNSUPPORTED_PDF_FORMAT_WARNING = """
Formato de pdf de factura electrónica no encontrado,
verifique la parametrizacion en la compañia.
"""
JOURNAL_RESOLUTION_ERROR = """
Error en la resolución del diario:
No se encontro ID de Resolución o Tipo de documento
"""
NO_CURRENCY_RATE_WARNING = """
No se encontró una tasa de cambio para la Fecha de la factura
"""
NO_REFERENCED_INVOICE_ERROR = """
No se encontró una factura electrónica a rectificar válida"""
UNACCEPTED_REFERENCE_ERROR = """
La factura electrónica a rectificar %s no tiene CUFE
"""
PDF_GENERATION_ERROR = """Hubo un error al generar el PDF de la factura"""
SEND_ERROR = """No fue posible enviar la factura al proovedor, error de conexión"""
PROVIDER_ERROR = """
Ha ocurrido un error al procesar la factura, por favor intente de nuevo"""
INVALID_EMAIL_ERROR = """No se encontró un email válido en el cliente"""
EMAIL_GENERATION_ERROR = """No se pudo generar email FE %s"""
# Logger
_logger = logging.getLogger(__name__)
class AccountMove(models.Model):
_inherit = 'account.move'
transaction_log_ids = fields.One2many(
string='Logs', comodel_name='ei.transaction.log',
inverse_name='invoice_id', copy=False)
ei_state = fields.Selection(
string='Estado FE', default='pending', readonly=True, copy=False,
selection=EI_STATE_SELECTION)
ei_cufe = fields.Char(
string='CUFE', readonly=True, size=96, tracking=True,
copy=False)
ei_cude = fields.Char(
string='CUDE', readonly=True, size=96, tracking=True,
copy=False)
ei_qr = fields.Char(string='Informacion QR', readonly=True, copy=False)
ei_xml_content = fields.Text(string='Contenido XML', copy=False)
ei_app_response = fields.Text(
string='Contenido XML Application Response', copy=False)
ei_email_sent = fields.Boolean(string='Email Enviado', copy=False)
ei_generation_date = fields.Char(
string='Hora de Generación', readonly=True, copy=False)
ei_validation_date = fields.Char(
string='Hora de Validacion', readonly=True, copy=False)
out_refund_id = fields.Many2one(
string='Factura Rectificada', comodel_name='account.move', copy=False)
invoice_reference = fields.Char(string='No. Orden / Referencia')
# Acknowledgement management
def _create_token(self):
self.ensure_one()
if not self.access_token:
self.access_token = uuid.uuid4().hex
def do_accept(self):
if self.ei_state in ('customer_accept', 'customer_reject'):
return False
self.ei_state = 'customer_accept'
return True
def do_reject(self):
if self.ei_state in ('customer_accept', 'customer_reject'):
return False
self.ei_state = 'customer_reject'
return True
# Log management
def create_transaction_log(self, invoice, state, content, document_type='none'):
self.env['ei.transaction.log'].create({
'invoice_id': invoice.id,
'date': datetime.now(),
'document_state': state,
'document_type': document_type,
'content': content
})
# Invoice processing
def action_post(self):
company = self.mapped(lambda invoice: invoice.company_id)
res = super(AccountMove, self).action_post()
if company.ei_automatic_generation:
self.generate_electronic_invoice()
return res
def _get_invoice_json(self, invoice):
company = invoice.company_id
company_partner = company.partner_id
invoice_partner = invoice.partner_id
journal = invoice.journal_id
document_type = journal.resolution_id.document_type
resolution = journal.resolution_id
if (not resolution.id_param or not resolution.prefix or not document_type):
self.create_transaction_log(
invoice, 'exception', JOURNAL_RESOLUTION_ERROR)
return False
generation_date_time = (invoice.create_date - timedelta(hours=UTC_TIME_CO)).strftime(
DEFAULT_SERVER_DATETIME_FORMAT) + UTC_ADJUST
invoice.ei_generation_date = generation_date_time
generation_time = generation_date_time[HOUR_CHARS:]
day_rate = self.env['res.currency.rate'].search([
('name', '=', invoice.invoice_date.strftime(DEFAULT_SERVER_DATE_FORMAT)),
('currency_id', '=', invoice.currency_id.id)
])
if invoice.currency_id != company.currency_id and not day_rate:
raise ValidationError(NO_CURRENCY_RATE_WARNING)
currency_name = invoice.currency_id.name
ncnd = "enabled" if document_type in ('91', '92') else "disabled"
case_reference_invoice = {
'91': invoice.reversed_entry_id or invoice.out_refund_id,
'92': invoice.debit_origin_id,
}
reference_invoice = case_reference_invoice.get(document_type, False)
if ncnd == "enabled":
if not reference_invoice:
self.create_transaction_log(
invoice, 'exception', NO_REFERENCED_INVOICE_ERROR)
return False
if not reference_invoice.ei_cufe:
self.create_transaction_log(
invoice, 'exception', UNACCEPTED_REFERENCE_ERROR % reference_invoice.name)
return False
datos_conexion = {
"token": company.software_token,
"documento": company_partner.ref_num,
"dv": str(company_partner.verification_code),
"id_cabecera": "0",
"id_usuario": "1"
}
tipo_documento = {
"numero": document_type
}
basicos_factura = {
"consecutivo": invoice.name.replace(journal.resolution_id.prefix, ''),
"moneda": currency_name,
"tipo_operacion": company.ei_id_customization,
"fecha_factura": invoice.invoice_date.strftime(DEFAULT_SERVER_DATE_FORMAT),
"hora_factura": generation_time
}
respuesta = {
"ruta_post": company.service_url_post,
"ruta_get": company.service_url_get,
"metodo": "ajax",
"extra1": "",
"extra2": ""
}
param_basico = {
"id_param": str(journal.resolution_id.id_param),
"test": "0",
"ambiente": "1" if company.ei_environment == 'production' else "2",
"ruta_to_soap": ("SendBillSync"
if company.ei_environment == 'production'
else "SendTestSetAsync")
}
facturador = {
"ProviderID": company_partner.ref_num,
"dv": str(company_partner.verification_code)
}
autorizacion_descarga = {
"activo": "enabled",
"numero_documento": "",
"dv": "",
"tipo_documento": company_partner.ref_type_id.code_dian
}
WithholdingTaxTotal = {
"aplica": "0"
}
def validate_country_code(country_code):
if country_code == '169':
return 'CO'
return str(country_code)
def is_colombia(country_code):
return country_code in ('169', 'CO')
datos_empresa = {
"Pais": validate_country_code(company_partner.country_id.code),
"departamento": company_partner.state_id.code,
"municipio": company_partner.state_id.code + company_partner.city_id.zipcode,
"direccion": company_partner.street,
"nombre_sucursal": company_partner.city_id.name or company_partner.name
}
partner_country = invoice_partner.country_id.code
datos_cliente = {
"tipo_persona": "1",
"Pais": partner_country,
"municipio": (str(invoice_partner.state_id.code)
+ str(invoice_partner.city_id.zipcode)
if is_colombia(partner_country) else ""),
"numero_documento": invoice_partner.ref_num,
"dv": str(invoice_partner.verification_code),
"tipo_documento": invoice_partner.ref_type_id.code_dian,
"departamento": (invoice_partner.state_id.code
if is_colombia(partner_country) else ""),
"direccion": (invoice_partner.street or "") + (invoice_partner.street2 or ""),
"nombre_sucursal": "",
"RUT_nombre": invoice_partner.name or 'Receptor',
"RUT_pais": partner_country,
"RUT_departamento": (invoice_partner.state_id.code
if is_colombia(partner_country) else ""),
"RUT_municipio": (str(invoice_partner.state_id.code)
+ str(invoice_partner.city_id.zipcode)
if is_colombia(partner_country) else ""),
"RUT_direcci\u00f3n": invoice_partner.street,
"RUT_impuesto": "01",
"Respon_fiscales": "",
"Num_matricula_mercantil": "",
"Nombre_contacto": invoice_partner.name,
"Tel_contacto": invoice_partner.phone,
"Correo_contacto": invoice_partner.email,
"Nota_contacto": ""
}
datos_transportadora = {
"active": "disabled",
"tipo_persona": "1",
"Pais": "",
"municipio": "",
"numero_documento": "",
"dv": "",
"tipo_documento": "",
"departamento": "",
"direccion": "",
"nombre_sucursal": "",
"RUT_nombre": "",
"RUT_pais": "",
"RUT_departamento": "",
"RUT_municipio": "",
"RUT_direcci\u00f3n": "",
"RUT_impuesto": "",
"Respon_fiscales": "",
"Num_matricula_mercantil": "",
"Nombre_contacto": "",
"Tel_contacto": "",
"Telfax_contacto": "",
"Correo_contacto": "",
"Nota_contacto": ""
}
QR = {
"active": "enabled"
}
Periodo_pago = {
"active": "disabled",
"fecha_inicial": "",
"fecha_final": ""
}
payment_term = sum(invoice.invoice_payment_term_id.line_ids.mapped(
lambda line: line.days))
Metodo_pago = {
"active": "enabled",
"codigo_metodo": "2" if payment_term else "1",
"codigo_medio": "ZZZ",
"fecha_vencimiento": (invoice.invoice_date_due.strftime(DEFAULT_SERVER_DATE_FORMAT)
or invoice.invoice_date.strftime(DEFAULT_SERVER_DATE_FORMAT)),
"identificacion_metodo": "Mutuo Acuerdo"
}
Referencia_factura = {
"active": ncnd,
"referencia_afectada": (reference_invoice.name if ncnd == "enabled" else ""),
"cufe_cude": (reference_invoice.ei_cufe if ncnd == "enabled" else ""),
"algoritm_cufe_cude": "CUFE-SHA384",
"fecha_factura": (reference_invoice.invoice_date.strftime(DEFAULT_SERVER_DATE_FORMAT)
if ncnd == "enabled" else "")
}
Referencia_factura2 = {
"active": "disabled",
"referencia_afectada": "",
"cufe_cude": "",
"algoritm_cufe_cude": "",
"fecha_factura": ""
}
respuesta_discrepancia = {
"active": ncnd,
"referencia": (reference_invoice.name
if ncnd == "enabled" else ""),
"codigo": ("4" if document_type == '91'
else "6" if document_type == "92" else ""),
"descripci\u00f3n_correccion": ""
}
order_reference = invoice.invoice_reference or ""
order_de_referencia = {
"active": "enabled" if order_reference else "disabled",
"codigo": order_reference,
"IssueDate": invoice.invoice_date.strftime(DEFAULT_SERVER_DATE_FORMAT),
}
Referencia_envio = {
"active": "disabled",
"id": ""
}
Referencia_recibido = {
"active": "disabled",
"id": ""
}
Terminos_de_entrega = {
"active": "disabled",
"terminos_Especiales": "",
"cod_respo_perdida": ""
}
currency_rate = "enabled" if invoice.currency_id != company.currency_id else "disabled"
currency_rate_val = day_rate.rate or 1.0
Tasa_cambio = {
"active": currency_rate,
"Divisa_base": invoice.currency_id.name,
"Divisa_a_convertir": company.currency_id.name,
"Valor": str(currency_rate_val),
"Fecha_conversion": invoice.invoice_date.strftime(DEFAULT_SERVER_DATE_FORMAT)
}
AdditionalDocumentReference = {
"active": "disabled",
"pre_consec": "",
"fecha_creacion": "",
"identificador": ""
}
Anticipos = [
{
"active": "disabled",
"tipo_anticipo": "RED3123856",
"valor_pago": "1000.00",
"fecha_recibido": "2018-10-01",
"fecha_realizado": "2018-09-29",
"hora_realizado": "23:02:05",
"instrucciones": "Prepago recibido"
},
{
"active": "disabled",
"tipo_anticipo": "RED3123857",
"valor_pago": "850.00",
"fecha_recibido": "2018-10-01",
"fecha_realizado": "2018-09-29",
"hora_realizado": "23:02:05",
"instrucciones": "Prepago recibido"
}
]
Productos_servicios = []
# Invoice lines
for line in invoice.invoice_line_ids.filtered(
lambda line: not line.exclude_from_invoice_tab and line.quantity > 0.0):
sample = line.discount == 100.0
line_taxes = [
[
tax.ei_code,
TAM_MAP_NAME.get(tax.ei_code, ''),
str(abs(round(tax.amount)))
] for tax in
line.tax_ids if tax.ei_code == '01'
]
taxes = reduce(operator.add, line_taxes or [0]) or []
line_detail = {
"active": "enabled",
"Cantidad": str(line.quantity),
"unidad_cantidad": str(line.product_uom_id.code_dian),
"Costo_unidad": str(round(line.price_unit / currency_rate_val, PRECISION)),
"Muestra": "Si" if sample else "No",
"DeliveryLocation_active": ("enabled" if line.product_id.default_code
else "disabled"),
"DeliveryLocation_esq_id": "999",
"DeliveryLocation_nombre": "",
"DeliveryLocation_dato": line.product_id.default_code or "",
"Codigo_muestra": "01" if sample else "0",
"Desc_muestra": str(line.discount or 0.0),
"Valor_muestra": (str(round(line.price_unit / currency_rate_val, PRECISION))
if sample else "0"),
"Descuento_cargo": "Credito",
"ID_descuento_cargo": "11",
"Porcentaje_descuento_cargo": "0.0" if sample else str(line.discount or 0.0),
"Descripcion_descuento_cargo": "Otro descuento",
"Mandatario": "",
"Descripcion": line.product_id.name or "",
"Prefijo_codigo_producto": "",
"Codigo_producto": line.product_id.default_code or "",
"Cantidad_x_paquete": "1",
"Marca": "ninguna",
"Modelo": "ninguna",
"esquema_id": "",
"esquema_dato": "",
"esquema_name": "",
"array_impuestos": taxes,
"tributo_unidad": "0",
"SellersItemIdentification_ID": "",
"InformationContentProviderParty_ID": ""
}
Productos_servicios.append(line_detail)
datafe = {
"datos_conexion": datos_conexion,
"tipo_documento": tipo_documento,
"basicos_factura": basicos_factura,
"respuesta": respuesta,
"param_basico": param_basico,
"facturador": facturador,
"autorizacion_descarga": autorizacion_descarga,
"WithholdingTaxTotal": WithholdingTaxTotal,
"datos_empresa": datos_empresa,
"datos_cliente": datos_cliente,
"datos_transportadora": datos_transportadora,
"QR": QR,
"Periodo_pago": Periodo_pago,
"Metodo_pago": Metodo_pago,
"Referencia_factura": Referencia_factura,
"Referencia_factura2": Referencia_factura2,
"respuesta_discrepancia": respuesta_discrepancia,
"order_de_referencia": order_de_referencia,
"Referencia_envio": Referencia_envio,
"Referencia_recibido": Referencia_recibido,
"Terminos_de_entrega": Terminos_de_entrega,
"Tasa_cambio": Tasa_cambio,
"AdditionalDocumentReference": AdditionalDocumentReference,
"Anticipos": Anticipos,
"Productos_servicios": Productos_servicios
}
return datafe
def _set_validation_date(self, invoice, xml_response):
try:
xml_issue_date = re.search(
r'(.*)',
xml_response).group(1)
xml_issue_time = re.search(
r'(.*)',
xml_response).group(1)
invoice.ei_validation_date = ' '.join(
(xml_issue_date, xml_issue_time))
except:
invoice.ei_validation_date = invoice.ei_generation_date
def send_to_provider(self, url, datafe, invoice):
data = 'json_data=' + \
base64.b64encode(json.dumps(datafe).encode(
'utf-8')).decode('utf-8')
try:
response = requests.post(
url, data=data, headers=HEADERS, verify=False)
self.create_transaction_log(
invoice, 'sent',
json.dumps(datafe).encode('utf-8'), 'json'
)
return response
except:
self.create_transaction_log(
invoice, 'exception', SEND_ERROR
)
return {}
def process_response(self, response, invoice):
document_type = invoice.journal_id.resolution_id.document_type
try:
response_body = response.content.decode('utf-8')
valid = re.search(r'valid: \'(\w*)\'', response_body).group(1)
cufe = (re.search(r'cufe: \'(\w*)\'', response_body).group(1)
if document_type == '01' else '')
cude = (re.search(r'cufe: \'(\w*)\'', response_body).group(1)
if document_type in ('91', '92') else '')
qr_code = re.search(r'qr: \'(.*)\'', response_body).group(1)
response_64 = re.search(
r'response_64: \'(.*)\'', response_body).group(1)
response_xml = str(base64.b64decode(response_64), 'utf-8')
info_check = (invoice.name in response_xml
if invoice.company_id.ei_environment == 'production' else True)
_logger.info("Verificacion de informacion recibida: %s, %s" % (
invoice.name, info_check))
if not info_check:
self.create_transaction_log(
invoice, 'provider_error', PROVIDER_ERROR
)
invoice.ei_state = 'exception'
return {}
if not re.search('Documento validado por la DIAN', response_xml):
response = re.search(r'response: \'(.*)\'', response_body)
response = response.group(1) if response else ''
self.create_transaction_log(
invoice, 'dian_reject', response_xml or response
)
invoice.ei_state = 'dian_reject'
return {}
response_xml = base64.b64decode(response_64)
self._set_validation_date(invoice, response_xml)
return {
'valid': valid,
'cufe': cufe,
'cude': cude,
'qr': qr_code,
'response_64': response_xml
}
except:
error_log = re.search(
r'response_error: \'(.*)\'', response_body)
log_content = (error_log.group(1)
if error_log
else (response_body or response.content or ''))
self.create_transaction_log(
invoice, 'dian_reject', log_content
)
invoice.ei_state = 'dian_reject'
return {}
def send_invoice(self, invoice):
company = invoice.company_id
datafe = self._get_invoice_json(invoice)
if not datafe:
return False
response = self.send_to_provider(company.service_url, datafe, invoice)
if not response:
return False
result = self.process_response(response, invoice)
if not result:
return False
invoice.ei_cufe = result['cufe']
invoice.ei_cude = result['cude']
invoice.ei_qr = result['qr']
response_64 = result['response_64']
invoice.ei_state = 'dian_accept'
invoice.create_transaction_log(
invoice, 'dian_accept', response_64, 'app_response'
)
self.env.cr.commit()
def generate_electronic_invoice(self):
company = self.mapped(lambda invoice: invoice.company_id)
if not company.electronic_invoice or company.ei_database != self._cr.dbname:
raise ValidationError(WRONG_DB_MATCH_WARNING)
if not company.ei_environment:
raise ValidationError(ENV_MISSING_WARNING)
if not company.ei_id_customization:
raise ValidationError(OP_TYPE_MISSING_WARNING)
#
to_send_invoices = self.filtered(
lambda invoice: invoice.ei_state
not in ('dian_accept', 'customer_accept', 'customer_reject')
and invoice.move_type in ['out_invoice', 'out_refund'])
for invoice in to_send_invoices:
self.send_invoice(invoice)
if invoice.ei_state != 'dian_accept':
continue
if not company.auto_acceptance_email:
continue
invoice.send_acknowlegement_email()
# Email Management
def get_invoice_attachments(self):
self.ensure_one()
self.get_attachments()
return True
def resend_acknowlegement_email(self):
self.send_acknowlegement_email(force_send=True)
def valid_email_address(self):
regex = r'(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)'
emails = self.partner_id.ei_email or self.partner_id.email or ''
email_list = emails.split(';')
return list(filter(
lambda email: re.match(regex, email),
map(lambda email: parseaddr(email)[1], email_list)))
def send_acknowlegement_email(self, force_send=False):
email_pool = self.env['mail.mail']
for invoice in self:
invoice._create_token()
if not invoice.valid_email_address():
self.create_transaction_log(
invoice, 'email_fail', INVALID_EMAIL_ERROR
)
continue
email = invoice.prepare_email()
if not email:
_logger.error(EMAIL_GENERATION_ERROR, invoice.name)
continue
email_pool = email_pool + email
invoice.create_transaction_log(invoice, 'email_sent', 'OK')
invoice.ei_email_sent = True
if force_send:
email_pool.send()
def prepare_email(self):
self.ensure_one()
mail_pool = self.env['mail.mail']
ctx = self._context.copy()
ctx.update({
'dbname': self._cr.dbname,
})
template = self.env.ref(
'electronic_invoice_dian.electronic_invoice_customer_acknowlegement')
try:
mail_id = template.with_context(ctx).send_mail(
self.id, force_send=False)
except:
return mail_pool
mail = mail_pool.browse(mail_id)
attachment_ids = self.get_attachments()
if not attachment_ids:
return mail_pool
zipped_attachments = self._zip_attachments(attachment_ids)
if zipped_attachments:
mail.write({'attachment_ids': [(6, 0, zipped_attachments.ids)]})
return mail
return mail_pool
# Mail attachments
def _zip_attachments(self, attachments):
self.ensure_one()
filestore = os.path.join(
config['data_dir'], 'filestore', self.env.cr.dbname, )
xml_file, pdf_file, = self.name + '.xml', self.name + '.pdf'
zip_file = self.name + '.zip'
att_zip_file = 'anexos_' + zip_file
invoice_attachments = attachments.filtered(
lambda att: att.name in [xml_file, pdf_file])
other_attachments = attachments.filtered(
lambda att: att.name not in [xml_file, pdf_file])
current_dir = os.getcwd()
try:
os.chdir(self.company_id.ei_tmp_path)
if other_attachments:
with ZipFile(att_zip_file, 'w') as zip_invoice:
for att in other_attachments:
zip_invoice.write(os.path.join(
filestore, att.store_fname), att.name)
with ZipFile(zip_file, 'w') as zip_invoice:
for att in invoice_attachments:
zip_invoice.write(os.path.join(
filestore, att.store_fname), att.name)
if other_attachments:
zip_invoice.write(att_zip_file, att_zip_file)
zipped_attachment = self.env['ir.attachment'].sudo().create({
'name': zip_file,
'type': 'binary',
'datas': base64.encodebytes(open(zip_file, 'br').read()),
'res_model': 'account.move',
'res_id': self.id,
'mimetype': 'application/zip'
})
map(os.remove, [zip_file] +
([att_zip_file] if other_attachments else[]))
os.chdir(current_dir)
return zipped_attachment
except Exception as exc:
_logger.error(exc)
os.chdir(current_dir)
return attachments
def get_attachments(self):
company = self.company_id
xml_file = self.name + '.xml'
pdf_file = self.name + '.pdf'
attachment_pool = self.env['ir.attachment']
attachment_domain = [
('res_model', '=', 'account.move'),
('res_id', '=', self.id),
]
attachment_ids = attachment_pool.search(attachment_domain)
attachment_names = attachment_ids.mapped(lambda att: att.name)
if xml_file not in attachment_names:
if not self.create_xml_attachment():
return attachment_pool
if pdf_file not in attachment_names:
if not self.create_pdf_attachment():
return attachment_pool
updated_attachment_ids = attachment_pool.search(
attachment_domain)
sale_order_attachments = (company.attach_customer_order
and self.get_sale_order_attachments()
or attachment_pool)
picking_attachments = (company.attach_delivery_note
and self.get_picking_attachments()
or attachment_pool)
if company.attach_invoice_docs:
return (updated_attachment_ids + sale_order_attachments
+ picking_attachments)
xml_and_pdf_attachments = updated_attachment_ids.filtered(
lambda att: att.name in (xml_file, pdf_file)
)
return (xml_and_pdf_attachments + sale_order_attachments
+ picking_attachments)
def get_cufe_from_log(self):
acceptance_log = self.transaction_log_ids.filtered(
lambda log: 'ApplicationResponse' in log.content
and self.name in log.content
and 'Documento validado por la DIAN' in log.content)
if not acceptance_log:
return ''
app_response = acceptance_log[0].content
cufe_group = re.search(
r'(.*)',
app_response)
if not cufe_group:
return ''
cufe_cude = cufe_group.group(1)
if cufe_cude != self.ei_cufe and self.journal_id.resolution_id.document_type == '01':
self.ei_qr = self.ei_qr.replace(self.ei_cufe, cufe_cude)
self.ei_cufe = cufe_cude
if cufe_cude != self.ei_cude and self.journal_id.resolution_id.document_type in (
'91', '92', '03'):
self.ei_qr = self.ei_qr.replace(self.ei_cude, cufe_cude)
self.ei_cude = cufe_cude
return cufe_cude
def _validate_in_dian(self, cuxe):
if self.company_id.ei_environment == 'test':
return True
dian_request = requests.get(DIAN_INVOICE_URL_BASE + cuxe)
dian_response = dian_request.content
if cuxe in dian_response.decode('utf-8'):
return True
return False
def _get_xml_json(self, invoice):
company = self.company_id
company_partner = company.partner_id
document_type = invoice.journal_id.resolution_id.document_type
return {
"datos_conexion": {
"token": company.software_token,
"documento": company_partner.ref_num
},
"key": {
"cufe": invoice.ei_cufe if document_type == '01' else invoice.ei_cude
},
"Datos_software": {
"ambiente": ("1" if company.ei_environment == 'production'
else "0")
}
}
def get_xml(self, datafe):
data = 'json_data=' + \
base64.b64encode(json.dumps(datafe).encode(
'utf-8')).decode('utf-8')
try:
response = requests.post(
URL_XML, data=data, headers=HEADERS, verify=False)
xmlb64 = re.search(
r'(.*)',
str(response.content, 'utf-8')).group(1)
xml_fe = base64.b64decode(xmlb64)
return xml_fe
except:
pass
return ''
def create_xml_attachment(self):
cufe_cude = self.get_cufe_from_log() or self.ei_cufe or self.ei_cude
if not cufe_cude or not self._validate_in_dian(cufe_cude):
self.ei_state = 'exception'
self.ei_cufe = ''
self.ei_cude = ''
self.ei_qr = ''
self.ei_app_response = ''
self.ei_xml_content = ''
return False
xml_fe = self.ei_xml_content or self.get_xml(self._get_xml_json(self))
if not xml_fe:
return False
if not self.ei_xml_content:
self.ei_xml_content = xml_fe
filename = self.name + '.xml'
xml_att_document = self.build_attached_document()
xml_fe_normalized = (xml_att_document.encode('utf-8') if
isinstance(xml_att_document, str) else xml_att_document)
data_attach = {
'name': filename,
'datas': base64.b64encode(xml_fe_normalized),
'res_model': 'account.move',
'res_id': self.id,
'mimetype': 'application/xml',
'type': 'binary'
}
return self.env['ir.attachment'].sudo().create(data_attach)
def create_pdf_attachment(self):
attachment_pool = self.env['ir.attachment']
self.ensure_one()
report = self.company_id.ei_report_id
if report and report.report_type == 'qweb-pdf':
try:
pdf_data, __ = report.sudo()._render_qweb_pdf(self.id)
created_attachment = attachment_pool.search([
('res_model', '=', 'account.move'),
('res_id', '=', self.id),
('name', '=', self.name + '.pdf'),
])
if created_attachment:
return True
data_attach = {
'name': self.name + '.pdf',
'datas': base64.b64encode(pdf_data),
'res_model': 'account.move',
'res_id': self.id,
'mimetype': 'application/pdf',
'type': 'binary'
}
self.env['ir.attachment'].sudo().create(data_attach)
return True
except Exception as exc:
_logger.error(exc)
raise ValidationError(PDF_GENERATION_ERROR)
else:
raise ValidationError(UNSUPPORTED_PDF_FORMAT_WARNING)
def get_order_attachment(self, order):
attachment_pool = self.env['ir.attachment']
if not order.customer_po_file:
return attachment_pool
filename = order.customer_po_name or 'OrdenDeCompra.pdf'
existing_attachment = attachment_pool.search([
('res_model', '=', 'sale.order'),
('res_id', '=', order.id),
('name', '=', filename),
])
if existing_attachment:
return existing_attachment
data_attach = {
'name': filename,
'datas': order.customer_po_file,
'res_model': 'sale.order',
'res_id': order.id,
'mimetype': 'application/pdf',
'type': 'binary'
}
return self.env['ir.attachment'].sudo().create(data_attach)
def get_sale_orders(self):
order_lines = self.env['sale.order.line'].search([
('invoice_lines', 'in', self.invoice_line_ids.ids)
])
orders = order_lines.mapped(lambda line: line.order_id)
return orders
def get_sale_order_attachments(self):
self.ensure_one()
attachment_pool = self.env['ir.attachment']
orders = self.get_sale_orders()
if not orders:
return attachment_pool
attachments = orders.mapped(self.get_order_attachment)
return attachments
def get_picking_attachments(self):
return self.env['ir.attachment']
# Mass send, Unused
def ei_email_mass_send(self):
to_email_invoices = self.env['account.move'].search([
('ei_state', '=', 'dian_accep'),
('ei_email_sent', '=', False)
], limit=100)
to_email_invoices.send_acknowlegement_email()
# XML AttachedDocument
def _get_header_tags(self):
company = self.company_id
utc_adj = '-05:00'
return {
'UBLVersionID': 'DIAN 2.1',
'CustomizationID': 'Documentos adjuntos',
'ProfileID': 'DIAN 2.1',
'ProfileExecutionID': ("1" if company.ei_environment == 'production' else "2"),
'ID': uuid.uuid4().hex,
'IssueDate': self.ei_generation_date.split(' ')[0],
'IssueTime': self.ei_generation_date.split(' ')[1],
'DocumentType': u'Contenedor de Factura Electrónica',
'ParentDocumentID': str(self.name)
}
def _get_sender_tags(self):
company = self.company_id
return {
'RegistrationName': company.name,
'CompanyID': company.partner_id.ref_num,
'TaxLevelCode': company.tributary_obligations or 'R-99-PN',
'ID': '01',
'Name': 'IVA'
}
def _get_sender_attrs(self):
company = self.company_id
return {
'CompanyID': {
'schemeAgencyID': "195",
'schemeID': str(company.partner_id.verification_code or 0),
'schemeName': company.partner_id.ref_type_id.code_dian
},
'TaxLevelCode': {
'listName': "48"
},
}
def _get_receiver_tags(self):
return {
'RegistrationName': self.partner_id.name,
'CompanyID': self.partner_id.ref_num,
'TaxLevelCode': 'R-99-PN',
'ID': '01',
'Name': 'IVA'
}
def _get_receiver_attrs(self):
company = self.company_id
return {
'CompanyID': {
'schemeAgencyID': "195",
'schemeID': str(self.partner_id.verification_code or 0),
'schemeName': self.partner_id.ref_type_id.code_dian
},
'TaxLevelCode': {
'listName': "48"
},
}
def _get_attachment_tags(self):
return {
'MimeCode': 'text/xml',
'EncodingCode': 'UTF-8',
'Description': 'ei_xml_content',
}
def _get_doc_line_tags(self):
issue_date = self.ei_generation_date
validation_date, validation_time = (self.ei_validation_date.split(
' ') if self.ei_validation_date else ('', ''))
return {
'LineID': '1',
'ID': self.name,
'UUID': (self.ei_cufe or self.ei_cude),
'IssueDate': issue_date[:10],
'DocumentType': 'ApplicationResponse',
'MimeCode': 'text/xml',
'EncodingCode': 'UTF-8',
'Description': 'ei_app_response',
'ValidatorID': u'Unidad Especial Dirección de Impuestos y Aduanas Nacionales',
'ValidationResultCode': u'02',
'ValidationDate': str(validation_date),
'ValidationTime': str(validation_time),
}
def _get_doc_line_attrs(self):
return {
'UUID': {
'schemeName': "CUFE-SHA384"
}
}
def build_attached_document(self):
vals = {
'header': {
'tags': self._get_header_tags(),
'attrs': {}
},
'sender': {
'tags': self._get_sender_tags(),
'attrs': self._get_sender_attrs()
},
'receiver': {
'tags': self._get_receiver_tags(),
'attrs': self._get_receiver_attrs()
},
'attachment': {
'tags': self._get_attachment_tags(),
'attrs': {}
},
'doc_line': {
'tags': self._get_doc_line_tags(),
'attrs': self._get_doc_line_attrs()
}
}
log_app_response = self.transaction_log_ids.filtered(
lambda log: log.document_type == 'app_response')
app_response = log_app_response[0].content if log_app_response else ''
return xml_helper.build_xml_attached_document(
self.ei_xml_content, app_response, vals)