forked from Mapan/odoo17e
880 lines
42 KiB
Python
880 lines
42 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from functools import partial
|
|
|
|
from odoo import _, api, fields, models
|
|
from odoo.tools import frozendict, float_round
|
|
from odoo.tools.misc import formatLang, format_date
|
|
from odoo.exceptions import ValidationError
|
|
|
|
from odoo.addons.l10n_ec_edi.models.account_tax import L10N_EC_TAXSUPPORTS
|
|
|
|
L10N_EC_VAT_RATES = {
|
|
5: 5.0,
|
|
2: 12.0,
|
|
10: 13.0,
|
|
3: 14.0,
|
|
4: 15.0,
|
|
0: 0.0,
|
|
6: 0.0,
|
|
7: 0.0,
|
|
8: 8.0,
|
|
}
|
|
L10N_EC_VAT_SUBTAXES = {
|
|
'vat05': 5,
|
|
'vat08': 8,
|
|
'vat12': 2,
|
|
'vat13': 10,
|
|
'vat14': 3,
|
|
'vat15': 4,
|
|
'zero_vat': 0,
|
|
'not_charged_vat': 6,
|
|
'exempt_vat': 7,
|
|
} # NOTE: non-IVA cases such as ICE and IRBPNR not supported
|
|
L10N_EC_VAT_TAX_NOT_ZERO_GROUPS = (
|
|
'vat05',
|
|
'vat08',
|
|
'vat12',
|
|
'vat13',
|
|
'vat14',
|
|
'vat15',
|
|
)
|
|
L10N_EC_VAT_TAX_ZERO_GROUPS = (
|
|
'zero_vat',
|
|
'not_charged_vat',
|
|
'exempt_vat',
|
|
)
|
|
L10N_EC_VAT_TAX_GROUPS = tuple(L10N_EC_VAT_TAX_NOT_ZERO_GROUPS + L10N_EC_VAT_TAX_ZERO_GROUPS) # all VAT taxes
|
|
L10N_EC_WITHHOLD_CODES = {
|
|
'withhold_vat_purchase': 2,
|
|
'withhold_income_purchase': 1,
|
|
}
|
|
L10N_EC_WITHHOLD_VAT_CODES = {
|
|
0.0: 7, # 0% vat withhold
|
|
10.0: 9, # 10% vat withhold
|
|
20.0: 10, # 20% vat withhold
|
|
30.0: 1, # 30% vat withhold
|
|
50.0: 11, # 50% vat withhold
|
|
70.0: 2, # 70% vat withhold
|
|
100.0: 3, # 100% vat withhold
|
|
} # NOTE: non-IVA cases such as ICE and IRBPNR not supported
|
|
# Codes from tax report "Form 103", useful for withhold automation:
|
|
L10N_EC_WTH_FOREIGN_GENERAL_REGIME_CODES = ['402', '403', '404', '405', '406', '407', '408', '409', '410', '411', '412', '413', '414', '415', '416', '417', '418', '419', '420', '421', '422', '423']
|
|
L10N_EC_WTH_FOREIGN_TAX_HAVEN_OR_LOWER_TAX_CODES = ['424', '425', '426', '427', '428', '429', '430', '431', '432', '433']
|
|
L10N_EC_WTH_FOREIGN_NOT_SUBJECT_WITHHOLD_CODES = ['412', '423', '433']
|
|
L10N_EC_WTH_FOREIGN_SUBJECT_WITHHOLD_CODES = list(set(L10N_EC_WTH_FOREIGN_GENERAL_REGIME_CODES) - set(L10N_EC_WTH_FOREIGN_NOT_SUBJECT_WITHHOLD_CODES))
|
|
L10N_EC_WTH_FOREIGN_DOUBLE_TAXATION_CODES = ['402', '403', '404', '405', '406', '407', '408', '409', '410', '411', '412']
|
|
L10N_EC_WITHHOLD_FOREIGN_REGIME = [('01', '(01) General Regime'), ('02', '(02) Fiscal Paradise'), ('03', '(03) Preferential Tax Regime')]
|
|
|
|
|
|
class AccountMove(models.Model):
|
|
_inherit = "account.move"
|
|
|
|
# ===== Authorization number show/edit fields =====
|
|
l10n_show_ec_authorization = fields.Boolean(
|
|
string='Show Authorization',
|
|
compute='_compute_l10n_ec_show_edit_authorization',
|
|
help='Technical field to show "Authorization number" field in the move form.',
|
|
)
|
|
l10n_edit_ec_authorization = fields.Boolean(
|
|
string='Edit Authorization',
|
|
compute='_compute_l10n_ec_show_edit_authorization',
|
|
help='Technical field to edit "Authorization number" field in the move form.',
|
|
)
|
|
|
|
# ===== EDI fields =====
|
|
l10n_ec_authorization_number = fields.Char(
|
|
string="Authorization number",
|
|
size=49,
|
|
copy=False, index=True,
|
|
tracking=True,
|
|
help="Ecuador: EDI authorization number (same as access key), set upon posting",
|
|
)
|
|
l10n_ec_authorization_date = fields.Datetime(
|
|
string="Authorization date",
|
|
copy=False, readonly=True, tracking=True,
|
|
help="Ecuador: Date on which government authorizes the document, unset if document is cancelled.",
|
|
)
|
|
l10n_latam_internal_type = fields.Selection(related='l10n_latam_document_type_id.internal_type')
|
|
|
|
# ===== WITHHOLD fields =====
|
|
l10n_ec_withhold_type = fields.Selection(related='journal_id.l10n_ec_withhold_type')
|
|
l10n_ec_withhold_date = fields.Date(
|
|
string="Withhold Date",
|
|
readonly=True,
|
|
copy=False,
|
|
)
|
|
# Technical field to show/hide "ADD WITHHOLD" button
|
|
l10n_ec_show_add_withhold = fields.Boolean(
|
|
compute='_compute_l10n_ec_show_add_withhold',
|
|
string="Allow Withhold",
|
|
)
|
|
l10n_ec_withhold_line_ids = fields.Many2many(
|
|
comodel_name='account.move.line',
|
|
string="Withhold Lines",
|
|
copy=False,
|
|
compute='_compute_l10n_ec_withhold_wth_fields',
|
|
help="The withholding lines in a withhold",
|
|
)
|
|
l10n_ec_related_withhold_line_ids = fields.One2many(
|
|
comodel_name='account.move.line',
|
|
inverse_name='l10n_ec_withhold_invoice_id',
|
|
string="Related withhold lines",
|
|
help="The withhold lines related to this invoice",
|
|
)
|
|
# Technical field for the number of invoices linked to a withhold
|
|
l10n_ec_withhold_origin_invoice_count = fields.Integer(
|
|
compute='_compute_l10n_ec_withhold_wth_fields',
|
|
string="Invoices Count",
|
|
)
|
|
l10n_ec_withhold_ids = fields.Many2many(
|
|
comodel_name='account.move',
|
|
compute='_compute_l10n_ec_withhold_inv_fields',
|
|
string="Withholds",
|
|
help="The withholds related to this invoice",
|
|
)
|
|
# Technical field for the number of invoices linked to this withhold
|
|
l10n_ec_withhold_count = fields.Integer(
|
|
compute='_compute_l10n_ec_withhold_inv_fields',
|
|
string="Withholds Count",
|
|
)
|
|
# Sales/Purchases subtotals, and total widget
|
|
l10n_ec_withhold_subtotals = fields.Json(compute='_compute_l10n_ec_withhold_subtotals')
|
|
l10n_ec_withhold_foreign_regime = fields.Selection(
|
|
selection=L10N_EC_WITHHOLD_FOREIGN_REGIME,
|
|
string="Foreign Fiscal Regime",
|
|
)
|
|
|
|
# ===== COMPUTE / ONCHANGE / CONSTRAINTS METHODS =====
|
|
|
|
@api.depends('l10n_latam_document_type_id', 'l10n_ec_authorization_number', 'journal_id')
|
|
def _compute_l10n_ec_show_edit_authorization(self):
|
|
not_ec_moves = self.filtered(lambda m: m.country_code != 'EC' or not m.l10n_latam_document_type_id) # Not EC documents and documents without a document type
|
|
not_ec_moves = not_ec_moves.filtered(lambda m: not m._l10n_ec_is_withholding()) # Not EC withholds
|
|
not_ec_moves.l10n_show_ec_authorization = False
|
|
not_ec_moves.l10n_edit_ec_authorization = False
|
|
ec_moves = (self - not_ec_moves)
|
|
for move in ec_moves:
|
|
# Always show and edit the authorization number for EC documents
|
|
l10n_show_ec_authorization = True
|
|
l10n_edit_ec_authorization = True
|
|
allow_electronic_document = move.journal_id.type == 'sale' or move.journal_id.l10n_ec_is_purchase_liquidation or move.journal_id.l10n_ec_withhold_type == 'in_withhold'
|
|
if allow_electronic_document and (any(x.code == 'ecuadorian_edi' for x in move.journal_id.edi_format_ids) or move.edi_document_ids):
|
|
# Show authorization number when filled out
|
|
# Do not edit the authorization number when the journal allow electronic invoicing or have an EDI document
|
|
l10n_show_ec_authorization = bool(move.l10n_ec_authorization_number)
|
|
l10n_edit_ec_authorization = False
|
|
elif move.journal_id.l10n_ec_withhold_type == 'out_withhold' or\
|
|
(move.journal_id.type == 'purchase' and move._is_manual_document_number()):
|
|
# Edit and show the autorization number when:
|
|
# - The document it's an out withhold
|
|
# - It's a document with manual authorization
|
|
l10n_edit_ec_authorization = True
|
|
move.l10n_show_ec_authorization = l10n_show_ec_authorization
|
|
move.l10n_edit_ec_authorization = l10n_edit_ec_authorization
|
|
|
|
@api.depends('country_code', 'l10n_latam_document_type_id.code', 'l10n_ec_withhold_ids')
|
|
def _compute_l10n_ec_show_add_withhold(self):
|
|
# shows/hide "ADD WITHHOLD" button on invoices
|
|
invoices_ec = self.filtered(lambda inv: inv.country_code == 'EC')
|
|
(self - invoices_ec).l10n_ec_show_add_withhold = False
|
|
for invoice in invoices_ec:
|
|
codes_to_withhold = [
|
|
'01', # Factura compra
|
|
'02', # Nota de venta
|
|
'03', # Liquidacion compra
|
|
'05', # Nota de Débito
|
|
'08', # Entradas a espectaculos
|
|
'09', # Tiquetes
|
|
'11', # Pasajes
|
|
'12', # Inst FInancieras
|
|
'20', # Estado
|
|
'21', # Carta porte aereo
|
|
'47', # Nota de crédito de reembolso
|
|
'48', # Nota de débito de reembolso
|
|
]
|
|
add_withhold = invoice.country_code == 'EC' and invoice.l10n_latam_document_type_id.code in codes_to_withhold
|
|
invoice.l10n_ec_show_add_withhold = add_withhold
|
|
|
|
@api.depends('country_code', 'l10n_ec_withhold_type', 'line_ids.tax_ids', 'line_ids.l10n_ec_withhold_invoice_id')
|
|
def _compute_l10n_ec_withhold_wth_fields(self):
|
|
withholds_ec = self.filtered(lambda wth: wth.country_code == 'EC')
|
|
withholds_not_ec = (self - withholds_ec)
|
|
withholds_not_ec.l10n_ec_withhold_line_ids = False
|
|
withholds_not_ec.l10n_ec_withhold_origin_invoice_count = False
|
|
for withhold in withholds_ec:
|
|
if withhold._l10n_ec_is_withholding(): # fields related to a withhold entry
|
|
withhold.l10n_ec_withhold_line_ids = withhold.line_ids.filtered(lambda l: l.tax_ids)
|
|
withhold.l10n_ec_withhold_origin_invoice_count = len(withhold.line_ids.mapped('l10n_ec_withhold_invoice_id'))
|
|
else:
|
|
withhold.l10n_ec_withhold_line_ids = False
|
|
withhold.l10n_ec_withhold_origin_invoice_count = False
|
|
|
|
@api.depends('country_code', 'line_ids.tax_ids.tax_group_id', 'line_ids.l10n_ec_withhold_tax_amount', 'line_ids.balance')
|
|
def _compute_l10n_ec_withhold_subtotals(self):
|
|
def line_dict(withhold_line):
|
|
return {
|
|
'tax_group': withhold_line.tax_ids.tax_group_id,
|
|
'amount': withhold_line.l10n_ec_withhold_tax_amount,
|
|
'base': abs(withhold_line.balance),
|
|
}
|
|
|
|
moves_ec = self.filtered(lambda move: move.country_code == 'EC')
|
|
(self - moves_ec).l10n_ec_withhold_subtotals = False
|
|
for move in moves_ec:
|
|
if move.l10n_ec_withhold_type:
|
|
lines = move.l10n_ec_withhold_line_ids.mapped(line_dict)
|
|
move.l10n_ec_withhold_subtotals = self._l10n_ec_withhold_subtotals_dict(move.currency_id, lines)
|
|
else:
|
|
move.l10n_ec_withhold_subtotals = {}
|
|
|
|
@api.depends('country_code', 'line_ids')
|
|
def _compute_l10n_ec_withhold_inv_fields(self):
|
|
invoices_ec = self.filtered(lambda inv: inv.country_code == 'EC')
|
|
invoices_not_ec = (self - invoices_ec)
|
|
invoices_not_ec.l10n_ec_withhold_ids = False
|
|
invoices_not_ec.l10n_ec_withhold_count = False
|
|
for invoice in invoices_ec:
|
|
withhold_ids = False
|
|
withhold_count = False
|
|
if invoice.is_invoice():
|
|
withhold_ids = self.env['account.move.line'].search([('l10n_ec_withhold_invoice_id', '=', invoice.id)]).mapped('move_id')
|
|
withhold_count = len(withhold_ids)
|
|
invoice.l10n_ec_withhold_ids = withhold_ids
|
|
invoice.l10n_ec_withhold_count = withhold_count
|
|
|
|
@api.onchange('l10n_latam_document_type_id', 'l10n_latam_document_number', 'partner_id')
|
|
def _inverse_l10n_latam_document_number(self):
|
|
super()._inverse_l10n_latam_document_number()
|
|
self._l10n_ec_check_number_prefix()
|
|
|
|
def write(self, vals):
|
|
out_withholds = self.filtered(lambda m: m.l10n_ec_withhold_type == 'out_withhold' and m.country_code == 'EC')
|
|
out_withholds._l10_ec_check_tax_lock_date(vals)
|
|
return super().write(vals)
|
|
|
|
def _l10_ec_check_tax_lock_date(self, vals):
|
|
# Check the locks date for changes in ref field values
|
|
for move in self:
|
|
lock_date = move.company_id._get_user_fiscal_lock_date()
|
|
if 'ref' in vals and lock_date and move.date <= lock_date:
|
|
raise ValidationError(_("The operation is refused as it would impact an already issued tax statement. "
|
|
"Please change the journal entry date or check the fiscal lock date (%s) to proceed."),
|
|
format_date(self.env, lock_date))
|
|
|
|
# ===== BUTTONS =====
|
|
|
|
def l10n_ec_add_withhold(self):
|
|
# Launches the withholds wizard linked to selected invoices
|
|
return {
|
|
'name': _("Withhold"),
|
|
'view_type': 'form',
|
|
'view_mode': 'form',
|
|
'view_id': False,
|
|
'res_model': 'l10n_ec.wizard.account.withhold',
|
|
'context': {'active_ids': self.ids, 'active_model': 'account.move'},
|
|
'type': 'ir.actions.act_window',
|
|
'target': 'new',
|
|
}
|
|
|
|
def l10n_ec_action_view_withholds(self):
|
|
# Navigate from the invoice to its withholds
|
|
withhold_ids = self.l10n_ec_withhold_ids.ids
|
|
if len(withhold_ids) == 1:
|
|
return {
|
|
'name': _('Withholding'),
|
|
'view_type': 'form',
|
|
'view_mode': 'form',
|
|
'view_id': False,
|
|
'res_model': 'account.move',
|
|
'type': 'ir.actions.act_window',
|
|
'res_id': withhold_ids[0],
|
|
}
|
|
else:
|
|
action = self.env["ir.actions.actions"]._for_xml_id('account.action_move_journal_line')
|
|
action['name'] = _("Withholds")
|
|
action['domain'] = f"[('id', 'in', {withhold_ids!r})]"
|
|
return action
|
|
|
|
def l10n_ec_action_view_invoices(self):
|
|
# Navigate from the withhold to its invoices
|
|
l10n_ec_withhold_origin_ids = self.line_ids.mapped('l10n_ec_withhold_invoice_id').ids
|
|
if len(l10n_ec_withhold_origin_ids) == 1:
|
|
return {
|
|
'name': _("Invoices"),
|
|
'view_type': 'form',
|
|
'view_mode': 'form',
|
|
'view_id': False,
|
|
'res_model': 'account.move',
|
|
'type': 'ir.actions.act_window',
|
|
'res_id': l10n_ec_withhold_origin_ids[0],
|
|
}
|
|
else:
|
|
action_ref = 'account.action_move_out_invoice_type'
|
|
if self.l10n_ec_withhold_type == 'in_withhold':
|
|
action_ref = 'account.action_move_in_invoice_type'
|
|
action = self.env["ir.actions.actions"]._for_xml_id(action_ref)
|
|
action['name'] = _('Invoices')
|
|
action['domain'] = f"[('id', 'in', {l10n_ec_withhold_origin_ids!r})]"
|
|
return action
|
|
|
|
def l10n_ec_action_send_withhold(self):
|
|
self.ensure_one()
|
|
template = self.env.ref('l10n_ec_edi.email_template_edi_withhold')
|
|
compose_form = self.env.ref('mail.email_compose_message_wizard_form')
|
|
ctx = {
|
|
**self.env.context,
|
|
'default_model': 'account.move',
|
|
'default_res_ids': self.ids,
|
|
'default_template_id': template.id,
|
|
'default_composition_mode': 'comment',
|
|
'default_email_layout_xmlid': 'mail.mail_notification_layout_with_responsible_signature',
|
|
'force_email': True,
|
|
}
|
|
|
|
return {
|
|
'name': _('Compose Email'),
|
|
'type': 'ir.actions.act_window',
|
|
'res_model': 'mail.compose.message',
|
|
'views': [(compose_form.id, 'form')],
|
|
'target': 'new',
|
|
'context': ctx,
|
|
}
|
|
|
|
# ===== OVERRIDES PORTAL WITHHOLD AND PURCHASE LIQUIDATION =====
|
|
|
|
def _compute_amount(self):
|
|
# EXTENDS account to properly compute withhold subtotals to be shown in tree view, email template, etc.
|
|
withholds = self.filtered(lambda x: x._l10n_ec_is_withholding())
|
|
withholds._l10n_ec_wth_calculate_amount()
|
|
other_moves = self - withholds
|
|
super(AccountMove, other_moves)._compute_amount()
|
|
|
|
def _l10n_ec_wth_calculate_amount(self):
|
|
# Sister method to _compute_amount(), withhold subtotals behaves just like payment subtotals
|
|
# Could be computed from the payable lines but also from the withhold lines, we take the second approach
|
|
for withhold in self:
|
|
total_untaxed, total_untaxed_currency = 0.0, 0.0
|
|
total_tax, total_tax_currency = 0.0, 0.0
|
|
total_residual, total_residual_currency = 0.0, 0.0
|
|
total, total_currency = 0.0, 0.0
|
|
|
|
for line in withhold.l10n_ec_withhold_line_ids:
|
|
# === Withhold journal entry ===
|
|
total += line.l10n_ec_withhold_tax_amount
|
|
total_currency += line.l10n_ec_withhold_tax_amount
|
|
|
|
sign = withhold.direction_sign
|
|
withhold.amount_untaxed = sign * total_untaxed_currency
|
|
withhold.amount_tax = sign * total_tax_currency
|
|
withhold.amount_total = sign * total_currency
|
|
withhold.amount_residual = -sign * total_residual_currency
|
|
withhold.amount_untaxed_signed = -total_untaxed
|
|
withhold.amount_tax_signed = -total_tax
|
|
withhold.amount_total_signed = abs(total) if withhold.move_type == 'entry' else -total
|
|
withhold.amount_residual_signed = total_residual
|
|
withhold.amount_total_in_currency_signed = abs(withhold.amount_total) if withhold.move_type == 'entry' else -(sign * withhold.amount_total)
|
|
|
|
def _get_name_invoice_report(self):
|
|
# EXTENDS account_move
|
|
self.ensure_one()
|
|
doc_type_code = self.l10n_latam_document_type_id.code
|
|
if self.l10n_latam_use_documents and self.country_code == 'EC':
|
|
if (self.move_type in ('out_invoice', 'out_refund') and doc_type_code in ['01', '04', '05', '41']) \
|
|
or (self.move_type in ('in_invoice') and doc_type_code in ['03', '41']):
|
|
return 'l10n_ec_edi.report_invoice_document'
|
|
return super(AccountMove, self)._get_name_invoice_report()
|
|
|
|
def _is_manual_document_number(self):
|
|
# EXTEND l10n_latam_invoice_document to exclude purchase liquidations and include sales withhold
|
|
self.ensure_one()
|
|
if self.journal_id.company_id.account_fiscal_country_id.code == 'EC':
|
|
if self.journal_id.l10n_ec_is_purchase_liquidation:
|
|
return False
|
|
return super()._is_manual_document_number()
|
|
|
|
def _get_l10n_latam_documents_domain(self):
|
|
# EXTENDS l10n_ec
|
|
if not self.journal_id.l10n_ec_is_purchase_liquidation:
|
|
return super()._get_l10n_latam_documents_domain()
|
|
return [('country_id.code', '=', 'EC'), ('internal_type', '=', 'purchase_liquidation')]
|
|
|
|
def _l10n_ec_check_number_prefix(self):
|
|
# validates that entity and emission point matches the ones configured in the journal
|
|
to_review = self.filtered(lambda x: x.journal_id.l10n_ec_require_emission and x.l10n_latam_document_type_id
|
|
and x.l10n_latam_document_number and (x.l10n_latam_manual_document_number or not x.highest_name))
|
|
for move in to_review:
|
|
prefix = move.journal_id.l10n_ec_entity + '-' + move.journal_id.l10n_ec_emission
|
|
number = move.l10n_latam_document_type_id._format_document_number(move.l10n_latam_document_number)
|
|
if prefix != number[:7]:
|
|
raise ValidationError(_('Check the document number "%s", the expected prefix is "%s".', self.l10n_latam_document_number, prefix))
|
|
|
|
def _l10n_ec_check_in_withhold_number_prefix(self):
|
|
# Check the document number only for in withholds
|
|
in_withholds = self.filtered(lambda move: move.journal_id.l10n_ec_withhold_type == 'in_withhold')
|
|
for withhold in in_withholds:
|
|
prefix = 'Ret ' + withhold.journal_id.l10n_ec_entity + '-' + withhold.journal_id.l10n_ec_emission + '-' # The prefix "Ret" is fixed in code
|
|
number = withhold.sequence_prefix
|
|
if prefix != number:
|
|
raise ValidationError(_('Check the document number "%s", the expected prefix is "%s".', number, prefix))
|
|
|
|
# ===== INVOICE XML GENERATION=====
|
|
|
|
def _l10n_ec_get_payment_data(self):
|
|
""" Get payment data for the XML. """
|
|
payment_data = []
|
|
pay_term_line_ids = self.line_ids.filtered(
|
|
lambda line: line.account_id.account_type in ('asset_receivable', 'liability_payable')
|
|
)
|
|
for line in pay_term_line_ids:
|
|
payment_vals = {
|
|
'payment_code': self.l10n_ec_sri_payment_id.code,
|
|
'payment_total': abs(line.balance),
|
|
}
|
|
payment_vals.update({
|
|
'payment_term': max(((line.date_maturity or self.invoice_date) - self.invoice_date).days, 0),
|
|
'time_unit': "dias",
|
|
})
|
|
payment_data.append(payment_vals)
|
|
return payment_data
|
|
|
|
def _l10n_ec_get_invoice_additional_info(self):
|
|
return {
|
|
"Referencia": self.name, # Reference
|
|
"Vendedor": self.invoice_user_id.name or '', # Salesperson
|
|
"E-mail": self.invoice_user_id.email or '',
|
|
}
|
|
|
|
def _l10n_ec_get_taxes_grouped(self, extra_group='tax_group'):
|
|
self.ensure_one()
|
|
|
|
def group_by(base_line, tax_values):
|
|
tax_id = tax_values['tax_repartition_line'].tax_id
|
|
code_percentage = L10N_EC_VAT_SUBTAXES[tax_id.tax_group_id.l10n_ec_type]
|
|
values = {
|
|
'code': self._l10n_ec_map_tax_groups(tax_id),
|
|
'code_percentage': code_percentage,
|
|
'rate': L10N_EC_VAT_RATES[code_percentage],
|
|
}
|
|
if extra_group == 'tax_group':
|
|
values['tax_group_id'] = tax_id.tax_group_id.id
|
|
elif extra_group == 'tax_support':
|
|
values['taxsupport'] = tax_id.l10n_ec_code_taxsupport
|
|
return values
|
|
|
|
return self._prepare_edi_tax_details(grouping_key_generator=group_by)
|
|
|
|
def _l10n_ec_map_tax_groups(self, tax_id):
|
|
# Maps different tax types (aka groups) to codes for electronic invoicing
|
|
ec_type = tax_id.tax_group_id.l10n_ec_type
|
|
if ec_type in L10N_EC_VAT_TAX_GROUPS:
|
|
return 2
|
|
elif ec_type == 'ice':
|
|
return 3
|
|
elif ec_type == 'irbpnr':
|
|
return 5
|
|
|
|
def _l10n_ec_get_invoice_edi_data(self):
|
|
def line_discount(line):
|
|
return line.currency_id.round(line._l10n_ec_prepare_edi_vals_to_export_USD()['price_discount'])
|
|
|
|
data = {
|
|
'taxes_data': self._l10n_ec_get_taxes_grouped(),
|
|
'additional_info': self._l10n_ec_get_invoice_additional_info(),
|
|
'discount_total': sum(map(line_discount, self.invoice_line_ids)),
|
|
}
|
|
if self.move_type == 'out_refund':
|
|
data.update({
|
|
'modified_doc': self.reversed_entry_id,
|
|
})
|
|
if self.l10n_latam_document_type_id.internal_type == 'debit_note':
|
|
data.update({
|
|
'modified_doc': self.debit_origin_id,
|
|
'invoice_lines': list(self.invoice_line_ids.filtered(lambda x: x.display_type == 'product')),
|
|
})
|
|
return data
|
|
|
|
def _l10n_ec_is_withholding(self):
|
|
return self.country_code == 'EC' and self.l10n_ec_withhold_type in ('in_withhold', 'out_withhold')
|
|
|
|
# ===== WITHHOLD XML GENERATION=====
|
|
|
|
def _l10n_ec_get_withhold_additional_info(self):
|
|
# Sister method to _l10n_ec_get_invoice_additional_info(), gets an additional info dict
|
|
data = {}
|
|
if self.commercial_partner_id.street or self.commercial_partner_id.street2:
|
|
data = {
|
|
"Direccion": " ".join(filter(None, [self.commercial_partner_id.street, self.commercial_partner_id.street2])),
|
|
}
|
|
if self.commercial_partner_id.email:
|
|
data["Email"] = self.commercial_partner_id.email
|
|
if self.commercial_partner_id.phone:
|
|
data['Telefono'] = self.commercial_partner_id.phone
|
|
return data
|
|
|
|
def _l10n_ec_get_withhold_edi_data(self):
|
|
# Computes the data needed for building the withhold xml, to be sent to qweb engine
|
|
self.ensure_one()
|
|
data = {
|
|
'taxsupport_lines': self._l10n_ec_get_withhold_edi_data_lines(),
|
|
'fiscal_period': str(self.date.month).zfill(2) + '/' + str(self.date.year),
|
|
"additional_info": self._l10n_ec_get_withhold_additional_info(),
|
|
'withhold_subtotals': self.l10n_ec_withhold_subtotals,
|
|
}
|
|
return data
|
|
|
|
@api.model
|
|
def _l10n_ec_wth_map_tax_code(self, withhold_line):
|
|
# Maps purchase withhold taxes to codes for assembling the EDI document
|
|
code = False
|
|
report_code = False
|
|
l10n_ec_type = withhold_line.tax_ids.tax_group_id.l10n_ec_type
|
|
if l10n_ec_type == 'withhold_income_purchase':
|
|
code = report_code = withhold_line.tax_ids.l10n_ec_code_ats
|
|
elif l10n_ec_type == 'withhold_vat_purchase':
|
|
percentage = abs(withhold_line.tax_ids.amount)
|
|
code = L10N_EC_WITHHOLD_VAT_CODES.get(percentage)
|
|
report_code = withhold_line.tax_ids.l10n_ec_code_applied
|
|
return code, report_code
|
|
|
|
def _l10n_ec_wth_get_foreign_data(self):
|
|
"""To include in the XML"""
|
|
self.ensure_one()
|
|
foreign_data = {
|
|
'identification': '01',
|
|
'paying_country': 'NA',
|
|
'double_taxation': 'NO',
|
|
'subject_withhold': 'NO',
|
|
'fiscal_payment': '',
|
|
'regime_type': '',
|
|
}
|
|
# This validation is for foreign partners with the field country_code
|
|
if self.commercial_partner_id.country_code != 'EC':
|
|
foreign_data['identification'] = '02'
|
|
taxes = self.line_ids.mapped('tax_ids')
|
|
if taxes:
|
|
foreign_data['paying_country'] = self.commercial_partner_id.country_id.l10n_ec_code_tax_haven or 'NA'
|
|
if any(tax_id.l10n_ec_code_base in L10N_EC_WTH_FOREIGN_GENERAL_REGIME_CODES for tax_id in taxes):
|
|
foreign_data['paying_country'] = self.commercial_partner_id.country_id.l10n_ec_code_ats or 'NA'
|
|
if any(tax_id.l10n_ec_code_base in L10N_EC_WTH_FOREIGN_DOUBLE_TAXATION_CODES for tax_id in taxes):
|
|
foreign_data['double_taxation'] = 'SI'
|
|
foreign_data['subject_withhold'] = 'SI'
|
|
if any(tax_id.l10n_ec_code_base in L10N_EC_WTH_FOREIGN_NOT_SUBJECT_WITHHOLD_CODES for tax_id in taxes):
|
|
foreign_data['subject_withhold'] = 'NO'
|
|
foreign_data['fiscal_payment'] = 'SI'
|
|
foreign_data['regime_type'] = self.l10n_ec_withhold_foreign_regime
|
|
return foreign_data
|
|
|
|
def _l10n_ec_get_withhold_edi_data_lines(self):
|
|
# Withholds has 3 levels, the header, the taxsupports, and its related withholds. As follows:
|
|
# Withhold Header, with withhold number, date, etc.
|
|
# |-- Taxsupports, list of taxsupports affected by the withhold, includes amount subtotals
|
|
# |-- Withhold lines, includes withhold tax, base, amount
|
|
if not self.l10n_ec_withhold_line_ids:
|
|
return {}
|
|
invoice = self.l10n_ec_withhold_line_ids[0].l10n_ec_withhold_invoice_id # current version supports only 1 invoice per withhold
|
|
taxsupport_detail = invoice._l10n_ec_get_taxes_grouped(extra_group='tax_support')['tax_details'].values()
|
|
foreign_data = self._l10n_ec_wth_get_foreign_data()
|
|
taxsupport_lines = {}
|
|
for line in self.l10n_ec_withhold_line_ids:
|
|
taxsupport = line.l10n_ec_code_taxsupport
|
|
if not taxsupport_lines.get(taxsupport):
|
|
taxsupport_amount_untaxed = sum(d['base_amount'] for d in taxsupport_detail if d['taxsupport'] == taxsupport)
|
|
taxsupport_amount_total = sum(d['base_amount'] + d['tax_amount'] for d in taxsupport_detail if d['taxsupport'] == taxsupport)
|
|
taxsupport_lines[taxsupport] = {
|
|
'invoice_taxsupport_code': taxsupport, # repeated from key, for readibility
|
|
'invoice_document_type': invoice.l10n_latam_document_type_id.name,
|
|
'invoice_document_type_code': invoice.l10n_latam_document_type_id_code,
|
|
'invoice_document_number': invoice.l10n_latam_document_number.replace('-', '').rjust(15, '0')[-15:],
|
|
'invoice_document_date': invoice.invoice_date.strftime('%d/%m/%Y'),
|
|
'invoice_amount_untaxed': taxsupport_amount_untaxed,
|
|
'invoice_amount_total': taxsupport_amount_total,
|
|
'invoice_taxes': [d for d in taxsupport_detail if d['taxsupport'] == taxsupport],
|
|
'withhold_lines': [], # to be extended below
|
|
'withhold_lines_count': 0,
|
|
'invoice_payments': [{'payment_code': invoice.l10n_ec_sri_payment_id.code,
|
|
'payment_amount': taxsupport_amount_total}],
|
|
}
|
|
taxsupport_lines[taxsupport].update(foreign_data)
|
|
code, report_code = self._l10n_ec_wth_map_tax_code(line)
|
|
taxsupport_lines[taxsupport]['withhold_lines'].append({
|
|
'tax_type': line.tax_ids.tax_group_id.l10n_ec_type,
|
|
'tax_type_code': L10N_EC_WITHHOLD_CODES.get(line.tax_ids.tax_group_id.l10n_ec_type),
|
|
'tax_code': code,
|
|
'tax_report_code': report_code,
|
|
'tax_base_amount': abs(line.balance),
|
|
'tax_rate': abs(line.tax_ids.amount), # even if the tax is negative
|
|
'tax_amount': abs(line.l10n_ec_withhold_tax_amount),
|
|
})
|
|
taxsupport_lines[taxsupport]['withhold_lines_count'] += 1
|
|
return list(taxsupport_lines.values())
|
|
|
|
def _l10n_ec_get_formas_de_pago(self):
|
|
"""Gets the value for the formasDePago key in the XML."""
|
|
self.ensure_one()
|
|
return [self.l10n_ec_sri_payment_id.code]
|
|
|
|
# ===== HELPER METHODS FOR WIZARD (Calculate amounts from invoice for withholding) =====
|
|
|
|
def _l10n_ec_get_inv_taxsupports_and_amounts(self):
|
|
""" Returns a dict of the base and tax amounts grouped by tax support for this invoice"""
|
|
self.ensure_one()
|
|
taxsupports = {}
|
|
for line in self.line_ids:
|
|
base_tax = line.tax_ids.filtered(lambda t: t.l10n_ec_code_taxsupport)
|
|
taxsupport_code = False
|
|
if line.tax_group_id.l10n_ec_type in L10N_EC_VAT_TAX_GROUPS:
|
|
taxsupport_code = line.tax_line_id.l10n_ec_code_taxsupport
|
|
elif base_tax:
|
|
taxsupport_code = base_tax.l10n_ec_code_taxsupport
|
|
|
|
if taxsupport_code:
|
|
taxsupports.setdefault(taxsupport_code, {
|
|
'amount_base': 0.0,
|
|
'amount_vat': 0.0,
|
|
})
|
|
if base_tax:
|
|
sign = -1 if line.move_id.is_inbound() else 1
|
|
taxsupports[taxsupport_code]['amount_base'] += sign * line.balance
|
|
else:
|
|
taxsupports[taxsupport_code]['amount_vat'] += abs(line.balance)
|
|
return taxsupports
|
|
|
|
def _get_profit_vat_tax_grouped_details(self):
|
|
""" This methods is to return the amounts grouped by tax support and the withhold tax to be applied"""
|
|
# Create a grouped tax method to return profit and vat tax details with _prepare_edi_tax_details method
|
|
self.ensure_one()
|
|
|
|
def grouping_function(wth_tax_index, base_line, tax_values):
|
|
line = base_line['record']
|
|
# Should return tuple doc sustento + withholding tax
|
|
tax_support = base_line['taxes'].l10n_ec_code_taxsupport
|
|
# Profit withhold logic
|
|
withhold = line._get_suggested_supplier_withhold_taxes()[wth_tax_index]
|
|
return {'tax_support': tax_support,
|
|
'withhold_tax': withhold}
|
|
|
|
# Calculate tax_details grouped for the (profit, VAT) withholds
|
|
return (
|
|
self._prepare_edi_tax_details(grouping_key_generator=partial(grouping_function, 1)), # profit grouping
|
|
self._prepare_edi_tax_details(grouping_key_generator=partial(grouping_function, 0)) # VAT grouping
|
|
)
|
|
|
|
# ===== WITHHOLD TAX SUMMARY WIDGET METHODS =====
|
|
|
|
@api.model
|
|
def _l10n_ec_withhold_subtotals_dict(self, currency_id, lines):
|
|
"""
|
|
This method returns the information for the tax summary widgets in both the withhold wizard as in the withholding
|
|
itself. That is why the lines are passed as parameter.
|
|
"""
|
|
vat_amount, pro_amount, vat_base, pro_base = 0.0, 0.0, 0.0, 0.0
|
|
vat_tax_group, pro_tax_group = None, None
|
|
for line in lines:
|
|
tax_group_id = line['tax_group']
|
|
if tax_group_id:
|
|
if tax_group_id.l10n_ec_type in ['withhold_vat_sale', 'withhold_vat_purchase']:
|
|
vat_tax_group = tax_group_id
|
|
vat_amount += line['amount']
|
|
vat_base += line['base']
|
|
elif tax_group_id.l10n_ec_type in ['withhold_income_sale', 'withhold_income_purchase']:
|
|
pro_tax_group = tax_group_id
|
|
pro_amount += line['amount']
|
|
pro_base += line['base']
|
|
|
|
if not (vat_tax_group or pro_tax_group):
|
|
return False # widget gives errors if no tax groups
|
|
|
|
wth_subtotals = {
|
|
'formatted_amount_total': formatLang(self.env, vat_amount + pro_amount, currency_obj=currency_id),
|
|
'allow_tax_edition': False,
|
|
'groups_by_subtotal': {},
|
|
'subtotals_order': [],
|
|
'subtotals': [],
|
|
'display_tax_base': False,
|
|
}
|
|
|
|
def add_subtotal(amount, base, currency, key):
|
|
# Add a subtotal to the widget
|
|
# We need to add a group_by_subtotal, subtotals and subtotals_order otherwise the widget will crash
|
|
formatted_base = formatLang(self.env, base, currency_obj=currency)
|
|
wth_subtotals['groups_by_subtotal'][key] = []
|
|
wth_subtotals['subtotals_order'].append(key)
|
|
wth_subtotals['subtotals'].append({
|
|
'name': key,
|
|
'formatted_amount': _('(base: %s) %s', formatted_base, formatLang(self.env, amount, currency_obj=currency))
|
|
})
|
|
|
|
if vat_tax_group:
|
|
add_subtotal(vat_amount, vat_base, currency_id, _("VAT Withhold"))
|
|
if pro_tax_group:
|
|
add_subtotal(pro_amount, pro_base, currency_id, _("Profit Withhold"))
|
|
|
|
return wth_subtotals
|
|
|
|
# ===== EDIs UNIQUE AUTHORIZATION NUMBER GENERATION =====
|
|
|
|
def _post(self, soft=True):
|
|
# Company must assign the unique authorization number before sending to SRI, it can be used in offline mode
|
|
moves = super()._post(soft)
|
|
self._l10n_ec_check_in_withhold_number_prefix() # Only for in withholds
|
|
for move in moves.filtered(lambda m: m.country_code == 'EC'):
|
|
if any(x.code == 'ecuadorian_edi' for x in move.journal_id.edi_format_ids):
|
|
move._l10n_ec_set_authorization_number()
|
|
return moves
|
|
|
|
def _l10n_ec_set_authorization_number(self):
|
|
self.ensure_one()
|
|
company = self.company_id
|
|
# NOTE: withholds don't have l10n_latam_document_type_id (WTH journals use separate sequence)
|
|
document_code_sri = '07' if self._l10n_ec_is_withholding() else self.l10n_latam_document_type_id.code
|
|
environment = company.l10n_ec_production_env and '2' or '1'
|
|
serie = self.journal_id.l10n_ec_entity + self.journal_id.l10n_ec_emission
|
|
sequential = self.name.split('-')[2].rjust(9, '0')
|
|
num_filler = '31215214' # can be any 8 digits, thanks @3cloud !
|
|
emission = '1' # corresponds to "normal" emission, "contingencia" no longer supported
|
|
|
|
if not (document_code_sri and company.partner_id.vat and environment
|
|
and serie and sequential and num_filler and emission):
|
|
return ''
|
|
|
|
now_date = (self.l10n_ec_withhold_date if self._l10n_ec_is_withholding() else self.invoice_date).strftime('%d%m%Y')
|
|
key_value = now_date + document_code_sri + company.partner_id.vat + environment + serie + sequential + num_filler + emission
|
|
self.l10n_ec_authorization_number = key_value + str(self._l10n_ec_get_check_digit(key_value))
|
|
|
|
@api.model
|
|
def _l10n_ec_get_check_digit(self, key):
|
|
sum_total = sum([int(key[-i - 1]) * (i % 6 + 2) for i in range(len(key))])
|
|
sum_check = 11 - (sum_total % 11)
|
|
if sum_check >= 10:
|
|
sum_check = 11 - sum_check
|
|
return sum_check
|
|
|
|
|
|
class AccountMoveLine(models.Model):
|
|
_inherit = 'account.move.line'
|
|
|
|
l10n_ec_withhold_invoice_id = fields.Many2one(
|
|
comodel_name='account.move',
|
|
string="Invoice",
|
|
copy=False,
|
|
ondelete='restrict',
|
|
help="Link the withholding line to its invoice",
|
|
)
|
|
l10n_ec_code_taxsupport = fields.Selection(
|
|
selection=L10N_EC_TAXSUPPORTS,
|
|
string="Tax Support",
|
|
help="Indicates if the purchase invoice supports tax credit or cost or expenses, conforming table 5 of ATS",
|
|
)
|
|
l10n_ec_withhold_tax_amount = fields.Monetary(
|
|
string="Withhold Tax Amount",
|
|
compute='_compute_withhold_tax_amount',
|
|
)
|
|
|
|
@api.depends('tax_ids')
|
|
def _compute_withhold_tax_amount(self):
|
|
for line in self.filtered('move_id.l10n_ec_withhold_type'):
|
|
currency_rate = line.balance / line.amount_currency if line.amount_currency != 0 else 1
|
|
line.l10n_ec_withhold_tax_amount = line.currency_id.round(
|
|
currency_rate * abs(line.price_total - line.price_subtotal))
|
|
|
|
def _compute_tax_key(self):
|
|
""" Override to allow extra keys/split in the withholding tax lines"""
|
|
super()._compute_tax_key()
|
|
for line in self.filtered('l10n_ec_withhold_invoice_id'):
|
|
line.tax_key = frozendict(**line.tax_key,
|
|
l10n_ec_withhold_invoice_id=line.l10n_ec_withhold_invoice_id.id,
|
|
l10n_ec_code_taxsupport=line.l10n_ec_code_taxsupport)
|
|
|
|
def _compute_all_tax(self):
|
|
""" Override to allow extra keys/split in the withholding tax lines"""
|
|
super()._compute_all_tax()
|
|
for line in self.filtered('l10n_ec_withhold_invoice_id'):
|
|
for key in list(line.compute_all_tax.keys()):
|
|
new_key = frozendict(**key,
|
|
l10n_ec_withhold_invoice_id=line.l10n_ec_withhold_invoice_id.id,
|
|
l10n_ec_code_taxsupport=line.l10n_ec_code_taxsupport)
|
|
line.compute_all_tax[new_key] = line.compute_all_tax.pop(key)
|
|
|
|
def _get_suggested_supplier_withhold_taxes(self):
|
|
'''
|
|
Returns the VAT and profit withhold tax to be applied on the vendor bill line to calculate a default
|
|
for in the wizard
|
|
|
|
For purchases adds prevalence for tax mapping to ease withholds in Ecuador, in the following order:
|
|
For profit withholding tax:
|
|
- If payment type is credit/debit/gift card then only use withhold code 332G (no VAT tax), else:
|
|
- partner_id.taxpayer_type.profit_withhold_tax_id, if not set then
|
|
- product_id profit withhold, if not set then
|
|
- company fallback profit withhold for goods or for services
|
|
For vat withhold tax:
|
|
- If document type is purchase liquidation then withhold 100% from VAT
|
|
- If product is consumable then taxpayer_type vat_goods_withhold_tax_id
|
|
- If product is services or not set then taxpayer_type vat_services_withhold_tax_id
|
|
'''
|
|
self.ensure_one()
|
|
vat_withhold_tax = False
|
|
profit_withhold_tax = False
|
|
taxpayer_type = self.move_id.commercial_partner_id.l10n_ec_taxpayer_type_id
|
|
is_domestic = self.move_id.commercial_partner_id.country_id.code == 'EC'
|
|
product_type = 'services' # it includes service, event, course and others
|
|
if self.product_id.type in ['consu', 'product']:
|
|
product_type = 'goods'
|
|
|
|
# suggest profit withhold
|
|
if self.move_id.l10n_ec_sri_payment_id.code in ['16', '18', '19']:
|
|
# override all withholds on payments with credit, debit or gift card
|
|
profit_withhold_tax = self.company_id.l10n_ec_withhold_credit_card_tax_id
|
|
return (vat_withhold_tax, profit_withhold_tax) # no other taxes apply
|
|
elif taxpayer_type.profit_withhold_tax_id:
|
|
profit_withhold_tax = taxpayer_type.profit_withhold_tax_id
|
|
elif self.product_id.l10n_ec_withhold_tax_id:
|
|
profit_withhold_tax = self.product_id.l10n_ec_withhold_tax_id
|
|
elif is_domestic and product_type == 'services':
|
|
profit_withhold_tax = self.company_id.l10n_ec_withhold_services_tax_id
|
|
elif is_domestic and product_type == 'goods':
|
|
profit_withhold_tax = self.company_id.l10n_ec_withhold_goods_tax_id
|
|
|
|
# suggest vat withhold
|
|
tax_groups = self.tax_ids.mapped('tax_group_id.l10n_ec_type')
|
|
if any(tax_group in L10N_EC_VAT_TAX_NOT_ZERO_GROUPS for tax_group in tax_groups):
|
|
if self.journal_id.l10n_ec_is_purchase_liquidation:
|
|
# law mandates to withhold 100% VAT on purchase liquidations
|
|
vat_withhold_tax = self.env['account.tax'].search([
|
|
*self.env['account.tax']._check_company_domain(self.company_id),
|
|
('tax_group_id.l10n_ec_type', '=', 'withhold_vat_purchase'),
|
|
('l10n_ec_code_applied', '=', '731'), # code for vat withhold 100%
|
|
])
|
|
elif product_type == 'services':
|
|
vat_withhold_tax = taxpayer_type.vat_services_withhold_tax_id
|
|
else: # goods
|
|
vat_withhold_tax = taxpayer_type.vat_goods_withhold_tax_id
|
|
return (vat_withhold_tax, profit_withhold_tax)
|
|
|
|
def _l10n_ec_prepare_edi_vals_to_export_USD(self):
|
|
results = super()._prepare_edi_vals_to_export()
|
|
currency_rate = self.balance / self.amount_currency if self.amount_currency != 0 else 1
|
|
|
|
included_taxes = self.tax_ids.filtered(lambda t: t.price_include)
|
|
if self.quantity and self.discount != 100.0 and included_taxes:
|
|
base_line = self._convert_to_tax_base_line_dict()
|
|
base_line['extra_context'].update({'round': False, 'round_base': False})
|
|
taxes_res = base_line['taxes']._origin.with_context(**base_line['extra_context']).compute_all(
|
|
base_line['price_unit'],
|
|
currency=base_line['currency'] or self.env.company.currency_id,
|
|
quantity=1,
|
|
product=base_line['product'],
|
|
partner=base_line['partner'],
|
|
is_refund=base_line['is_refund'],
|
|
handle_price_include=base_line['handle_price_include'],
|
|
)
|
|
price_unit = taxes_res['total_excluded']
|
|
else:
|
|
price_unit = self.price_unit
|
|
|
|
return {
|
|
'price_discount': float_round(results['price_discount'] * currency_rate, precision_digits=6),
|
|
'price_unit': float_round(price_unit * currency_rate, precision_digits=6),
|
|
}
|