# -*- 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}}