19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:31:16 +01:00
parent 89c6e82fe7
commit 1b82c20a58
572 changed files with 43570 additions and 53303 deletions

View file

@ -0,0 +1,8 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import account_move
from . import account_tax
from . import l10n_latam_identification_type
from . import res_company
from . import res_partner
from . import template_uy
from . import l10n_latam_document_type

View file

@ -0,0 +1,49 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
# Let us match the document types to properly suggest the DN and CN documents
# NOTE: this can be avoided if we have an extra subclassification of UY documents
UY_DOC_SUBTYPES = [
["0"], # not electronic
["101", "102", "103", "201", "202", "203"], # e-ticket
["111", "112", "113", "211", "212", "213"], # e-invoice
["121", "122", "123", "221", "222", "223"], # e-inv-expo
["151", "152", "153", "251", "252", "253"], # e-boleta (not implemented yet)
]
class AccountMove(models.Model):
_inherit = 'account.move'
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.l10n_latam_use_documents and self.company_id.account_fiscal_country_id.code == "UY" and self.l10n_latam_document_type_id:
return self._l10n_uy_get_formatted_sequence()
return super()._get_starting_sequence()
def _l10n_uy_get_formatted_sequence(self, number=0):
return "%s A%07d" % (self.l10n_latam_document_type_id.doc_code_prefix, number)
def _get_last_sequence_domain(self, relaxed=False):
where_string, param = super(AccountMove, self)._get_last_sequence_domain(relaxed)
if self.company_id.account_fiscal_country_id.code == "UY" and self.l10n_latam_use_documents:
where_string += " AND l10n_latam_document_type_id = %(l10n_latam_document_type_id)s"
param['l10n_latam_document_type_id'] = self.l10n_latam_document_type_id.id or 0
return where_string, param
def _get_l10n_latam_documents_domain(self):
""" If this is a reversal or debit, suggest only related subtypes """
self.ensure_one()
domain = super()._get_l10n_latam_documents_domain()
if self.country_code == "UY" and (original_move := self.reversed_entry_id or self.debit_origin_id):
matching_subtype_codes = [
subtype for subtype in UY_DOC_SUBTYPES
if original_move.l10n_latam_document_type_id.code in subtype
]
if matching_subtype_codes:
# restrict to the codes from the subtype matching the one of the original_move (e.g. 'e-ticket')
codes = self.env["l10n_latam.document.type"].search(domain).mapped('code')
allowed_codes = set(codes).intersection(set(matching_subtype_codes[0]))
domain += [("code", "in", tuple(allowed_codes))]
return domain

View file

@ -0,0 +1,10 @@
# 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_uy_tax_category = fields.Selection([
('vat', 'VAT'),
], string="Tax Category", help="UY: Use to group the transactions in the Financial Reports required by DGI")

View file

@ -0,0 +1,35 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, models
from odoo.exceptions import UserError
import re
class L10n_LatamDocumentType(models.Model):
_inherit = 'l10n_latam.document.type'
def _format_document_number(self, document_number):
""" format and validate the document_number"""
self.ensure_one()
if self.country_id.code != "UY":
return super()._format_document_number(document_number)
if not document_number:
return False
if self.code == "0":
return document_number
document_number = document_number.strip()
number_part = re.findall(r'[\d]+', document_number)
serie_part = re.findall(r'^[A-Za-z]+', document_number)
if not serie_part or len(serie_part) > 1 or len(serie_part[0]) > 2 \
or not number_part or len(number_part) > 1 or len(number_part[0]) > 7:
raise UserError(_(
"%(document_number)s is not a valid value for %(document_type)s.\n"
"The document number must be entered with a maximum of 2 letters for the first part "
"and 7 numbers for the second. The following are examples of valid document numbers:\n"
"- XX0000001\n - YY0000123\n - A0000001",
document_number=document_number,
document_type=self.name,
))
return serie_part[0].upper() + number_part[0].zfill(7)

View file

@ -0,0 +1,8 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields
class L10n_LatamIdentificationType(models.Model):
_inherit = "l10n_latam.identification.type"
l10n_uy_dgi_code = fields.Char('DGI Code')

View file

@ -0,0 +1,14 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class ResCompany(models.Model):
_inherit = 'res.company'
def _localization_use_documents(self):
""" Uruguayan localization use documents """
self.ensure_one()
return self.chart_template == 'uy' or self.account_fiscal_country_id.code == "UY" or super()._localization_use_documents()
def _is_latam(self):
return super()._is_latam() or self.country_code == 'UY'

View file

@ -0,0 +1,101 @@
import logging
import re
from odoo import api, models, _
from odoo.exceptions import ValidationError
_logger = logging.getLogger(__name__)
class ResPartner(models.Model):
_inherit = "res.partner"
def _run_check_identification(self, validation='error'):
"""Add validation of UY document types CI and NIE """
if validation == 'error':
ci_nie_types = self.filtered(lambda p:
p.l10n_latam_identification_type_id.l10n_uy_dgi_code in ("1", "3")
and p.l10n_latam_identification_type_id.country_id.code == "UY"
and p.vat
)
for partner in ci_nie_types:
if not partner._l10n_uy_ci_nie_is_valid():
raise ValidationError(self._l10n_uy_build_vat_error_message(partner))
return super()._run_check_identification(validation=validation)
@api.model
def _l10n_uy_build_vat_error_message(self, partner):
""" Similar to _build_vat_error_message but using latam doc type name instead of vat_label
NOTE: maybe can be implemented in master to l10n_latam_base for the use of different doc types """
vat_label = _("CI/NIE")
expected_format = _("3:402.010-2 or 93:402.010-1 (CI or NIE)")
# Catch use case where the record label is about the public user (name: False)
if partner.name:
msg = "\n" + _(
"The %(vat_label)s number [%(wrong_vat)s] for %(partner_label)s does not seem to be valid."
"\nNote: the expected format is %(expected_format)s",
vat_label=vat_label,
wrong_vat=partner.vat,
partner_label=_("partner [%s]", partner.name),
expected_format=expected_format,
)
else:
msg = "\n" + _(
"The %(vat_label)s number [%(wrong_vat)s] does not seem to be valid."
"\nNote: the expected format is %(expected_format)s",
vat_label=vat_label,
wrong_vat=partner.vat,
expected_format=expected_format,
)
return msg
def _l10n_uy_ci_nie_is_valid(self):
""" Check if the partner's CI or NIE number is a valid one.
CI:
1) The ID number is taken up to the second to last position, that is, the first 6 or 7 digits.
2) Each digit is multiplied by a different factor starting from right to left, the factors are:
2, 9, 8, 7, 6, 3, 4.
3) The products obtained are added:
4) The base module 10 is calculated on this result to obtain the check digit, expressed in another way,
the next number ending in zero is taken that follows the result of the addition (for the example
would be 60) subtracting the sum itself: 60 - 59 = 1. The verification digit of the example ID is 1.
NOTE: If the ID has fewer digits, it is preceded with zeros and the mechanism described above is applied
NIE:
The calculation for the NIE is the same as that used for the CI. The only difference is that we skip the
first number
Both algorithms where extracted from Uruware's Technical Manual (section 9.2 and 9.3)
Return: False is not valid, True is valid
"""
self.ensure_one()
# The VAT must consist only numbers (format could have these characters ":., " we can skip them later)
invalid_chars = re.findall(r"[^0-9:., \-]", self.vat)
if invalid_chars:
return False
ci_nie_number = re.sub("[^0-9]", "", self.vat)
# we get the validation digit, if NIE doc type we skip the first digit
is_nie = self.l10n_latam_identification_type_id.l10n_uy_dgi_code == "1"
verif_digit = int(ci_nie_number[-1])
ci_nie_number = ci_nie_number[1:-1] if is_nie else ci_nie_number[0:-1]
# If number is < 7 digits we add 0 to the left
ci_nie_number = "%07d" % int(ci_nie_number)
# If NIE > 7 digits is not valid
if len(ci_nie_number) > 7:
return False
verification_vector = (2, 9, 8, 7, 6, 3, 4)
num_sum = sum(int(ci_nie_number[i]) * verification_vector[i] for i in range(7))
res = -num_sum % 10
return res == verif_digit

View file

@ -0,0 +1,73 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, _
from odoo.addons.account.models.chart_template import template
class AccountChartTemplate(models.AbstractModel):
_inherit = 'account.chart.template'
@template('uy')
def _get_uy_template_data(self):
return {
'property_account_receivable_id': 'uy_code_11300',
'property_account_payable_id': 'uy_code_21100',
'code_digits': '6',
'name': _('Uruguayan Generic Chart of Accounts'),
}
@template('uy', 'res.company')
def _get_uy_res_company(self):
return {
self.env.company.id: {
'account_fiscal_country_id': 'base.uy',
'bank_account_code_prefix': '1111',
'cash_account_code_prefix': '1112',
'transfer_account_code_prefix': '11120',
'account_default_pos_receivable_account_id': 'uy_code_11307',
'income_currency_exchange_account_id': 'uy_code_4302',
'expense_currency_exchange_account_id': 'uy_code_5302',
'account_journal_early_pay_discount_loss_account_id': 'uy_code_5303',
'account_journal_early_pay_discount_gain_account_id': 'uy_code_4303',
'account_sale_tax_id': 'vat1',
'account_purchase_tax_id': 'vat4',
'deferred_expense_account_id': 'uy_code_11407',
'deferred_revenue_account_id': 'uy_code_21321',
'income_account_id': 'uy_code_4102',
'expense_account_id': 'uy_code_5100',
'account_stock_journal_id': 'inventory_valuation',
'account_stock_valuation_id': 'uy_code_11704',
},
}
@template('uy', 'account.journal')
def _get_uy_account_journal(self):
return {
'sale': {
"name": _("Sales"),
"code": "0001",
"l10n_latam_use_documents": True,
"refund_sequence": False,
},
'purchase': {
"name": _("Purchases"),
"code": "0002",
"l10n_latam_use_documents": True,
"refund_sequence": False,
},
}
def _load(self, template_code, company, install_demo, force_create=True):
""" Set companies rut as the company identification type after install the chart of account,
this one is the uruguayan vat """
res = super()._load(template_code, company, install_demo, force_create)
if template_code == 'uy':
company.partner_id.l10n_latam_identification_type_id = self.env.ref('l10n_uy.it_rut')
return res
@template('uy', 'account.account')
def _get_uy_account_account(self):
return {
'uy_code_11704': {
'account_stock_variation_id': 'uy_code_5401',
},
}