forked from Mapan/odoo17e
542 lines
28 KiB
Python
542 lines
28 KiB
Python
# -*- 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
|
|
|
|
|
|
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))
|
|
|
|
def _l10n_co_edi_get_round_amount(self, amount):
|
|
if amount == '':
|
|
return ''
|
|
if abs(amount - float("%.2f" % amount)) > 0.00001:
|
|
return "%.3f" % amount
|
|
return '%.2f' % amount
|
|
|
|
def _l10n_co_edi_prepare_tim_sections(self, taxes_dict, invoice_currency, retention, tax_details=None, actual_tax_details=None, in_COP=False):
|
|
# taxes_dict is no longer used and will be removed in master
|
|
tax_details = tax_details or {}
|
|
actual_tax_details = actual_tax_details or {}
|
|
|
|
suffix = '' if in_COP else '_currency'
|
|
currency_name = 'COP' if in_COP else invoice_currency.name
|
|
base_amount_field = f'base_amount{suffix}'
|
|
tax_amount_field = f'tax_amount{suffix}'
|
|
|
|
# Mapping CO tax type -> TIM section
|
|
new_taxes_dict = defaultdict(lambda: {
|
|
'TIM_1': bool(retention),
|
|
'TIM_2': 0.0,
|
|
'TIM_3': currency_name,
|
|
'TIM_4': 0.0,
|
|
'TIM_5': currency_name,
|
|
'IMPS': [],
|
|
})
|
|
|
|
for grouping_key, tax_detail in actual_tax_details['tax_details'].items():
|
|
tax_type = grouping_key['l10n_co_edi_type']
|
|
if tax_type.retention != retention:
|
|
continue
|
|
# Construct the IMP and add it to the TIM section (one IMP per tax *rate*)
|
|
tim = new_taxes_dict[tax_type.code]
|
|
if tax_type.code == '05':
|
|
imp_2 = abs(tax_detail[tax_amount_field] * 100 / 15)
|
|
elif tax_type.code == '34':
|
|
imp_2 = sum(line.product_id.volume * line.quantity
|
|
for line in tax_detail['records']) # Volume
|
|
else:
|
|
imp_2 = abs(tax_detail[base_amount_field])
|
|
imp = {
|
|
'IMP_1': tax_type.code,
|
|
'IMP_2': imp_2,
|
|
'IMP_3': currency_name,
|
|
'IMP_4': abs(tax_detail[tax_amount_field]),
|
|
'IMP_5': currency_name,
|
|
'IMP_11': tax_type.name,
|
|
}
|
|
if grouping_key['amount_type'] == 'fixed':
|
|
imp.update({
|
|
'IMP_6': 0,
|
|
'IMP_7': 1,
|
|
'IMP_8': '94',
|
|
'IMP_9': grouping_key['amount'], # Tax rate
|
|
'IMP_10': currency_name,
|
|
})
|
|
if tax_type.code == '22':
|
|
imp['IMP_8'] = 'BO'
|
|
elif tax_type.code == '34':
|
|
imp.update({
|
|
'IMP_7': imp['IMP_2'],
|
|
'IMP_8': 'MLT',
|
|
'IMP_9': imp['IMP_2'] and float_round(abs(tax_detail[tax_amount_field]) * 100 / imp['IMP_2'], 2),
|
|
})
|
|
else:
|
|
imp.update({
|
|
'IMP_6': 15.0 if tax_type.code == '05' else abs(grouping_key['amount']),
|
|
'IMP_7': '',
|
|
'IMP_8': '',
|
|
'IMP_9': '',
|
|
'IMP_10': '',
|
|
})
|
|
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 new_taxes_dict
|
|
|
|
# -------------------------------------------------------------------------
|
|
# 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_monetary(number, currency):
|
|
# Format the monetary values to avoid trailing decimals (e.g. 90.85000000000001).
|
|
return float_repr(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:
|
|
|
|
<NOT><NOT_1>text</NOT_1></NOT>
|
|
|
|
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.category_id == self.env.ref('uom.product_uom_categ_vol'))
|
|
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.category_id == self.env.ref('uom.product_uom_categ_kgm'))
|
|
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.category_id == self.env.ref('uom.product_uom_categ_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('<a.*?>')
|
|
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',
|
|
}
|
|
|
|
def group_tax_retention(base_line, tax_values):
|
|
tax = tax_values['tax_repartition_line'].tax_id
|
|
return {'tax': tax, 'l10n_co_edi_type': tax.l10n_co_edi_type}
|
|
|
|
def group_tax_tim(base_line, tax_values):
|
|
""" Tax details to be used for the TIM section: taxes should be grouped per CO tax type, then per tax rate.
|
|
"""
|
|
tax = tax_values['tax_repartition_line'].tax_id
|
|
return {
|
|
'amount': tax.amount,
|
|
'amount_type': tax.amount_type,
|
|
'l10n_co_edi_type': tax.l10n_co_edi_type,
|
|
}
|
|
|
|
def l10n_co_filter_to_apply(base_line, tax_values):
|
|
return tax_values['tax_repartition_line'].tax_id.l10n_co_edi_type.code not in code_to_filter
|
|
|
|
tax_details_tim = invoice._prepare_edi_tax_details(filter_to_apply=l10n_co_filter_to_apply, grouping_key_generator=group_tax_tim)
|
|
tax_details = invoice._prepare_edi_tax_details(filter_to_apply=l10n_co_filter_to_apply, grouping_key_generator=group_tax_retention)
|
|
retention_taxes = [(group, detail) for group, detail in tax_details['tax_details'].items() if detail['l10n_co_edi_type'].retention]
|
|
regular_taxes = [(group, detail) for group, detail in tax_details['tax_details'].items() if not detail['l10n_co_edi_type'].retention]
|
|
|
|
exempt_tax_dict = {}
|
|
tax_group_covered_goods = self.env.ref('l10n_co.tax_group_covered_goods', raise_if_not_found=False)
|
|
for line in invoice.invoice_line_ids:
|
|
if tax_group_covered_goods and tax_group_covered_goods in line.mapped('tax_ids.tax_group_id'):
|
|
exempt_tax_dict[line.id] = True
|
|
|
|
# Remove in master: retention_lines_listdict, regular_lines_listdict no longer used
|
|
retention_lines = move_lines_with_tax_type.filtered(
|
|
lambda move: move.tax_line_id.l10n_co_edi_type.retention)
|
|
retention_lines_listdict = defaultdict(list)
|
|
for line in retention_lines:
|
|
retention_lines_listdict[line.tax_line_id.l10n_co_edi_type.code].append(line)
|
|
|
|
regular_lines = move_lines_with_tax_type - retention_lines
|
|
regular_lines_listdict = defaultdict(list)
|
|
for line in regular_lines:
|
|
regular_lines_listdict[line.tax_line_id.l10n_co_edi_type.code].append(line)
|
|
|
|
zero_tax_details = defaultdict(float)
|
|
for line, tax_detail in tax_details['tax_details_per_record'].items():
|
|
for tax, detail in tax_detail.get('tax_details').items():
|
|
if not detail.get('tax_amount'):
|
|
tax = tax.get('tax')
|
|
for grouped_tax in detail.get('group_tax_details'):
|
|
zero_tax_details[tax.l10n_co_edi_type.code] += abs(grouped_tax.get('base_amount'))
|
|
retention_taxes_new = self._l10n_co_edi_prepare_tim_sections(retention_lines_listdict, invoice.currency_id, True, None, tax_details_tim)
|
|
regular_taxes_new = self._l10n_co_edi_prepare_tim_sections(regular_lines_listdict, invoice.currency_id, False, zero_tax_details, tax_details_tim)
|
|
|
|
retention_taxes_new_COP = self._l10n_co_edi_prepare_tim_sections(retention_lines_listdict, invoice.currency_id, True, None, tax_details_tim, in_COP=True)
|
|
regular_taxes_new_COP = self._l10n_co_edi_prepare_tim_sections(retention_lines_listdict, invoice.currency_id, False, zero_tax_details, tax_details_tim, in_COP=True)
|
|
|
|
# The rate should indicate how many pesos is one foreign currency
|
|
currency_rate_number = tax_details['base_amount'] / tax_details['base_amount_currency'] if tax_details['base_amount_currency'] else 1
|
|
currency_rate = "%.2f" % currency_rate_number
|
|
|
|
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]))
|
|
|
|
# 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 = "{0:0=2d}".format(int(invoice.l10n_co_edi_type))
|
|
|
|
# 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"
|
|
|
|
# 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].selection).get(description_code) if description_code else None
|
|
|
|
invoice_lines = invoice.invoice_line_ids.filtered(lambda line: line.display_type == 'product')
|
|
invoice_lines_values = {}
|
|
for line in invoice_lines:
|
|
price_subtotal = sign * line.balance
|
|
if line.discount == 100:
|
|
price_subtotal_before_discount = line.price_unit * line.quantity * currency_rate_number
|
|
else:
|
|
price_subtotal_before_discount = price_subtotal / (1 - line.discount / 100)
|
|
if line.quantity:
|
|
price_unit = price_subtotal_before_discount / line.quantity
|
|
else:
|
|
price_unit = line.price_unit * currency_rate_number
|
|
invoice_lines_values[line.id] = {
|
|
'price_unit': price_unit,
|
|
'price_subtotal': price_subtotal,
|
|
'discount_amount': price_subtotal_before_discount - price_subtotal,
|
|
'price_subtotal_before_discount': price_subtotal_before_discount,
|
|
}
|
|
|
|
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,
|
|
'retention_taxes': retention_taxes,
|
|
'retention_taxes_new': retention_taxes_new,
|
|
'retention_taxes_new_COP': retention_taxes_new_COP,
|
|
'regular_taxes': regular_taxes,
|
|
'regular_taxes_new': regular_taxes_new,
|
|
'regular_taxes_new_COP': regular_taxes_new_COP,
|
|
'tax_details': tax_details,
|
|
'tax_types': invoice.mapped('line_ids.tax_ids.l10n_co_edi_type'),
|
|
'exempt_tax_dict': exempt_tax_dict,
|
|
'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,
|
|
'invoice_lines': invoice_lines,
|
|
'invoice_lines_values': invoice_lines_values,
|
|
'validation_time': validation_time,
|
|
'delivery_date': invoice.invoice_date + timedelta(1),
|
|
'description_code': description_code,
|
|
'description': description,
|
|
'format_monetary': format_monetary,
|
|
'format_domestic_phone_number': format_domestic_phone_number,
|
|
'_l10n_co_edi_get_round_amount': self._l10n_co_edi_get_round_amount
|
|
})
|
|
return b'<?xml version="1.0" encoding="utf-8"?>' + 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.with_context(no_new_invoice=True).message_post(
|
|
body=_('Electronic invoice submission succeeded. Message from Carvajal:') + Markup('<br/>)' + 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.with_context(no_new_invoice=True).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()
|
|
if self.code != 'ubl_carvajal':
|
|
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,
|
|
}
|
|
|
|
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.now()
|
|
oldest_date = now - timedelta(days=5)
|
|
newest_date = now + timedelta(days=10)
|
|
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 == '2' 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 <= fields.Datetime.to_datetime(move.invoice_date) <= newest_date):
|
|
move.message_post(body=_('The issue date can not be older than 5 days or more than 5 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."))
|
|
|
|
# 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.volume == 0:
|
|
edi_result.append(_("You should set a volume 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}}
|