# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, api, models, _
from odoo.tools.float_utils import float_compare
from odoo.tools import DEFAULT_SERVER_TIME_FORMAT, float_repr, float_round
from odoo.tools import html2plaintext
from .carvajal_request import CarvajalRequest
import pytz
import base64
import re
from collections import defaultdict
from datetime import timedelta
from markupsafe import Markup
from .account_invoice import L10N_CO_EDI_TYPE
class AccountEdiFormat(models.Model):
_inherit = 'account.edi.format'
# -------------------------------------------------------------------------
# Helpers
# -------------------------------------------------------------------------
@api.model
def _l10n_co_edi_generate_electronic_invoice_filename(self, invoice):
'''Generates the filename for the XML sent to Carvajal. A separate
sequence is used because Carvajal requires the invoice number
to only contain digits.
'''
seq_code = 'l10n_co_edi.filename'
IrSequence = self.env['ir.sequence'].with_company(invoice.company_id)
invoice_number = IrSequence.next_by_code(seq_code)
# if a sequence does not yet exist for this company create one
if not invoice_number:
IrSequence.sudo().create({
'name': 'Colombian electronic invoicing sequence for company %s' % invoice.company_id.id,
'code': seq_code,
'implementation': 'no_gap',
'padding': 10,
'number_increment': 1,
'company_id': invoice.company_id.id,
})
invoice_number = IrSequence.next_by_code(seq_code)
return 'face_{}{:0>10}{:010x}.xml'.format(invoice._l10n_co_edi_get_electronic_invoice_type(),
invoice.company_id.vat,
int(invoice_number))
@api.model
def _l10n_co_edi_prepare_tii_sections(self, base_line, aggregated_values):
is_vendor_bill = base_line['record'].move_id.is_purchase_document()
currency_name = 'COP' if not is_vendor_bill else base_line['currency_id'].name
base_amount_field = 'base_amount' if not is_vendor_bill else 'base_amount_currency'
tax_amount_field = 'tax_amount' if not is_vendor_bill else 'tax_amount_currency'
tii_sections = []
for grouping_key, values in aggregated_values.items():
if not grouping_key or grouping_key['skip']:
continue
type_name = grouping_key['l10n_co_edi_type'].name
type_code = grouping_key['l10n_co_edi_type'].code
amount = grouping_key['amount']
amount_type = grouping_key['amount_type']
iim2 = values[tax_amount_field]
if type_code == '05':
if amount == -2.85:
iim4 = abs(values[base_amount_field] * 0.19)
else:
iim4 = abs(values[base_amount_field] * 0.05)
elif type_code == '34':
iim4 = base_line['product_id'].l10n_co_edi_ref_nominal_tax * base_line['quantity']
else:
iim4 = abs(values[base_amount_field])
if type_code != '34' or not iim2:
iim9 = amount
else:
iim9 = (iim2 * 100 / iim4) if iim4 else 0.0
iim6 = 15.0 if type_code == '05' else abs(amount)
if is_vendor_bill and amount_type != 'fixed':
tii4 = (iim6 * iim4 / 100) - iim2
else:
tii4 = None
values = {
'TII_1': abs(iim2),
'TII_2': currency_name,
'TII_3': 'true' if grouping_key['l10n_co_edi_type'].retention else 'false',
'TII_4': tii4,
'TII_5': currency_name if is_vendor_bill else None,
'IIM_1': type_code,
'IIM_2': abs(iim2),
'IIM_3': currency_name,
'IIM_4': iim4,
'IIM_5': currency_name,
'IIM_6': iim6,
'IIM_7': '',
'IIM_8': '',
'IIM_9': '',
'IIM_10': '',
'IIM_11': type_name,
}
if amount_type == 'fixed':
if type_code == '22':
iim8 = 'BO'
elif type_code == '34':
iim8 = 'MLT'
else:
iim8 = '94'
values.update({
'IIM_6': None,
'IIM_7': iim4 if type_code == '34' else '1',
'IIM_8': iim8,
'IIM_9': iim9,
'IIM_10': currency_name,
})
tii_sections.append(values)
return tii_sections
@api.model
def _l10n_co_edi_prepare_tim_sections(self, invoice, values_per_grouping_key, is_retention, in_cop):
is_vendor_bill = invoice.is_purchase_document()
currency_name = 'COP' if in_cop else invoice.currency_id.name
base_amount_field = 'base_amount' if in_cop else 'base_amount_currency'
tax_amount_field = 'tax_amount' if in_cop else 'tax_amount_currency'
tim_per_code = defaultdict(lambda: {
'TIM_1': 'true' if is_retention else 'false',
'TIM_2': 0.0,
'TIM_3': currency_name,
'TIM_4': 0.0 if is_vendor_bill else None,
'TIM_5': currency_name if is_vendor_bill else None,
'IMPS': [],
})
for grouping_key, values in values_per_grouping_key.items():
if not grouping_key or grouping_key['skip'] or grouping_key['is_retention'] != is_retention:
continue
type_name = grouping_key['l10n_co_edi_type'].name
type_code = grouping_key['l10n_co_edi_type'].code
amount = grouping_key['amount']
tim = tim_per_code[type_code]
tim['type_code'] = type_code
if type_code == '05':
imp2 = abs(values[tax_amount_field] * 100 / 15)
elif type_code == '34':
base_line_x_taxes_data = values['base_line_x_taxes_data']
imp2 = sum(
base_line['product_id'].l10n_co_edi_ref_nominal_tax * base_line['quantity']
for base_line, _taxes_data in base_line_x_taxes_data
)
else:
imp2 = abs(values[base_amount_field])
imp = {
'IMP_1': type_code,
'IMP_2': imp2,
'IMP_3': currency_name,
'IMP_4': abs(values[tax_amount_field]),
'IMP_5': currency_name,
'IMP_11': type_name,
}
if grouping_key['amount_type'] == 'fixed':
imp.update({
'IMP_6': 0,
'IMP_6_dp': 2,
'IMP_7': 1,
'IMP_8': '94',
'IMP_9': grouping_key['amount'], # Tax rate
'IMP_10': currency_name,
})
if type_code == '22':
imp['IMP_8'] = 'BO'
elif type_code == '34':
imp.update({
'IMP_7': imp['IMP_2'],
'IMP_8': 'MLT',
'IMP_9': float_round(abs(values[tax_amount_field]) * 100 / imp['IMP_2'], 2) if imp['IMP_2'] else None,
})
else:
imp6_dp = 2
if type_code == '05':
imp6 = 15.0
else:
imp6 = abs(amount)
if abs(imp6 - float("%.2f" % imp6)) > 0.00001:
imp6_dp = 3
imp.update({
'IMP_6': imp6,
'IMP_6_dp': imp6_dp,
'IMP_7': '',
'IMP_8': '',
'IMP_9': '',
'IMP_10': '',
})
if tim['TIM_4'] is not None:
tim['TIM_4'] += float_round((imp['IMP_6'] / 100.0 * imp['IMP_2']) - imp['IMP_4'], 2)
tim['TIM_2'] += imp['IMP_4']
tim['IMPS'].append(imp)
return list(tim_per_code.values())
# -------------------------------------------------------------------------
# Generation
# -------------------------------------------------------------------------
def _l10n_co_edi_generate_xml(self, invoice):
'''Renders the XML that will be sent to Carvajal.'''
def format_domestic_phone_number(phone):
'''The CDE_3 field only allows for 10 characters (since Anexo 1.9).
Probably since Colombian telephone numbers are 10 digit numbers when excluding the country prefix (January 2024).
'''
phone = (phone or '').replace(' ', '')
if len(phone) <= 10:
return phone
phone = re.sub(r'^(\+57|0057)', '', phone)
return phone[:10]
def format_float(number, dp):
return float_repr(number, dp) if number not in (None, '') else number
def format_monetary(number, currency):
return format_float(number, currency.decimal_places)
def get_notas():
'''This generates notes in a particular format. These notes are pieces
of text that are added to the PDF in various places. |'s are
interpreted as newlines by Carvajal. Each note is added to the
XML as follows:
text
One might wonder why Carvajal uses this arbitrary format
instead of some extra simple XML tags but such questions are best
left to philosophers, not dumb developers like myself.
'''
# Volume has to be reported in l (not e.g. ml).
if invoice.move_type in ('in_invoice', 'in_refund'):
company_partner = invoice.company_id.partner_id
invoice_partner = invoice.partner_id.commercial_partner_id
return [
'23.-%s' % ("|".join([
company_partner.street or '',
company_partner.city or '',
company_partner.country_id.name or '',
company_partner.phone or '',
company_partner.email or '',
])),
'24.-%s' % ("|".join([invoice_partner.phone or '',
invoice_partner.ref or '',
invoice_partner.email or '',
])),
]
lines = invoice.invoice_line_ids.filtered(lambda line: line.product_uom_id and line.product_uom_id._has_common_reference(self.env.ref('uom.product_uom_litre')))
liters = sum(line.product_uom_id._compute_quantity(line.quantity, self.env.ref('uom.product_uom_litre')) for line in lines)
total_volume = int(liters)
# Weight has to be reported in kg (not e.g. g).
lines = invoice.invoice_line_ids.filtered(lambda line: line.product_uom_id and line.product_uom_id._has_common_reference(self.env.ref('uom.product_uom_gram')))
kg = sum(line.product_uom_id._compute_quantity(line.quantity, self.env.ref('uom.product_uom_kgm')) for line in lines)
total_weight = int(kg)
# Units have to be reported as units (not e.g. boxes of 12).
lines = invoice.invoice_line_ids.filtered(lambda line: line.product_uom_id and line.product_uom_id._has_common_reference(self.env.ref('uom.product_uom_unit')))
units = sum(line.product_uom_id._compute_quantity(line.quantity, self.env.ref('uom.product_uom_unit')) for line in lines)
total_units = int(units)
withholding_amount = invoice.amount_untaxed + abs(sum(invoice.line_ids.filtered(lambda line: line.tax_line_id and not line.tax_line_id.l10n_co_edi_type.retention).mapped('amount_currency')))
amount_in_words = invoice.currency_id.with_context(lang=invoice.partner_id.lang or 'es_ES').amount_to_text(withholding_amount)
reg_a_tag = re.compile('')
clean_narration = re.sub(reg_a_tag, '', invoice.narration) if invoice.narration else False
narration = (html2plaintext(clean_narration or '') and html2plaintext(clean_narration) + ' ') + (invoice.invoice_origin or '')
notas = [
'1.-%s|%s|%s|%s|%s|%s' % (invoice.company_id.l10n_co_edi_header_gran_contribuyente or '',
invoice.company_id.l10n_co_edi_header_tipo_de_regimen or '',
invoice.company_id.l10n_co_edi_header_retenedores_de_iva or '',
invoice.company_id.l10n_co_edi_header_autorretenedores or '',
invoice.company_id.l10n_co_edi_header_resolucion_aplicable or '',
invoice.company_id.l10n_co_edi_header_actividad_economica or ''),
'2.-%s' % (invoice.company_id.l10n_co_edi_header_bank_information or '').replace('\n', '|'),
('3.- %s' % (narration or 'N/A'))[:5000],
'6.- %s|%s' % (html2plaintext(invoice.invoice_payment_term_id.note), amount_in_words),
'7.- %s' % (invoice.company_id.website),
'8.-%s|%s|%s' % (invoice.partner_id.commercial_partner_id._get_vat_without_verification_code() or '', invoice.partner_shipping_id.phone or '', invoice.invoice_origin and invoice.invoice_origin.split(',')[0] or ''),
'10.- | | | |%s' % (invoice.invoice_origin and invoice.invoice_origin.split(',')[0] or 'N/A'),
'11.- |%s| |%s|%s' % (total_units, total_weight, total_volume)
]
return notas
invoice = invoice.with_context(lang=invoice.partner_id.lang)
code_to_filter = ['07', 'ZZ'] if invoice.move_type in ('in_invoice', 'in_refund') else ['ZZ']
move_lines_with_tax_type = invoice.line_ids.filtered(lambda l: l.tax_line_id.l10n_co_edi_type.code not in [False] + code_to_filter)
ovt_tax_codes = ('01C', '02C', '03C')
ovt_taxes = move_lines_with_tax_type.filtered(lambda move: move.tax_line_id.l10n_co_edi_type.code in ovt_tax_codes).tax_line_id
invoice_type_to_ref_1 = {
'out_invoice': 'IV',
'out_refund': 'NC',
}
AccountTax = self.env['account.tax']
base_amls = invoice.line_ids.filtered(lambda x: x.display_type == 'product')
base_lines = [invoice._prepare_product_base_line_for_taxes_computation(x) for x in base_amls]
tax_amls = invoice.line_ids.filtered('tax_repartition_line_id')
tax_lines = [invoice._prepare_tax_line_for_taxes_computation(x) for x in tax_amls]
AccountTax._add_tax_details_in_base_lines(base_lines, invoice.company_id)
AccountTax._round_base_lines_tax_details(base_lines, invoice.company_id, tax_lines=tax_lines)
tax_group_covered_goods = self.env.ref('l10n_co.tax_group_covered_goods', raise_if_not_found=False)
for index, base_line in enumerate(base_lines, start=1):
price_unit = base_line['price_unit']
quantity = base_line['quantity']
discount = base_line['discount']
rate = base_line['rate']
tax_details = base_line['tax_details']
base_line['co_edi_index'] = index
base_line['co_price_subtotal'] = tax_details['total_excluded'] + tax_details['delta_total_excluded']
if discount == 100:
co_price_subtotal_before_discount = (price_unit * quantity) / rate if rate else 0.0
else:
co_price_subtotal_before_discount = base_line['co_price_subtotal'] / (1 - discount / 100)
base_line['co_price_subtotal_before_discount'] = co_price_subtotal_before_discount
base_line['co_discount_amount'] = co_price_subtotal_before_discount - base_line['co_price_subtotal']
if quantity:
co_price_unit = co_price_subtotal_before_discount / quantity
else:
co_price_unit = price_unit / rate if rate else 0.0
base_line['co_price_unit'] = co_price_unit
base_line['co_is_exempt_tax'] = tax_group_covered_goods and tax_group_covered_goods in base_line['tax_ids'].tax_group_id
def grouping_function_tim_sections(base_line, tax_data):
if not tax_data:
return None
tax = tax_data['tax']
return {
'amount': tax.amount,
'amount_type': tax.amount_type,
'l10n_co_edi_type': tax.l10n_co_edi_type,
'skip': tax_data['tax'].l10n_co_edi_type.code in code_to_filter,
'is_retention': tax.l10n_co_edi_type.retention,
}
base_lines_aggregated_values = AccountTax._aggregate_base_lines_tax_details(base_lines, grouping_function_tim_sections)
for base_line, aggregated_values in base_lines_aggregated_values:
base_line['co_tii_sections'] = self._l10n_co_edi_prepare_tii_sections(base_line, aggregated_values)
values_per_grouping_key = AccountTax._aggregate_base_lines_aggregated_values(base_lines_aggregated_values)
is_vendor_bill = invoice.is_purchase_document()
tim_sections_retention = self._l10n_co_edi_prepare_tim_sections(
invoice, values_per_grouping_key, True, not is_vendor_bill)
tim_sections_regular = self._l10n_co_edi_prepare_tim_sections(
invoice, values_per_grouping_key, False, not is_vendor_bill)
vmt_section = {
'vmt7': 0.0,
'vmt8': 0.0,
'vmt16': 0.0,
'vmt17': 0.0,
'vmt18': 0.0,
}
if is_vendor_bill or invoice.currency_id == invoice.company_currency_id:
vmt_tim_sections = tim_sections_retention + tim_sections_regular
else:
tim_sections_retention_foreign = self._l10n_co_edi_prepare_tim_sections(
invoice, values_per_grouping_key, True, False)
tim_sections_regular_foreign = self._l10n_co_edi_prepare_tim_sections(
invoice, values_per_grouping_key, False, False)
vmt_tim_sections = tim_sections_retention_foreign + tim_sections_regular_foreign
for tim_section in vmt_tim_sections:
type_code = tim_section['type_code']
if type_code == '01':
vmt_section['vmt7'] += tim_section['TIM_2']
elif type_code == '04':
vmt_section['vmt8'] += tim_section['TIM_2']
elif type_code == '06':
vmt_section['vmt16'] += tim_section['TIM_2']
elif type_code == '07':
vmt_section['vmt17'] += tim_section['TIM_2']
elif type_code == '08':
vmt_section['vmt18'] += tim_section['TIM_2']
# The rate should indicate how many pesos is one foreign currency
currency_rate = "%.2f" % (1 / invoice.invoice_currency_rate if invoice.invoice_currency_rate else 0.0)
sign = 1 if invoice.is_outbound() else -1
regular_tax_lines = invoice.line_ids.filtered(
lambda line: line.tax_line_id and not line.tax_line_id.l10n_co_edi_type.retention and line.tax_line_id.l10n_co_edi_type.code not in code_to_filter
)
withholding_amount = '%.2f' % (invoice.amount_untaxed + abs(sum(regular_tax_lines.mapped('amount_currency'))))
withholding_amount_company = '%.2f' % (-sign * invoice.amount_untaxed_signed + sum(sign * line.balance for line in regular_tax_lines))
# ENC_8: validation_time
validation_time = fields.Datetime.now()
validation_time = pytz.utc.localize(validation_time)
bogota_tz = pytz.timezone('America/Bogota')
validation_time = validation_time.astimezone(bogota_tz)
validation_time = validation_time.strftime(DEFAULT_SERVER_TIME_FORMAT) + "-05:00"
# ENC_9: edi_type
if invoice.move_type == 'out_refund':
edi_type = "91"
elif invoice.move_type == 'out_invoice' and invoice.l10n_co_edi_debit_note:
edi_type = "92"
else:
edi_type = invoice.l10n_co_edi_type
# description
description_field = None
if invoice.move_type in ('out_refund', 'in_refund'):
description_field = 'l10n_co_edi_description_code_credit'
if invoice.move_type in ('out_invoice', 'in_invoice') and invoice.l10n_co_edi_debit_note:
description_field = 'l10n_co_edi_description_code_debit'
description_code = invoice[description_field] if description_field else None
description = dict(invoice._fields[description_field]._description_selection(self.env)).get(description_code) if description_code else None
xml_content = self.env['ir.qweb']._render(self._l10n_co_edi_get_electronic_invoice_template(invoice), {
'invoice': invoice,
'sign': sign,
'edi_type': edi_type,
'company_partner': invoice.company_id.partner_id,
'sales_partner': invoice.user_id,
'invoice_partner': invoice.partner_id.commercial_partner_id,
'tax_types': invoice.mapped('line_ids.tax_ids.l10n_co_edi_type'),
'currency_rate': currency_rate,
'shipping_partner': invoice.partner_shipping_id,
'invoice_type_to_ref_1': invoice_type_to_ref_1,
'ovt_taxes': ovt_taxes,
'float_compare': float_compare,
'notas': get_notas(),
'withholding_amount': withholding_amount,
'withholding_amount_company': withholding_amount_company,
'base_lines': base_lines,
'tim_sections_retention': tim_sections_retention,
'tim_sections_regular': tim_sections_regular,
'vmt_section': vmt_section,
'validation_time': validation_time,
'delivery_date': invoice.invoice_date + timedelta(1),
'description_code': description_code,
'description': description,
'format_float': format_float,
'format_monetary': format_monetary,
'format_domestic_phone_number': format_domestic_phone_number,
})
return b'' + xml_content.encode()
def _l10n_co_edi_get_electronic_invoice_template(self, invoice):
if invoice.move_type in ('in_invoice', 'in_refund'):
return 'l10n_co_edi.electronic_invoice_vendor_document_xml'
return 'l10n_co_edi.electronic_invoice_xml'
def _l10n_co_post_invoice_step_1(self, invoice):
'''Sends the xml to carvajal.
'''
# == Generate XML ==
xml_filename = self._l10n_co_edi_generate_electronic_invoice_filename(invoice)
xml = self._l10n_co_edi_generate_xml(invoice)
attachment = self.env['ir.attachment'].create({
'name': xml_filename,
'res_id': invoice.id,
'res_model': invoice._name,
'type': 'binary',
'raw': xml,
'mimetype': 'application/xml',
'description': _('Colombian invoice UBL generated for the %s document.', invoice.name),
})
# == Upload ==
request = CarvajalRequest(invoice.move_type, invoice.company_id)
response = request.upload(xml_filename, xml)
if 'error' not in response:
invoice.l10n_co_edi_transaction = response['transactionId']
# == Chatter ==
invoice.message_post(
body=_('Electronic invoice submission succeeded. Message from Carvajal:') + Markup('
)' + response['message']),
attachment_ids=attachment.ids,
)
# Do not return the attachment because it is not signed yet.
else:
# Return the attachment with the error to allow debugging.
response['attachment'] = attachment
return response
def _l10n_co_post_invoice_step_2(self, invoice):
'''Checks the current status of an uploaded XML with Carvajal. It
posts the results in the invoice chatter and also attempts to
download a ZIP containing the official XML and PDF if the
invoice is reported as fully validated.
'''
request = CarvajalRequest(invoice.move_type, invoice.company_id)
response = request.check_status(invoice)
if not response.get('error'):
response['success'] = True
invoice.l10n_co_edi_cufe_cude_ref = response['l10n_co_edi_cufe_cude_ref']
# == Create the attachment ==
if 'filename' in response and 'xml_file' in response:
response['attachment'] = self.env['ir.attachment'].create({
'name': response['filename'],
'res_id': invoice.id,
'res_model': invoice._name,
'type': 'binary',
'datas': base64.b64encode(response['xml_file']),
'mimetype': 'application/xml',
'description': _('Colombian invoice UBL generated for the %s document.', invoice.name),
})
# == Chatter ==
invoice.message_post(body=response['message'], attachments=response['attachments'])
elif response.get('blocking_level') == 'error':
invoice.l10n_co_edi_transaction = False
return response
# -------------------------------------------------------------------------
# BUSINESS FLOW: EDI
# -------------------------------------------------------------------------
def _needs_web_services(self):
# OVERRIDE
return self.code == 'ubl_carvajal' or super()._needs_web_services()
def _is_compatible_with_journal(self, journal):
# OVERRIDE
self.ensure_one()
if self.code != 'ubl_carvajal':
return super()._is_compatible_with_journal(journal)
return journal.type in ['sale', 'purchase'] and journal.country_code == 'CO'
def _get_move_applicability(self, move):
# EXTENDS account_edi
self.ensure_one()
is_dian_used = (
'l10n_co_dian_provider' in move.company_id._fields
and move.company_id.l10n_co_dian_provider == 'dian'
)
if self.code != 'ubl_carvajal' or is_dian_used:
return super()._get_move_applicability(move)
# Determine on which invoices the EDI must be generated.
co_edi_needed = move.country_code == 'CO' and (
move.move_type in ('in_invoice', 'in_refund')
and bool(self.env.ref('l10n_co_edi.electronic_invoice_vendor_document_xml', raise_if_not_found=False))
) or (
move.move_type in ('out_invoice', 'out_refund')
)
if co_edi_needed:
if move.l10n_co_edi_transaction:
return {
'post': self._l10n_co_edi_post_invoice_step_2,
}
else:
return {
'post': self._l10n_co_edi_post_invoice_step_1,
'edi_content': self._l10n_co_edi_generate_xml,
}
def _check_move_configuration(self, move):
# OVERRIDE
self.ensure_one()
edi_result = super()._check_move_configuration(move)
if self.code != 'ubl_carvajal':
return edi_result
company = move.company_id
journal = move.journal_id
now = fields.Datetime.context_timestamp(self.with_context(tz='America/Bogota'), fields.Datetime.now()).date()
oldest_date = now - timedelta(days=6)
newest_date = now + timedelta(days=6)
if not company.sudo().l10n_co_edi_username or not company.sudo().l10n_co_edi_password or not company.l10n_co_edi_company or \
not company.sudo().l10n_co_edi_account:
edi_result.append(_("Carvajal credentials are not set on the company, please go to Accounting Settings and set the credentials."))
if (move.move_type != 'out_refund' and not move.debit_origin_id) and \
(not journal.l10n_co_edi_dian_authorization_number or not journal.l10n_co_edi_dian_authorization_date or not journal.l10n_co_edi_dian_authorization_end_date):
edi_result.append(_("'Resolución DIAN' fields must be set on the journal %s", journal.display_name))
if not move.partner_id.vat:
edi_result.append(_("You can not validate an invoice that has a partner without VAT number."))
if not move.company_id.partner_id.l10n_co_edi_obligation_type_ids:
edi_result.append(_("'Obligaciones y Responsabilidades' on the Customer Fiscal Data section needs to be set for the partner %s.", move.company_id.partner_id.display_name))
if not move.amount_total:
edi_result.append(_("You cannot send Documents in Carvajal without an amount."))
if not move.partner_id.commercial_partner_id.l10n_co_edi_obligation_type_ids:
edi_result.append(_("'Obligaciones y Responsabilidades' on the Customer Fiscal Data section needs to be set for the partner %s.", move.partner_id.commercial_partner_id.display_name))
if move.l10n_co_edi_type == L10N_CO_EDI_TYPE['Export Invoice'] and \
any(l.product_id and not l.product_id.l10n_co_edi_customs_code for l in move.invoice_line_ids):
edi_result.append(_("Every exportation product must have a customs code."))
elif move.invoice_date and not (oldest_date <= move.invoice_date <= newest_date):
move.message_post(body=_('The issue date can not be older than 6 days or more than 6 days in the future.'))
elif any(l.product_id and not l.product_id.default_code and \
not l.product_id.barcode and not l.product_id.unspsc_code_id for l in move.invoice_line_ids):
edi_result.append(_("Every product on a line should at least have a product code (barcode, internal, UNSPSC) set."))
if not move.company_id.partner_id.l10n_latam_identification_type_id.l10n_co_document_code:
edi_result.append(_("The Identification Number Type on the company\'s partner should be 'NIT'."))
if not move.partner_id.commercial_partner_id.l10n_latam_identification_type_id.l10n_co_document_code:
edi_result.append(_("The Identification Number Type on the customer\'s partner should be 'NIT'."))
if move.l10n_co_edi_operation_type == '20' and not move.l10n_co_edi_description_code_credit:
edi_result.append(_("Credit Notes that reference an invoice require to have a Credit Note Concept, please fill in this value"))
if not move.company_id.partner_id.email:
edi_result.append(_("Your company's contact should have a reception email set."))
if not move.l10n_co_edi_type:
edi_result.append(_("You need to set the Electronic Invoice Type on the invoice."))
# Sugar taxes
for line in move.invoice_line_ids:
if "IBUA" in line.tax_ids.l10n_co_edi_type.mapped('name') and line.product_id.l10n_co_edi_ref_nominal_tax == 0:
edi_result.append(_("You should set 'Volume in milliliters' on product: %s when using IBUA taxes.", line.product_id.name))
return edi_result
def _l10n_co_edi_post_invoice_step_1(self, invoice):
return {invoice: self._l10n_co_post_invoice_step_1(invoice)}
def _l10n_co_edi_post_invoice_step_2(self, invoice):
return {invoice: self._l10n_co_post_invoice_step_2(invoice)}
# to remove in master
def _l10n_co_edi_cancel_invoice(self, invoice):
return {invoice: {'success': True}}