Initial commit: L10N_Americas packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:52 +02:00
commit 12b27ce151
714 changed files with 79328 additions and 0 deletions

View file

@ -0,0 +1,12 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import res_partner
from . import l10n_ec_sri_payment
from . import l10n_latam_document_type
from . import account_move
from . import account_tax
from . import account_tax_group
from . import res_company
from . import account_journal
from . import account_chart_template

View file

@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class AccountChartTemplate(models.Model):
_inherit = "account.chart.template"
def _get_account_from_template(self, companies, template):
if template:
return self.env['account.account'].search([('company_id', 'in', companies.ids), ('code', '=', template.code)])
return self.env['account.account']
def _load(self, company):
# EXTENDS account to setup taxes groups accounts configuration
res = super()._load(company)
self._l10n_ec_configure_ecuadorian_tax_groups_accounts(company)
return res
def _l10n_ec_configure_ecuadorian_tax_groups_accounts(self, companies):
'''
Set tax groups accounts for automatic closing entry in 103 and 104 reports
The structure of the variable with the list of accounts by tax group:
('<tax_group_record_id>', '<payable_account_code>', '<receivable_account_code>')
'''
_TAX_GROUPS_ACCOUNTS_LIST = [
('tax_group_vat_05', '21070102', '11050202'),
('tax_group_vat_08', '21070102', '11050202'),
('tax_group_vat_12', '21070102', '11050202'),
('tax_group_vat_13', '21070102', '11050202'),
('tax_group_vat14', '21070102', '11050202'),
('tax_group_vat_15', '21070102', '11050202'),
('tax_group_vat0', '21070102', '11050202'),
('tax_group_vat_not_charged', '21070102', '11050202'),
('tax_group_vat_exempt', '21070102', '11050202'),
('tax_group_ice', '21070104', '21070104'),
('tax_group_irbpnr', '21070105', '21070105'),
('tax_group_withhold_vat_sale', '21070102', '11050203'),
('tax_group_withhold_vat_purchase', '21070102', '11050203'),
('tax_group_withhold_income_sale', '21070103', '11050201'),
('tax_group_withhold_income_purchase', '21070103', '11050201'),
('tax_group_outflows', '21070106', '11050205'),
('tax_group_others', '21070106', '11050205'),
]
for tax_group_xml_id, payable_account_code, receivable_account_code in _TAX_GROUPS_ACCOUNTS_LIST:
for company in companies.filtered(lambda company: company.account_fiscal_country_id.code == 'EC' and
company.chart_template_id == self.env.ref('l10n_ec.l10n_ec_ifrs')):
# search accounts
AccountObject = self.env['account.account']
company_domain = [('company_id', '=', company.id)]
payable_account_id = AccountObject.search([('code', '=', payable_account_code)] + company_domain)
receivable_account_id = AccountObject.search([('code', '=', receivable_account_code)] + company_domain)
# set accounts in tax groups by company
self.env.ref(f'l10n_ec.{tax_group_xml_id}').with_company(company).property_tax_payable_account_id = payable_account_id
self.env.ref(f'l10n_ec.{tax_group_xml_id}').with_company(company).property_tax_receivable_account_id = receivable_account_id
def _prepare_all_journals(self, acc_template_ref, company, journals_dict=None):
res = super()._prepare_all_journals(acc_template_ref, company, journals_dict=journals_dict)
if company.account_fiscal_country_id.code == 'EC':
for journal_values in res:
if journal_values.get('type') == 'sale':
journal_values.update({
'name': f"001-001 {journal_values['name']}",
'l10n_ec_entity': '001',
'l10n_ec_emission': '001',
'l10n_ec_emission_address_id': company.partner_id.id,
})
sale_account = acc_template_ref.get(self.env.ref('l10n_ec.ec410101', raise_if_not_found=False))
if sale_account:
journal_values['default_account_id'] = sale_account.id
if journal_values.get('type') == 'purchase':
purchase_account = acc_template_ref.get(self.env.ref('l10n_ec.ec52022816', raise_if_not_found=False))
if purchase_account:
journal_values['default_account_id'] = purchase_account.id
return res

View file

@ -0,0 +1,34 @@
from odoo import api, fields, models
class AccountJournal(models.Model):
_inherit = "account.journal"
l10n_ec_require_emission = fields.Boolean(
string='Require Emission',
compute='_compute_l10n_ec_require_emission',
help='True if an entity and emission point must be set on the journal'
)
l10n_ec_entity = fields.Char(string="Emission Entity", size=3, copy=False)
l10n_ec_emission = fields.Char(string="Emission Point", size=3, copy=False)
l10n_ec_emission_address_id = fields.Many2one(
comodel_name="res.partner",
string="Emission address",
domain="['|', ('id', '=', company_partner_id), '&', ('id', 'child_of', company_partner_id), ('type', '!=', 'contact')]",
)
@api.depends('type', 'country_code', 'l10n_latam_use_documents')
def _compute_l10n_ec_require_emission(self):
for journal in self:
journal.l10n_ec_require_emission = journal.type == 'sale' and journal.country_code == 'EC' and journal.l10n_latam_use_documents
# NOTE: Removed in master as it has no use
l10n_ec_emission_type = fields.Selection(
string="Emission type",
selection=[
("pre_printed", "Pre Printed"),
("auto_printer", "Auto Printer"),
("electronic", "Electronic"),
],
default="electronic",
)

View file

@ -0,0 +1,210 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.l10n_ec.models.res_partner import PartnerIdTypeEc
from odoo import fields, models, api
_DOCUMENTS_MAPPING = {
"01": [
'ec_dt_01',
'ec_dt_02',
'ec_dt_04',
'ec_dt_05',
'ec_dt_08',
'ec_dt_09',
'ec_dt_11',
'ec_dt_12',
'ec_dt_16',
'ec_dt_20',
'ec_dt_21',
'ec_dt_41',
'ec_dt_42',
'ec_dt_43',
'ec_dt_45',
'ec_dt_47',
'ec_dt_48'
],
"02": [
'ec_dt_03',
'ec_dt_04',
'ec_dt_05',
'ec_dt_09',
'ec_dt_19',
'ec_dt_41',
'ec_dt_294',
'ec_dt_344'
],
"03": [
'ec_dt_03',
'ec_dt_04',
'ec_dt_05',
'ec_dt_09',
'ec_dt_15',
'ec_dt_19',
'ec_dt_41',
'ec_dt_45',
'ec_dt_294',
'ec_dt_344'
],
"04": [
'ec_dt_01',
'ec_dt_04',
'ec_dt_05',
'ec_dt_41',
'ec_dt_44',
'ec_dt_47',
'ec_dt_48',
'ec_dt_49',
'ec_dt_50',
'ec_dt_51',
'ec_dt_52',
'ec_dt_370',
'ec_dt_371',
'ec_dt_372',
'ec_dt_373'
],
"05": [
'ec_dt_01',
'ec_dt_04',
'ec_dt_05',
'ec_dt_41',
'ec_dt_44',
'ec_dt_47',
'ec_dt_48',
'ec_dt_370',
'ec_dt_371',
'ec_dt_372',
'ec_dt_373'
],
"06": [
'ec_dt_01',
'ec_dt_04',
'ec_dt_05',
'ec_dt_41',
'ec_dt_44',
'ec_dt_47',
'ec_dt_48',
'ec_dt_370',
'ec_dt_371',
'ec_dt_372',
'ec_dt_373'
],
"07": [
'ec_dt_01',
'ec_dt_04',
'ec_dt_05',
],
"09": [
'ec_dt_01',
'ec_dt_04',
'ec_dt_05',
'ec_dt_15',
'ec_dt_16',
'ec_dt_41',
'ec_dt_47',
'ec_dt_48',
],
"20": [
'ec_dt_01',
'ec_dt_04',
'ec_dt_05',
'ec_dt_15',
'ec_dt_16',
'ec_dt_41',
'ec_dt_47',
'ec_dt_48'
],
"21": [
'ec_dt_01',
'ec_dt_04',
'ec_dt_05',
'ec_dt_15',
'ec_dt_16',
'ec_dt_41',
'ec_dt_47',
'ec_dt_48'
],
}
class AccountMove(models.Model):
_inherit = "account.move"
l10n_ec_sri_payment_id = fields.Many2one(
comodel_name="l10n_ec.sri.payment",
string="Payment Method (SRI)",
)
# NOTE: For backward compatibility, removed in master
def _get_l10n_ec_identification_type(self):
return PartnerIdTypeEc.get_ats_code_for_partner(self.partner_id, self.move_type)
@api.model
def _get_l10n_ec_documents_allowed(self, identification_code):
documents_allowed = self.env['l10n_latam.document.type']
for document_ref in _DOCUMENTS_MAPPING.get(identification_code.value, []):
document_allowed = self.env.ref('l10n_ec.%s' % document_ref, False)
if document_allowed:
documents_allowed |= document_allowed
return documents_allowed
def _get_l10n_latam_documents_domain(self):
self.ensure_one()
domain = super()._get_l10n_latam_documents_domain()
if self.country_code == 'EC' and self.journal_id.l10n_latam_use_documents:
if self.debit_origin_id: # show/hide the debit note document type
domain.extend([('internal_type', '=', 'debit_note')])
elif self.move_type in ('out_invoice', 'in_invoice'):
domain.extend([('internal_type', '=', 'invoice')])
allowed_documents = self._get_l10n_ec_documents_allowed(PartnerIdTypeEc.get_ats_code_for_partner(self.partner_id, self.move_type))
domain.extend([('id', 'in', allowed_documents.ids)])
return domain
def _get_ec_formatted_sequence(self, number=0):
return "%s %s-%s-%09d" % (
self.l10n_latam_document_type_id.doc_code_prefix,
self.journal_id.l10n_ec_entity,
self.journal_id.l10n_ec_emission,
number,
)
def _get_starting_sequence(self):
"""If use documents then will create a new starting sequence using the document type code prefix and the
journal document number with a 8 padding number"""
if (
self.journal_id.l10n_latam_use_documents
and self.company_id.country_id.code == "EC"
):
if self.l10n_latam_document_type_id:
return self._get_ec_formatted_sequence()
return super()._get_starting_sequence()
def _get_last_sequence_domain(self, relaxed=False):
where_string, param = super(AccountMove, self)._get_last_sequence_domain(relaxed)
if self.country_code == "EC" and self.l10n_latam_use_documents:
internal_type = self.l10n_latam_document_type_id.internal_type
document_types = self.env['l10n_latam.document.type'].search([
('internal_type', '=', internal_type),
('country_id.code', '=', 'EC'),
])
if document_types:
where_string += """
AND l10n_latam_document_type_id in %(l10n_latam_document_type_id)s
"""
param["l10n_latam_document_type_id"] = tuple(document_types.ids)
return where_string, param
def _skip_format_document_number(self):
"""
If a Credit Note is created from a Vendor Bill and the partner_id != "EC",
we want to allow the user to allocate any number without following the EC format.
"""
self.ensure_one()
if self.country_code == 'EC':
return (
self.l10n_latam_document_type_id.internal_type in ('credit_note', 'debit_note')
and self.partner_id.country_code != "EC"
and self.move_type == 'in_refund'
and self.journal_id.type == 'purchase'
)
super()._skip_format_document_number()

View file

@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class AccountTax(models.Model):
_inherit = "account.tax"
l10n_ec_code_base = fields.Char(
string="Code base",
help="Tax declaration code of the base amount prior to the calculation of the tax",
)
l10n_ec_code_applied = fields.Char(
string="Code applied",
help="Tax declaration code of the resulting amount after the calculation of the tax",
)
l10n_ec_code_ats = fields.Char(
string="Code ATS",
help="Tax Identification Code for the Simplified Transactional Annex",
)
class AccountTaxTemplate(models.Model):
_inherit = "account.tax.template"
def _get_tax_vals(self, company, tax_template_to_tax):
vals = super(AccountTaxTemplate, self)._get_tax_vals(
company, tax_template_to_tax
)
vals.update(
{
"l10n_ec_code_base": self.l10n_ec_code_base,
"l10n_ec_code_applied": self.l10n_ec_code_applied,
"l10n_ec_code_ats": self.l10n_ec_code_ats,
}
)
return vals
l10n_ec_code_base = fields.Char(
string="Code base",
help="Tax declaration code of the base amount prior to the calculation of the tax",
)
l10n_ec_code_applied = fields.Char(
string="Code applied",
help="Tax declaration code of the resulting amount after the calculation of the tax",
)
l10n_ec_code_ats = fields.Char(
string="Code ATS",
help="Tax Identification Code for the Simplified Transactional Annex",
)

View file

@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
_TYPE_EC = [
("vat05", "VAT 5%"),
("vat08", "VAT 8%"),
("vat12", "VAT 12%"),
("vat13", "VAT 13%"),
("vat14", "VAT 14%"),
("vat15", "VAT 15%"),
("zero_vat", "VAT 0%"),
("not_charged_vat", "VAT Not Charged"),
("exempt_vat", "VAT Exempt"),
("ice", "Special Consumptions Tax (ICE)"),
("irbpnr", "Plastic Bottles (IRBPNR)"),
("withhold_vat_sale", "VAT Withhold on Sales"),
("withhold_vat_purchase", "VAT Withhold on Purchases"),
("withhold_income_sale", "Profit Withhold on Sales"),
("withhold_income_purchase", "Profit Withhold on Purchases"),
("outflows_tax", "Exchange Outflows"),
("other", "Others"),
("withhold_vat", "VAT Withhold (Deprecated)"), # removed in master
("withhold_income_tax", "Profit Withhold (Deprecated)"), # removed in master
]
class AccountTaxGroup(models.Model):
_inherit = "account.tax.group"
l10n_ec_type = fields.Selection(
_TYPE_EC, string="Type Ecuadorian Tax", help="Ecuadorian taxes subtype"
)

View file

@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class SriPayment(models.Model):
_name = "l10n_ec.sri.payment"
_description = "SRI Payment Method"
name = fields.Char("Name")
code = fields.Char("Code")

View file

@ -0,0 +1,40 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, _
from odoo.exceptions import UserError
import re
class L10nLatamDocumentType(models.Model):
_inherit = "l10n_latam.document.type"
internal_type = fields.Selection(
selection_add=[
("purchase_liquidation", "Purchase Liquidation"),
("withhold", "Withhold"),
]
)
l10n_ec_check_format = fields.Boolean(
string="Check Number Format EC", default=False
)
def _format_document_number(self, document_number):
self.ensure_one()
if self.country_id != self.env.ref("base.ec"):
return super()._format_document_number(document_number)
if not document_number:
return False
if self.l10n_ec_check_format:
document_number = re.sub(r'\s+', "", document_number) # remove any whitespace
num_match = re.match(r'(\d{1,3})-(\d{1,3})-(\d{1,9})', document_number)
if num_match:
# Fill each number group with zeroes (3, 3 and 9 respectively)
document_number = "-".join([n.zfill(3 if i < 2 else 9) for i, n in enumerate(num_match.groups())])
else:
raise UserError(
_(u"Ecuadorian Document %s must be like 001-001-123456789")
% (self.display_name)
)
return document_number

View file

@ -0,0 +1,10 @@
from odoo import models
class ResCompany(models.Model):
_inherit = "res.company"
def _localization_use_documents(self):
self.ensure_one()
return self.account_fiscal_country_id.code == "EC" or super(ResCompany, self)._localization_use_documents()

View file

@ -0,0 +1,124 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import enum
import stdnum
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
def verify_final_consumer(vat):
return vat == '9' * 13 # final consumer is identified with 9999999999999
class PartnerIdTypeEc(enum.Enum):
"""
Ecuadorian partner identification type/code for ATS and SRI.
"""
IN_RUC = '01'
IN_CEDULA = '02'
IN_PASSPORT = '03'
OUT_RUC = '04'
OUT_CEDULA = '05'
OUT_PASSPORT = '06'
FINAL_CONSUMER = '07'
FOREIGN = '08'
@classmethod
def get_ats_code_for_partner(cls, partner, move_type):
"""
Returns ID code for move and partner based on subset of Table 2 of SRI's ATS specification
"""
partner_id_type = partner._l10n_ec_get_identification_type()
if move_type.startswith('in_'):
if partner_id_type == 'ruc': # includes final consumer
return cls.IN_RUC
elif partner_id_type == 'cedula':
return cls.IN_CEDULA
elif partner_id_type in ['foreign', 'passport']:
return cls.IN_PASSPORT
elif move_type.startswith('out_'):
if partner_id_type == 'ruc': # includes final consumer
return cls.OUT_RUC
elif partner_id_type == 'cedula':
return cls.OUT_CEDULA
elif partner_id_type in ['foreign', 'passport']:
return cls.OUT_PASSPORT
class ResPartner(models.Model):
_inherit = "res.partner"
l10n_ec_vat_validation = fields.Char(
string="VAT Error message validation",
compute="_compute_l10n_ec_vat_validation",
help="Error message when validating the Ecuadorian VAT",
)
@api.constrains("vat", "country_id", "l10n_latam_identification_type_id")
def check_vat(self):
it_ruc = self.env.ref("l10n_ec.ec_ruc", False)
it_dni = self.env.ref("l10n_ec.ec_dni", False)
ecuadorian_partners = self.filtered(
lambda x: x.country_id == self.env.ref("base.ec")
)
for partner in ecuadorian_partners:
if partner.vat:
if partner.l10n_latam_identification_type_id.id in (
it_ruc.id,
it_dni.id,
):
if partner.l10n_latam_identification_type_id.id == it_dni.id and len(partner.vat) != 10:
raise ValidationError(_('If your identification type is %s, it must be 10 digits')
% it_dni.display_name)
if partner.l10n_latam_identification_type_id.id == it_ruc.id and len(partner.vat) != 13:
raise ValidationError(_('If your identification type is %s, it must be 13 digits')
% it_ruc.display_name)
return super(ResPartner, self - ecuadorian_partners).check_vat()
@api.depends("vat", "country_id", "l10n_latam_identification_type_id")
def _compute_l10n_ec_vat_validation(self):
it_ruc = self.env.ref("l10n_ec.ec_ruc", False)
it_dni = self.env.ref("l10n_ec.ec_dni", False)
ruc = stdnum.util.get_cc_module("ec", "ruc")
ci = stdnum.util.get_cc_module("ec", "ci")
for partner in self:
partner.l10n_ec_vat_validation = False
if partner and partner.l10n_latam_identification_type_id in (it_ruc, it_dni) and partner.vat:
final_consumer = verify_final_consumer(partner.vat)
if not final_consumer:
if partner.l10n_latam_identification_type_id.id == it_dni.id and not ci.is_valid(partner.vat):
partner.l10n_ec_vat_validation = _("The VAT %s seems to be invalid as the tenth digit doesn't comply with the validation algorithm "
"(could be an old VAT number)") % partner.vat
if partner.l10n_latam_identification_type_id.id == it_ruc.id and not ruc.is_valid(partner.vat):
partner.l10n_ec_vat_validation = _("The VAT %s seems to be invalid as the tenth digit doesn't comply with the validation algorithm "
"(SRI has stated that this validation is not required anymore for some VAT numbers)") % partner.vat
def _l10n_ec_get_identification_type(self):
"""Maps Odoo identification types to Ecuadorian ones.
Useful for document type domains, electronic documents, ats, others.
"""
self.ensure_one()
id_types_by_xmlid = {
'l10n_ec.ec_dni': 'cedula', # DNI
'l10n_ec.ec_ruc': 'ruc', # RUC
'l10n_ec.ec_passport': 'ec_passport', # EC passport
'l10n_latam_base.it_pass': 'passport', # Passport
'l10n_latam_base.it_fid': 'foreign', # Foreign ID
'l10n_latam_base.it_vat': 'foreign',
}
# This method is orm-cached, which makes it more efficient in loops than get_external_id()
xmlid_by_res_id = {
self.env['ir.model.data']._xmlid_to_res_model_res_id(xmlid, raise_if_not_found=True)[1]: xmlid
for xmlid in id_types_by_xmlid
}
id_type_xmlid = xmlid_by_res_id.get(self.l10n_latam_identification_type_id.id)
if id_type_xmlid in id_types_by_xmlid:
return id_types_by_xmlid[id_type_xmlid]
if self.l10n_latam_identification_type_id.country_id.code != 'EC':
return 'foreign'