1
0
forked from Mapan/odoo17e
odoo17e-kedaikipas58/addons/l10n_ec_edi/models/account_move.py
2024-12-10 09:04:09 +07:00

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),
}