810 lines
36 KiB
Python
810 lines
36 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright 2021 Joan Marín <Github@JoanMarin>
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
|
|
|
import pytz
|
|
from dateutil import tz
|
|
from datetime import datetime, timedelta
|
|
from uuid import uuid4
|
|
from odoo import api, models, fields, SUPERUSER_ID, _
|
|
from odoo.exceptions import UserError
|
|
|
|
DIAN_TYPES = ('e-support_document', 'e-invoicing', 'e-credit_note', 'e-debit_note')
|
|
|
|
|
|
class AccountMove(models.Model):
|
|
_inherit = "account.move"
|
|
|
|
@api.model
|
|
def _default_operation_type(self):
|
|
user = self.env['res.users'].search([('id', '=', self.env.user.id)])
|
|
view_operation_type_field = False
|
|
|
|
if (user.has_group('l10n_co_account_e_invoicing.group_view_operation_type_field')
|
|
and self.env.user.id != SUPERUSER_ID):
|
|
view_operation_type_field = True
|
|
|
|
if 'move_type' in self._context.keys():
|
|
if self._context['move_type'] == 'out_invoice' and not view_operation_type_field:
|
|
return '10'
|
|
elif self._context['move_type'] == 'in_invoice':
|
|
return '10'
|
|
else:
|
|
return False
|
|
elif not view_operation_type_field:
|
|
return '10'
|
|
else:
|
|
return False
|
|
|
|
@api.model
|
|
def _default_invoice_type_code(self):
|
|
user = self.env['res.users'].search([('id', '=', self.env.user.id)])
|
|
view_invoice_type_field = False
|
|
|
|
if (user.has_group('l10n_co_account_e_invoicing.group_view_invoice_type_field')
|
|
and self.env.user.id != SUPERUSER_ID):
|
|
view_invoice_type_field = True
|
|
|
|
if 'move_type' in self._context.keys():
|
|
if self._context['move_type'] == 'out_invoice' and not view_invoice_type_field:
|
|
return '01'
|
|
elif self._context['move_type'] == 'in_invoice':
|
|
return '05'
|
|
else:
|
|
return False
|
|
elif not view_invoice_type_field:
|
|
return '01'
|
|
else:
|
|
return False
|
|
|
|
@api.model
|
|
def _default_send_invoice_to_dian(self):
|
|
return self.env.user.company_id.send_invoice_to_dian or '0'
|
|
|
|
def _compute_warn_certificate(self):
|
|
warn_remaining_certificate = False
|
|
warn_inactive_certificate = False
|
|
|
|
if self.company_id.einvoicing_enabled:
|
|
warn_inactive_certificate = True
|
|
|
|
if (self.company_id.certificate_file
|
|
and self.company_id.certificate_password
|
|
and self.company_id.certificate_date):
|
|
remaining_days = self.company_id.certificate_remaining_days or 0
|
|
today = fields.Date.context_today(self)
|
|
date_to = self.company_id.certificate_date
|
|
days = (date_to - today).days
|
|
warn_inactive_certificate = False
|
|
|
|
if days < remaining_days:
|
|
if days < 0:
|
|
warn_inactive_certificate = True
|
|
else:
|
|
warn_remaining_certificate = True
|
|
|
|
self.warn_inactive_certificate = warn_inactive_certificate
|
|
self.warn_remaining_certificate = warn_remaining_certificate
|
|
|
|
def _compute_sequence_resolution_id(self):
|
|
for invoice_id in self:
|
|
sequence_resolution = False
|
|
|
|
if (invoice_id.move_type == "out_invoice"
|
|
or (invoice_id.journal_id.sequence_id.dian_type == "e-support_document"
|
|
and invoice_id.move_type == "in_invoice")):
|
|
sequence_resolution_ids = self.env['ir.sequence.date_range'].search([
|
|
('sequence_id', '=', invoice_id.journal_id.sequence_id.id)])
|
|
|
|
for sequence_resolution_id in sequence_resolution_ids:
|
|
move_name = invoice_id.move_name or ''
|
|
number = move_name.replace(sequence_resolution_id.prefix or '', '')
|
|
|
|
if sequence_resolution_id.active_resolution:
|
|
sequence_resolution = sequence_resolution_id
|
|
|
|
if (number.isnumeric()
|
|
and sequence_resolution_id.number_from <= int(number)
|
|
and int(number) <= sequence_resolution_id.number_to):
|
|
sequence_resolution = sequence_resolution_id
|
|
|
|
invoice_id.sequence_resolution_id = sequence_resolution
|
|
|
|
def _compute_dbname(self):
|
|
self.dbname = self._cr.dbname
|
|
|
|
warn_remaining_certificate = fields.Boolean(
|
|
string="Warn About Remainings?",
|
|
compute="_compute_warn_certificate",
|
|
store=False)
|
|
warn_inactive_certificate = fields.Boolean(
|
|
string="Warn About Inactive Certificate?",
|
|
compute="_compute_warn_certificate",
|
|
store=False)
|
|
sequence_resolution_id = fields.Many2one(
|
|
comodel_name='ir.sequence.date_range',
|
|
string='Sequence Resolution',
|
|
compute='_compute_sequence_resolution_id',
|
|
store=False)
|
|
dbname = fields.Char(string="DB Name", compute="_compute_dbname", store=False)
|
|
invoice_datetime = fields.Datetime(string='Invoice Datetime', copy=False)
|
|
delivery_datetime = fields.Datetime(string='Delivery Datetime', copy=False)
|
|
operation_type = fields.Selection(
|
|
selection=[
|
|
('09', 'AIU'),
|
|
('10', 'Standard'),
|
|
('20', 'Credit note that references an e-invoice'),
|
|
('22', 'Credit note without reference to invoices *'),
|
|
('30', 'Debit note that references an e-invoice'),
|
|
('32', 'Debit note without reference to invoices *')],
|
|
string='Operation Type',
|
|
default=_default_operation_type,
|
|
copy=False)
|
|
invoice_type_code = fields.Selection(
|
|
selection=[
|
|
('01', 'E-invoice of sale'),
|
|
('03', 'E-document of transmission - type 03'),
|
|
('04', 'E-invoice of sale - type 04'),
|
|
('05', 'E-Support Document')],
|
|
string='Invoice Type',
|
|
default=_default_invoice_type_code,
|
|
copy=False)
|
|
send_invoice_to_dian = fields.Selection(
|
|
selection=[('0', 'Immediately'), ('1', 'After 1 Day'), ('2', 'After 2 Days')],
|
|
string='Send Invoice to DIAN?',
|
|
default=_default_send_invoice_to_dian,
|
|
copy=False)
|
|
access_token = fields.Char(string='Access Token', copy=False)
|
|
is_accepted_rejected = fields.Boolean(string='Is Accepted/Rejected?', copy=False)
|
|
accepted_rejected_datetime = fields.Datetime(
|
|
string='Datetime of Accepted/Rejected', copy=False)
|
|
receipt_document_reference = fields.Char(
|
|
string='Merchandise / Service Receipt Document',
|
|
copy=False)
|
|
dian_document_state = fields.Selection(
|
|
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')],
|
|
string='DIAN Document State',
|
|
default='pending',
|
|
copy=False,
|
|
readonly=True)
|
|
sale_order_id = fields.Many2one(
|
|
comodel_name='sale.order',
|
|
string='Sale Order',
|
|
copy=False,
|
|
readonly=True,
|
|
states={'draft': [('readonly', False)]})
|
|
merge_with_sale_order = fields.Boolean(
|
|
string='Merge With Sale Order?',
|
|
copy=False,
|
|
readonly=True,
|
|
states={'draft': [('readonly', False)]})
|
|
summary_line_ids = fields.One2many(
|
|
comodel_name='account.move.summary.line',
|
|
inverse_name='move_id',
|
|
string='Summary of Invoice Lines',
|
|
copy=False,
|
|
readonly=True,
|
|
states={'draft': [('readonly', False)]})
|
|
summary_amount_untaxed = fields.Monetary(
|
|
string='Untaxed Amount',
|
|
readonly=True,
|
|
copy=False)
|
|
summary_amount_tax = fields.Monetary(string='Tax', readonly=True, copy=False)
|
|
summary_amount_total = fields.Monetary(string='Total', readonly=True, copy=False)
|
|
dian_document_ids = fields.One2many(
|
|
comodel_name='account.move.dian.document',
|
|
inverse_name='invoice_id',
|
|
string='DIAN Documents',
|
|
copy=False,
|
|
readonly=True,
|
|
states={'draft': [('readonly', False)]})
|
|
|
|
def _create_token(self):
|
|
self.access_token = self.access_token or uuid4().hex
|
|
|
|
def _get_active_dian_resolution(self):
|
|
msg = _("You do not have an active dian resolution, contact with your administrator.")
|
|
resolution_number = False
|
|
|
|
if self.sequence_resolution_id:
|
|
resolution_number = self.sequence_resolution_id.resolution_number
|
|
date_from = self.sequence_resolution_id.date_from
|
|
date_to = self.sequence_resolution_id.date_to_resolution
|
|
prefix = self.sequence_resolution_id.prefix
|
|
number_from = self.sequence_resolution_id.number_from
|
|
number_to = self.sequence_resolution_id.number_to
|
|
technical_key = self.sequence_resolution_id.technical_key
|
|
|
|
if not resolution_number:
|
|
raise UserError(msg)
|
|
|
|
return {
|
|
'InvoiceAuthorization': resolution_number,
|
|
'StartDate': date_from,
|
|
'EndDate': date_to,
|
|
'Prefix': prefix,
|
|
'From': number_from,
|
|
'To': number_to,
|
|
'technical_key': technical_key
|
|
}
|
|
|
|
def _get_billing_reference(self):
|
|
msg1 = _("You can not make a refund invoice of an invoice with state different "
|
|
"to 'Posted'.")
|
|
msg2 = _("You can not make a refund invoice of an invoice with DIAN documents "
|
|
"with state 'Draft', 'Sent' or 'Cancelled'.")
|
|
billing_reference = {}
|
|
refund_invoice_id = self.reversed_entry_id or self.debit_origin_id
|
|
|
|
if refund_invoice_id:
|
|
if refund_invoice_id.state != 'posted':
|
|
raise UserError(msg1)
|
|
|
|
if refund_invoice_id.state == 'posted':
|
|
dian_document_state_done = False
|
|
dian_document_state_cancel = False
|
|
dian_document_state_sent = False
|
|
dian_document_state_draft = False
|
|
|
|
for dian_document_id in refund_invoice_id.dian_document_ids:
|
|
if dian_document_id.state == 'done':
|
|
dian_document_state_done = True
|
|
billing_reference['ID'] = refund_invoice_id.name
|
|
billing_reference['UUID'] = dian_document_id.cufe_cude
|
|
billing_reference[
|
|
'IssueDate'] = refund_invoice_id.invoice_date
|
|
billing_reference[
|
|
'CustomizationID'] = refund_invoice_id.operation_type
|
|
|
|
if dian_document_id.state == 'cancel':
|
|
dian_document_state_cancel = True
|
|
|
|
if dian_document_id.state == 'draft':
|
|
dian_document_state_draft = True
|
|
|
|
if dian_document_id.state == 'sent':
|
|
dian_document_state_sent = True
|
|
|
|
if ((not dian_document_state_done
|
|
and dian_document_state_cancel)
|
|
or dian_document_state_draft
|
|
or dian_document_state_sent):
|
|
raise UserError(msg2)
|
|
|
|
return billing_reference
|
|
|
|
def _get_payment_exchange_rate(self):
|
|
rate = 1
|
|
|
|
if self.currency_id != self.company_id.currency_id:
|
|
rate = self.currency_id._convert(
|
|
rate,
|
|
self.company_id.currency_id,
|
|
self.company_id,
|
|
self.date)
|
|
|
|
return {
|
|
'SourceCurrencyCode': self.currency_id.name,
|
|
'TargetCurrencyCode': self.company_id.currency_id.name,
|
|
'CalculationRate': rate,
|
|
'Date': self.date
|
|
}
|
|
|
|
def _get_einvoicing_taxes(self):
|
|
msg1 = _("Your tax: '%s', has no e-invoicing tax group type, contact with your "
|
|
"administrator.")
|
|
msg2 = _("Your withholding tax: '%s', has amount equal to zero (0), the "
|
|
"withholding taxes must have amount different to zero (0), contact with "
|
|
"your administrator.")
|
|
msg3 = _("Your tax: '%s', has negative amount or an amount equal to zero (0), "
|
|
"the taxes must have an amount greater than zero (0), contact with your "
|
|
"administrator.")
|
|
taxes = {}
|
|
tax_total_base = 0
|
|
withholding_taxes = {}
|
|
currency_rate = self._get_payment_exchange_rate()
|
|
|
|
for move_line in self.line_ids.filtered(lambda l: l.tax_line_id):
|
|
if move_line.tax_line_id.tax_group_id.is_einvoicing:
|
|
if not move_line.tax_line_id.tax_group_id.tax_group_type_id:
|
|
raise UserError(msg1 % move_line.name)
|
|
|
|
tax_code = move_line.tax_line_id.tax_group_id.tax_group_type_id.code
|
|
tax_name = move_line.tax_line_id.tax_group_id.tax_group_type_id.name
|
|
tax_type = move_line.tax_line_id.tax_group_id.tax_group_type_id.type
|
|
tax_percent = '{:.2f}'.format(move_line.tax_line_id.amount)
|
|
tax_amount = move_line.price_total
|
|
tax_base = move_line.tax_base_amount / currency_rate['CalculationRate']
|
|
|
|
if tax_type == 'withholding_tax' and move_line.tax_line_id.amount == 0:
|
|
raise UserError(msg2 % move_line.name)
|
|
|
|
if tax_type == 'tax' and move_line.tax_line_id.amount <= 0:
|
|
raise UserError(msg3 % move_line.name)
|
|
|
|
if tax_amount != (tax_base * move_line.tax_line_id.amount / 100):
|
|
tax_amount = tax_base * move_line.tax_line_id.amount / 100
|
|
|
|
if tax_type == 'withholding_tax' and move_line.tax_line_id.amount > 0:
|
|
if tax_code not in withholding_taxes:
|
|
withholding_taxes[tax_code] = {}
|
|
withholding_taxes[tax_code]['total'] = 0
|
|
withholding_taxes[tax_code]['name'] = tax_name
|
|
withholding_taxes[tax_code]['taxes'] = {}
|
|
|
|
if tax_percent not in withholding_taxes[tax_code]['taxes']:
|
|
withholding_taxes[tax_code]['taxes'][tax_percent] = {}
|
|
withholding_taxes[tax_code]['taxes'][tax_percent][
|
|
'base'] = 0
|
|
withholding_taxes[tax_code]['taxes'][tax_percent][
|
|
'amount'] = 0
|
|
|
|
withholding_taxes[tax_code]['total'] += tax_amount * (-1)
|
|
withholding_taxes[tax_code]['taxes'][tax_percent]['base'] += tax_base
|
|
tax_total_base += tax_base
|
|
withholding_taxes[tax_code]['taxes'][tax_percent][
|
|
'amount'] += tax_amount * (-1)
|
|
elif tax_type == 'withholding_tax' and move_line.tax_line_id.amount < 0:
|
|
# TODO 3.0 Las retenciones se recomienda no enviarlas a la DIAN
|
|
# Solo las positivas que indicarian una autorretencion, Si la DIAN
|
|
# pide que se envien las retenciones, seria quitar o comentar este if
|
|
pass
|
|
else:
|
|
if tax_code not in taxes:
|
|
taxes[tax_code] = {}
|
|
taxes[tax_code]['total'] = 0
|
|
taxes[tax_code]['name'] = tax_name
|
|
taxes[tax_code]['taxes'] = {}
|
|
|
|
if tax_percent not in taxes[tax_code]['taxes']:
|
|
taxes[tax_code]['taxes'][tax_percent] = {}
|
|
taxes[tax_code]['taxes'][tax_percent]['base'] = 0
|
|
taxes[tax_code]['taxes'][tax_percent]['amount'] = 0
|
|
|
|
taxes[tax_code]['total'] += tax_amount
|
|
taxes[tax_code]['taxes'][tax_percent]['base'] += tax_base
|
|
tax_total_base += tax_base
|
|
taxes[tax_code]['taxes'][tax_percent]['amount'] += tax_amount
|
|
|
|
if '01' not in taxes:
|
|
taxes['01'] = {}
|
|
taxes['01']['total'] = 0
|
|
taxes['01']['name'] = 'IVA'
|
|
taxes['01']['taxes'] = {}
|
|
taxes['01']['taxes']['0.00'] = {}
|
|
taxes['01']['taxes']['0.00']['base'] = 0
|
|
taxes['01']['taxes']['0.00']['amount'] = 0
|
|
|
|
if '04' not in taxes:
|
|
taxes['04'] = {}
|
|
taxes['04']['total'] = 0
|
|
taxes['04']['name'] = 'ICA'
|
|
taxes['04']['taxes'] = {}
|
|
taxes['04']['taxes']['0.00'] = {}
|
|
taxes['04']['taxes']['0.00']['base'] = 0
|
|
taxes['04']['taxes']['0.00']['amount'] = 0
|
|
|
|
if '03' not in taxes:
|
|
taxes['03'] = {}
|
|
taxes['03']['total'] = 0
|
|
taxes['03']['name'] = 'INC'
|
|
taxes['03']['taxes'] = {}
|
|
taxes['03']['taxes']['0.00'] = {}
|
|
taxes['03']['taxes']['0.00']['base'] = 0
|
|
taxes['03']['taxes']['0.00']['amount'] = 0
|
|
|
|
return {
|
|
'TaxesTotal': taxes,
|
|
'TaxesTotalBase': tax_total_base,
|
|
'WithholdingTaxesTotal': withholding_taxes}
|
|
|
|
def _get_invoice_lines(self):
|
|
msg1 = _("Your Unit of Measure: '%s', has no Unit of Measure Code, contact " +
|
|
"with your administrator.")
|
|
msg2 = _("The invoice line %s has no reference")
|
|
msg3 = _("Your product: '%s', has no reference price, contact with your " +
|
|
"administrator.")
|
|
msg4 = _("Your tax: '%s', has no e-invoicing tax group type, contact with " +
|
|
"your administrator.")
|
|
msg5 = _("Your product: '%s', cannot have two VAT type taxes.")
|
|
msg6 = _("Your withholding tax: '%s', has amount equal to zero (0), the " +
|
|
"withholding taxes must have amount different to zero (0), contact " +
|
|
"with your administrator.")
|
|
msg7 = _("Your tax: '%s', has negative amount or an amount equal to zero " +
|
|
"(0), the taxes must have an amount greater than zero (0), contact " +
|
|
"with your administrator.")
|
|
invoice_lines = {}
|
|
count = 1
|
|
exception = False
|
|
|
|
for invoice_line in self.invoice_line_ids.filtered(
|
|
lambda line: not line.display_type):
|
|
if not invoice_line.product_uom_id.product_uom_code_id:
|
|
raise UserError(msg1 % invoice_line.product_uom_id.name)
|
|
|
|
disc_amount = 0
|
|
total_wo_disc = 0
|
|
brand_name = False
|
|
model_name = invoice_line.product_id.default_code
|
|
|
|
if invoice_line.price_unit != 0 and invoice_line.quantity != 0:
|
|
total_wo_disc = invoice_line.price_unit * invoice_line.quantity
|
|
|
|
if total_wo_disc != 0 and invoice_line.discount != 0:
|
|
disc_amount = (total_wo_disc * invoice_line.discount) / 100
|
|
|
|
if not invoice_line.product_id or not invoice_line.product_id.default_code:
|
|
raise UserError(msg2 % invoice_line.name)
|
|
|
|
if invoice_line.price_subtotal <= 0 and invoice_line.reference_price <= 0:
|
|
raise UserError(msg3 % invoice_line.product_id.default_code)
|
|
|
|
if self.invoice_type_code == '02':
|
|
if invoice_line.product_id.product_brand_id:
|
|
brand_name = invoice_line.product_id.product_brand_id.name
|
|
|
|
if invoice_line.product_id.manufacturer_pref:
|
|
model_name = invoice_line.product_id.manufacturer_pref
|
|
|
|
invoice_lines[count] = {}
|
|
invoice_lines[count][
|
|
'unitCode'] = invoice_line.product_uom_id.product_uom_code_id.code
|
|
invoice_lines[count]['Quantity'] = '{:.2f}'.format(
|
|
invoice_line.quantity)
|
|
invoice_lines[count][
|
|
'PricingReferencePriceAmount'] = '{:.2f}'.format(
|
|
invoice_line.reference_price)
|
|
invoice_lines[count]['LineExtensionAmount'] = '{:.2f}'.format(
|
|
invoice_line.price_subtotal)
|
|
invoice_lines[count]['MultiplierFactorNumeric'] = '{:.2f}'.format(
|
|
invoice_line.discount)
|
|
invoice_lines[count]['AllowanceChargeAmount'] = '{:.2f}'.format(
|
|
disc_amount)
|
|
invoice_lines[count][
|
|
'AllowanceChargeBaseAmount'] = '{:.2f}'.format(total_wo_disc)
|
|
invoice_lines[count]['TaxesTotal'] = {}
|
|
invoice_lines[count]['WithholdingTaxesTotal'] = {}
|
|
invoice_lines[count][
|
|
'StandardItemIdentification'] = invoice_line.product_id.default_code
|
|
iva_count = 0
|
|
|
|
for tax in invoice_line.tax_ids:
|
|
if tax.amount_type == 'group':
|
|
tax_ids = tax.children_tax_ids
|
|
else:
|
|
tax_ids = tax
|
|
|
|
for tax_id in tax_ids:
|
|
if tax_id.tax_group_id.is_einvoicing:
|
|
if not tax_id.tax_group_id.tax_group_type_id:
|
|
raise UserError(msg4 % tax.name)
|
|
|
|
tax_type = tax_id.tax_group_id.tax_group_type_id.type
|
|
|
|
if tax_id.tax_group_id.tax_group_type_id.name == 'IVA':
|
|
iva_count += 1
|
|
|
|
if iva_count > 1:
|
|
dian_document_id = self.dian_document_ids.filtered(
|
|
lambda d: d.state != 'cancel')
|
|
exception = True
|
|
msg = msg5 % invoice_line.product_id.default_code
|
|
dian_document_id.write({'get_status_zip_response': msg})
|
|
|
|
if tax_type == 'withholding_tax' and tax_id.amount == 0:
|
|
raise UserError(msg6 % tax_id.name)
|
|
|
|
if tax_type == 'tax' and tax_id.amount <= 0:
|
|
raise UserError(msg7 % tax_id.name)
|
|
|
|
if tax_type == 'withholding_tax' and tax_id.amount > 0:
|
|
invoice_lines[count]['WithholdingTaxesTotal'] = (
|
|
invoice_line._get_invoice_lines_taxes(
|
|
tax_id,
|
|
tax_id.amount,
|
|
invoice_lines[count]['WithholdingTaxesTotal']))
|
|
elif tax_type == 'withholding_tax' and tax_id.amount < 0:
|
|
# TODO 3.0 Las retenciones se recomienda no enviarlas a la DIAN.
|
|
# Solo la parte positiva que indicaria una autoretencion, Si la DIAN
|
|
# pide que se envie la parte negativa, seria quitar o comentar este if
|
|
pass
|
|
else:
|
|
invoice_lines[count]['TaxesTotal'] = (
|
|
invoice_line._get_invoice_lines_taxes(
|
|
tax_id,
|
|
tax_id.amount,
|
|
invoice_lines[count]['TaxesTotal']))
|
|
|
|
if '01' not in invoice_lines[count]['TaxesTotal']:
|
|
invoice_lines[count]['TaxesTotal']['01'] = {}
|
|
invoice_lines[count]['TaxesTotal']['01']['total'] = 0
|
|
invoice_lines[count]['TaxesTotal']['01']['name'] = 'IVA'
|
|
invoice_lines[count]['TaxesTotal']['01']['taxes'] = {}
|
|
invoice_lines[count]['TaxesTotal']['01']['taxes']['0.00'] = {}
|
|
invoice_lines[count]['TaxesTotal']['01']['taxes']['0.00'][
|
|
'base'] = invoice_line.price_subtotal
|
|
invoice_lines[count]['TaxesTotal']['01']['taxes']['0.00'][
|
|
'amount'] = 0
|
|
|
|
if '04' not in invoice_lines[count]['TaxesTotal']:
|
|
invoice_lines[count]['TaxesTotal']['04'] = {}
|
|
invoice_lines[count]['TaxesTotal']['04']['total'] = 0
|
|
invoice_lines[count]['TaxesTotal']['04']['name'] = 'ICA'
|
|
invoice_lines[count]['TaxesTotal']['04']['taxes'] = {}
|
|
invoice_lines[count]['TaxesTotal']['04']['taxes']['0.00'] = {}
|
|
invoice_lines[count]['TaxesTotal']['04']['taxes']['0.00'][
|
|
'base'] = invoice_line.price_subtotal
|
|
invoice_lines[count]['TaxesTotal']['04']['taxes']['0.00'][
|
|
'amount'] = 0
|
|
|
|
if '03' not in invoice_lines[count]['TaxesTotal']:
|
|
invoice_lines[count]['TaxesTotal']['03'] = {}
|
|
invoice_lines[count]['TaxesTotal']['03']['total'] = 0
|
|
invoice_lines[count]['TaxesTotal']['03']['name'] = 'INC'
|
|
invoice_lines[count]['TaxesTotal']['03']['taxes'] = {}
|
|
invoice_lines[count]['TaxesTotal']['03']['taxes']['0.00'] = {}
|
|
invoice_lines[count]['TaxesTotal']['03']['taxes']['0.00'][
|
|
'base'] = invoice_line.price_subtotal
|
|
invoice_lines[count]['TaxesTotal']['03']['taxes']['0.00'][
|
|
'amount'] = 0
|
|
|
|
invoice_lines[count]['BrandName'] = brand_name
|
|
invoice_lines[count]['ModelName'] = model_name
|
|
invoice_lines[count]['ItemDescription'] = invoice_line.name
|
|
invoice_lines[count]['InformationContentProviderParty'] = (
|
|
invoice_line._get_information_content_provider_party_values())
|
|
invoice_lines[count]['PriceAmount'] = '{:.2f}'.format(
|
|
invoice_line.price_unit)
|
|
|
|
count += 1
|
|
|
|
if exception:
|
|
self.write({'dian_document_state': 'exception'})
|
|
else:
|
|
self.write({'dian_document_state': 'pending'})
|
|
|
|
return invoice_lines
|
|
|
|
def _set_invoice_lines_price_reference(self):
|
|
for invoice_line in self.invoice_line_ids.filtered(
|
|
lambda line: not line.display_type):
|
|
percentage = 100
|
|
margin_percentage = invoice_line.product_id.margin_percentage
|
|
|
|
if invoice_line.product_id.reference_price > 0:
|
|
reference_price = invoice_line.product_id.reference_price
|
|
elif 0 < margin_percentage < 100:
|
|
percentage = (percentage - margin_percentage) / 100
|
|
reference_price = invoice_line.product_id.standard_price / percentage
|
|
else:
|
|
reference_price = 0
|
|
|
|
invoice_line.write({
|
|
'cost_price': invoice_line.product_id.standard_price,
|
|
'reference_price': reference_price
|
|
})
|
|
|
|
return True
|
|
|
|
def update(self, values):
|
|
res = super(AccountMove, self).update(values)
|
|
|
|
for invoice_id in self:
|
|
if values.get('refund_type') == "credit":
|
|
invoice_id.operation_type = '20'
|
|
elif values.get('refund_type') == "debit":
|
|
invoice_id.operation_type = '30'
|
|
|
|
return res
|
|
|
|
def action_set_dian_document(self):
|
|
msg = _("The 'delivery date' must be equal or greater per maximum 10 days to "
|
|
"the 'invoice date'.")
|
|
timezone = pytz.timezone(self.env.user.tz or 'America/Bogota')
|
|
from_zone = tz.gettz('UTC')
|
|
to_zone = tz.gettz(timezone.zone)
|
|
|
|
for invoice_id in self:
|
|
if not invoice_id.company_id.einvoicing_enabled:
|
|
return True
|
|
|
|
if invoice_id.journal_id.sequence_id.dian_type not in DIAN_TYPES:
|
|
return True
|
|
|
|
if invoice_id.dian_document_ids.filtered(lambda d: d.state != 'cancel'):
|
|
return True
|
|
|
|
if not invoice_id.invoice_datetime:
|
|
invoice_datetime = datetime.strptime(
|
|
str(invoice_id.invoice_date) + ' 13:00:00',
|
|
'%Y-%m-%d %H:%M:%S').replace(tzinfo=from_zone)
|
|
invoice_datetime = invoice_datetime.astimezone(
|
|
to_zone).strftime('%Y-%m-%d %H:%M:%S')
|
|
invoice_id.invoice_datetime = invoice_datetime
|
|
|
|
if (invoice_id.company_id.automatic_delivery_datetime
|
|
and not invoice_id.delivery_datetime):
|
|
invoice_datetime = invoice_id.invoice_datetime
|
|
hours_added = timedelta(
|
|
hours=invoice_id.company_id.additional_hours_delivery_datetime)
|
|
invoice_id.delivery_datetime = invoice_datetime + hours_added
|
|
|
|
if not invoice_id.delivery_datetime:
|
|
raise UserError(msg)
|
|
|
|
invoice_date = invoice_id.invoice_date
|
|
delivery_date = datetime.strftime(invoice_id.delivery_datetime, '%Y-%m-%d')
|
|
delivery_date = datetime.strptime(delivery_date, '%Y-%m-%d').date()
|
|
days = (delivery_date - invoice_date).days
|
|
|
|
if days < 0 or days > 10:
|
|
raise UserError(msg)
|
|
|
|
invoice_id._set_invoice_lines_price_reference()
|
|
xml_filename = False
|
|
zipped_filename = False
|
|
ar_xml_filename = False
|
|
ad_zipped_filename = False
|
|
|
|
for dian_document_id in invoice_id.dian_document_ids:
|
|
xml_filename = dian_document_id.xml_filename
|
|
zipped_filename = dian_document_id.zipped_filename
|
|
ar_xml_filename = dian_document_id.ar_xml_filename
|
|
ad_zipped_filename = dian_document_id.ad_zipped_filename
|
|
break
|
|
|
|
dian_document_id = self.env['account.move.dian.document'].create({
|
|
'invoice_id': invoice_id.id,
|
|
'company_id': invoice_id.company_id.id,
|
|
'xml_filename': xml_filename,
|
|
'zipped_filename': zipped_filename,
|
|
'ar_xml_filename': ar_xml_filename,
|
|
'ad_zipped_filename': ad_zipped_filename})
|
|
set_files = dian_document_id.action_set_files()
|
|
|
|
if (not invoice_id.send_invoice_to_dian
|
|
and invoice_id.company_id.send_invoice_to_dian):
|
|
invoice_id.send_invoice_to_dian = invoice_id.company_id.send_invoice_to_dian
|
|
|
|
if (not invoice_id.invoice_type_code
|
|
and invoice_id.company_id.invoice_type_code):
|
|
invoice_id.invoice_type_code = invoice_id.company_id.invoice_type_code
|
|
|
|
if invoice_id.send_invoice_to_dian == '0':
|
|
if set_files:
|
|
if invoice_id.invoice_type_code in ('01', '02', '05'):
|
|
if dian_document_id.zip_key:
|
|
to_return = dian_document_id.action_GetStatusZip()
|
|
else:
|
|
to_return = dian_document_id._get_GetStatus(True)
|
|
|
|
if not to_return:
|
|
dian_document_id.action_send_zipped_file()
|
|
elif invoice_id.invoice_type_code == '04':
|
|
dian_document_id.action_send_mail()
|
|
else:
|
|
dian_document_id.send_failure_mail()
|
|
|
|
return True
|
|
|
|
def action_process_dian_document(self):
|
|
for invoice_id in self:
|
|
dian_document_id = invoice_id.dian_document_ids.filtered(
|
|
lambda d: d.state not in ('cancel', 'done'))
|
|
|
|
if dian_document_id:
|
|
dian_document_id.action_process()
|
|
|
|
return True
|
|
|
|
def action_merge_with_sale_order(self):
|
|
for invoice_id in self:
|
|
sale_order_id = invoice_id.sale_order_id
|
|
|
|
if sale_order_id:
|
|
move_id = sale_order_id._create_invoices()
|
|
move_line_data = []
|
|
|
|
for invoice_line_id in invoice_id.invoice_line_ids:
|
|
move_line_data.append({
|
|
'move_id': move_id.id,
|
|
'currency_id': invoice_line_id.currency_id.id,
|
|
'product_id': invoice_line_id.product_id.id,
|
|
'name': invoice_line_id.name,
|
|
'quantity': invoice_line_id.quantity,
|
|
'product_uom_id': invoice_line_id.product_uom_id.id,
|
|
'price_unit': invoice_line_id.price_unit,
|
|
'discount': invoice_line_id.discount,
|
|
'tax_ids': invoice_line_id.tax_ids,
|
|
'price_subtotal': invoice_line_id.price_subtotal,
|
|
'price_total': invoice_line_id.price_total,
|
|
'cost_price': invoice_line_id.cost_price,
|
|
'reference_price': invoice_line_id.reference_price})
|
|
|
|
self.env['account.move.summary.line'].create(move_line_data)
|
|
move_id.write({
|
|
'name': invoice_id.name,
|
|
'move_name': invoice_id.move_name,
|
|
'invoice_date': invoice_id.invoice_date,
|
|
'summary_amount_untaxed': invoice_id.amount_untaxed,
|
|
'summary_amount_tax': invoice_id.amount_tax,
|
|
'summary_amount_total': invoice_id.amount_total})
|
|
mail_message_ids = self.env['mail.message'].search(
|
|
[('model', '=', 'account.move'), ('res_id', '=', invoice_id.id)])
|
|
|
|
if mail_message_ids:
|
|
mail_message_ids.write({'res_id': move_id.id})
|
|
|
|
if invoice_id.dian_document_ids:
|
|
invoice_id.dian_document_ids.write({'invoice_id': move_id.id})
|
|
move_id.write({
|
|
'invoice_datetime': invoice_id.invoice_datetime,
|
|
'delivery_datetime': invoice_id.delivery_datetime,
|
|
'operation_type': invoice_id.operation_type,
|
|
'invoice_type_code': invoice_id.invoice_type_code,
|
|
'send_invoice_to_dian': invoice_id.send_invoice_to_dian,
|
|
'access_token': invoice_id.access_token,
|
|
'is_accepted_rejected': invoice_id.is_accepted_rejected,
|
|
'accepted_rejected_datetime': invoice_id.accepted_rejected_datetime,
|
|
'receipt_document_reference': invoice_id.receipt_document_reference,
|
|
'dian_document_state': invoice_id.dian_document_state,
|
|
'send_invoice_to_dian': invoice_id.send_invoice_to_dian})
|
|
|
|
if not invoice_id.dian_document_ids.filtered(lambda d: d.state == 'done'):
|
|
move_id.write({'merge_with_sale_order': True})
|
|
|
|
invoice_id.write({'move_name': False})
|
|
invoice_id.with_context(force_delete=True).unlink()
|
|
|
|
return sale_order_id.action_view_invoice()
|
|
|
|
return True
|
|
|
|
def action_post(self):
|
|
msg = _('Invoice totals do not match summary totals.')
|
|
res = super(AccountMove, self).action_post()
|
|
|
|
for invoice_id in self:
|
|
invoice_id.action_set_dian_document()
|
|
|
|
if invoice_id.merge_with_sale_order:
|
|
if not invoice_id.summary_line_ids:
|
|
invoice_id.button_draft()
|
|
else:
|
|
if (invoice_id.summary_amount_untaxed != invoice_id.amount_untaxed
|
|
or invoice_id.summary_amount_tax != invoice_id.amount_tax
|
|
or invoice_id.summary_amount_total != invoice_id.amount_total):
|
|
raise UserError(msg)
|
|
|
|
return res
|
|
|
|
def button_draft(self):
|
|
msg = _('You cannot cancel a invoice sent to the DIAN and that was approved.')
|
|
user = self.env['res.users'].search([('id', '=', self.env.user.id)])
|
|
allow_cancel_invoice_dian_document_done = False
|
|
|
|
if user.has_group(
|
|
'l10n_co_account_e_invoicing.group_allow_cancel_invoice_dian_document_done'):
|
|
allow_cancel_invoice_dian_document_done = True
|
|
|
|
for invoice_id in self:
|
|
if invoice_id.merge_with_sale_order and not invoice_id.summary_line_ids:
|
|
continue
|
|
|
|
for dian_document_id in invoice_id.dian_document_ids:
|
|
if dian_document_id.state == 'done':
|
|
if not allow_cancel_invoice_dian_document_done:
|
|
raise UserError(msg)
|
|
else:
|
|
dian_document_id.state = 'cancel'
|
|
self.write({'dian_document_state': 'pending'})
|
|
|
|
return super(AccountMove, self).button_draft()
|