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,18 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import l10n_latam_identification_type
from . import l10n_ar_afip_responsibility_type
from . import account_journal
from . import account_tax_group
from . import account_fiscal_position
from . import account_fiscal_position_template
from . import l10n_latam_document_type
from . import res_partner
from . import res_country
from . import res_currency
from . import res_company
from . import res_partner_bank
from . import uom_uom
from . import account_chart_template
from . import account_move
from . import account_move_line

View file

@ -0,0 +1,73 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, api, _
from odoo.exceptions import UserError
from odoo.http import request
class AccountChartTemplate(models.Model):
_inherit = 'account.chart.template'
def _get_fp_vals(self, company, position):
res = super()._get_fp_vals(company, position)
if company.country_id.code == "AR":
res['l10n_ar_afip_responsibility_type_ids'] = [
(6, False, position.l10n_ar_afip_responsibility_type_ids.ids)]
return res
def _prepare_all_journals(self, acc_template_ref, company, journals_dict=None):
""" In case of an Argentinean CoA, we modify the default values of the sales journal to be a preprinted journal"""
res = super()._prepare_all_journals(acc_template_ref, company, journals_dict=journals_dict)
if company.country_id.code == "AR":
for vals in res:
if vals['type'] == 'sale':
vals.update({
"name": _("Ventas Preimpreso"),
"code": "0001",
"l10n_ar_afip_pos_number": 1,
"l10n_ar_afip_pos_partner_id": company.partner_id.id,
"l10n_ar_afip_pos_system": 'II_IM',
"refund_sequence": False
})
return res
@api.model
def _get_ar_responsibility_match(self, chart_template_id):
""" return responsibility type that match with the given chart_template_id
"""
match = {
self.env.ref('l10n_ar.l10nar_base_chart_template').id: self.env.ref('l10n_ar.res_RM'),
self.env.ref('l10n_ar.l10nar_ex_chart_template').id: self.env.ref('l10n_ar.res_IVAE'),
self.env.ref('l10n_ar.l10nar_ri_chart_template').id: self.env.ref('l10n_ar.res_IVARI'),
}
return match.get(chart_template_id)
def _load(self, company):
""" Set companies AFIP Responsibility and Country if AR CoA is installed, also set tax calculation rounding
method required in order to properly validate match AFIP invoices.
Also, raise a warning if the user is trying to install a CoA that does not match with the defined AFIP
Responsibility defined in the company
"""
self.ensure_one()
coa_responsibility = self._get_ar_responsibility_match(self.id)
if coa_responsibility:
company_responsibility = company.l10n_ar_afip_responsibility_type_id
company.write({
'l10n_ar_afip_responsibility_type_id': coa_responsibility.id,
'country_id': self.env['res.country'].search([('code', '=', 'AR')]).id,
'tax_calculation_rounding_method': 'round_globally',
})
# set CUIT identification type (which is the argentinean vat) in the created company partner instead of
# the default VAT type.
company.partner_id.l10n_latam_identification_type_id = self.env.ref('l10n_ar.it_cuit')
res = super()._load(company)
# If Responsable Monotributista remove the default purchase tax
if self == self.env.ref('l10n_ar.l10nar_base_chart_template') or \
self == self.env.ref('l10n_ar.l10nar_ex_chart_template'):
company.account_purchase_tax_id = self.env['account.tax']
return res

View file

@ -0,0 +1,26 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, api, _
class AccountFiscalPosition(models.Model):
_inherit = 'account.fiscal.position'
l10n_ar_afip_responsibility_type_ids = fields.Many2many(
'l10n_ar.afip.responsibility.type', 'l10n_ar_afip_reponsibility_type_fiscal_pos_rel',
string='AFIP Responsibility Types', help='List of AFIP responsibilities where this fiscal position '
'should be auto-detected')
@api.model
def _get_fiscal_position(self, partner, delivery=None):
if self.env.company.country_id.code != "AR":
return super()._get_fiscal_position(partner, delivery=delivery)
return super(AccountFiscalPosition, self.with_context(l10n_ar_afip_responsibility_type_id=partner.l10n_ar_afip_responsibility_type_id.id))._get_fiscal_position(partner, delivery=delivery)
def _prepare_fpos_base_domain(self, vat_required):
domain = super()._prepare_fpos_base_domain(vat_required)
if 'l10n_ar_afip_responsibility_type_id' in self._context:
domain += ['|',
('l10n_ar_afip_responsibility_type_ids', '=', False),
('l10n_ar_afip_responsibility_type_ids', '=', self._context.get('l10n_ar_afip_responsibility_type_id'))]
return domain

View file

@ -0,0 +1,11 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class AccountFiscalPositionTemplate(models.Model):
_inherit = 'account.fiscal.position.template'
l10n_ar_afip_responsibility_type_ids = fields.Many2many(
'l10n_ar.afip.responsibility.type', 'l10n_ar_afip_reponsibility_type_fiscal_pos_temp_rel',
string='AFIP Responsibility Types')

View file

@ -0,0 +1,141 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, api, _
from odoo.exceptions import ValidationError, RedirectWarning
class AccountJournal(models.Model):
_inherit = "account.journal"
l10n_ar_afip_pos_system = fields.Selection(
selection='_get_l10n_ar_afip_pos_types_selection', string='AFIP POS System')
l10n_ar_afip_pos_number = fields.Integer(
'AFIP POS Number', help='This is the point of sale number assigned by AFIP in order to generate invoices')
company_partner = fields.Many2one('res.partner', related='company_id.partner_id')
l10n_ar_afip_pos_partner_id = fields.Many2one(
'res.partner', 'AFIP POS Address', help='This is the address used for invoice reports of this POS',
domain="['|', ('id', '=', company_partner), '&', ('id', 'child_of', company_partner), ('type', '!=', 'contact')]"
)
def _get_l10n_ar_afip_pos_types_selection(self):
""" Return the list of values of the selection field. """
return [
('II_IM', _('Pre-printed Invoice')),
('RLI_RLM', _('Online Invoice')),
('BFERCEL', _('Electronic Fiscal Bond - Online Invoice')),
('FEERCELP', _('Export Voucher - Billing Plus')),
('FEERCEL', _('Export Voucher - Online Invoice')),
('CPERCEL', _('Product Coding - Online Voucher')),
]
def _get_journal_letter(self, counterpart_partner=False):
""" Regarding the AFIP responsibility of the company and the type of journal (sale/purchase), get the allowed
letters. Optionally, receive the counterpart partner (customer/supplier) and get the allowed letters to work
with him. This method is used to populate document types on journals and also to filter document types on
specific invoices to/from customer/supplier
"""
self.ensure_one()
letters_data = {
'issued': {
'1': ['A', 'B', 'E', 'M'],
'4': ['C'],
'5': [],
'6': ['C', 'E'],
'7': ['B', 'C', 'I'],
'8': ['B', 'C', 'I'],
'9': ['I'],
'10': [],
'13': ['C', 'E'],
'15': [],
'16': [],
},
'received': {
'1': ['A', 'B', 'C', 'E', 'M', 'I'],
'4': ['B', 'C', 'I'],
'5': ['B', 'C', 'I'],
'6': ['A', 'B', 'C', 'M', 'I'],
'7': ['B', 'C', 'I'],
'8': ['E', 'B', 'C'],
'9': ['E', 'B', 'C'],
'10': ['E', 'B', 'C'],
'13': ['A', 'B', 'C', 'M', 'I'],
'15': ['B', 'C', 'I'],
'16': ['A', 'C', 'M'],
},
}
if not self.company_id.l10n_ar_afip_responsibility_type_id:
action = self.env.ref('base.action_res_company_form')
msg = _('Can not create chart of account until you configure your company AFIP Responsibility and VAT.')
raise RedirectWarning(msg, action.id, _('Go to Companies'))
letters = letters_data['issued' if self.type == 'sale' else 'received'][
self.company_id.l10n_ar_afip_responsibility_type_id.code]
if counterpart_partner:
counterpart_letters = letters_data['issued' if self.type == 'purchase' else 'received'].get(
counterpart_partner.l10n_ar_afip_responsibility_type_id.code, [])
letters = list(set(letters) & set(counterpart_letters))
return letters
def _get_journal_codes(self):
self.ensure_one()
if self.type != 'sale':
return []
return self._get_codes_per_journal_type(self.l10n_ar_afip_pos_system)
@api.model
def _get_codes_per_journal_type(self, afip_pos_system):
usual_codes = ['1', '2', '3', '6', '7', '8', '11', '12', '13']
mipyme_codes = ['201', '202', '203', '206', '207', '208', '211', '212', '213']
invoice_m_code = ['51', '52', '53']
receipt_m_code = ['54']
receipt_codes = ['4', '9', '15']
expo_codes = ['19', '20', '21']
zeta_codes = ['80', '83']
if afip_pos_system == 'II_IM':
# pre-printed invoice
return usual_codes + receipt_codes + expo_codes + invoice_m_code + receipt_m_code
elif afip_pos_system == 'RAW_MAW':
# electronic/online invoice
return usual_codes + receipt_codes + invoice_m_code + receipt_m_code + mipyme_codes
elif afip_pos_system == 'RLI_RLM':
return usual_codes + receipt_codes + invoice_m_code + receipt_m_code + mipyme_codes + zeta_codes
elif afip_pos_system in ['CPERCEL', 'CPEWS']:
# invoice with detail
return usual_codes + invoice_m_code
elif afip_pos_system in ['BFERCEL', 'BFEWS']:
# Bonds invoice
return usual_codes + mipyme_codes
elif afip_pos_system in ['FEERCEL', 'FEEWS', 'FEERCELP']:
return expo_codes
@api.constrains('type', 'l10n_ar_afip_pos_system', 'l10n_ar_afip_pos_number', 'l10n_latam_use_documents')
def _check_afip_configurations(self):
""" Do not let the user update the journal if it already contains confirmed invoices """
journals = self.filtered(lambda x: x.company_id.account_fiscal_country_id.code == "AR" and x.type in ['sale', 'purchase'])
invoices = self.env['account.move'].search([('journal_id', 'in', journals.ids), ('posted_before', '=', True)], limit=1)
if invoices:
raise ValidationError(
_("You can not change the journal's configuration if it already has validated invoices") + ' ('
+ ', '.join(invoices.mapped('journal_id').mapped('name')) + ')')
@api.constrains('l10n_ar_afip_pos_number')
def _check_afip_pos_number(self):
to_review = self.filtered(
lambda x: x.type == 'sale' and x.l10n_latam_use_documents and
x.company_id.account_fiscal_country_id.code == "AR")
if to_review.filtered(lambda x: x.l10n_ar_afip_pos_number == 0):
raise ValidationError(_('Please define an AFIP POS number'))
if to_review.filtered(lambda x: x.l10n_ar_afip_pos_number > 99999):
raise ValidationError(_('Please define a valid AFIP POS number (5 digits max)'))
@api.onchange('l10n_ar_afip_pos_number', 'type')
def _onchange_set_short_name(self):
""" Will define the AFIP POS Address field domain taking into account the company configured in the journal
The short code of the journal only admit 5 characters, so depending on the size of the pos_number (also max 5)
we add or not a prefix to identify sales journal.
"""
if self.type == 'sale' and self.l10n_ar_afip_pos_number:
self.code = "%05i" % self.l10n_ar_afip_pos_number

View file

@ -0,0 +1,411 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields, api, _
from odoo.exceptions import UserError, RedirectWarning, ValidationError
from odoo.tools.misc import formatLang
from dateutil.relativedelta import relativedelta
import logging
_logger = logging.getLogger(__name__)
class AccountMove(models.Model):
_inherit = 'account.move'
@api.model
def _l10n_ar_get_document_number_parts(self, document_number, document_type_code):
# import shipments
if document_type_code in ['66', '67']:
pos = invoice_number = '0'
else:
pos, invoice_number = document_number.split('-')
return {'invoice_number': int(invoice_number), 'point_of_sale': int(pos)}
l10n_ar_afip_responsibility_type_id = fields.Many2one(
'l10n_ar.afip.responsibility.type', string='AFIP Responsibility Type', help='Defined by AFIP to'
' identify the type of responsibilities that a person or a legal entity could have and that impacts in the'
' type of operations and requirements they need.')
l10n_ar_currency_rate = fields.Float(copy=False, digits=(16, 6), readonly=True, string="Currency Rate")
# Mostly used on reports
l10n_ar_afip_concept = fields.Selection(
compute='_compute_l10n_ar_afip_concept', selection='_get_afip_invoice_concepts', string="AFIP Concept",
help="A concept is suggested regarding the type of the products on the invoice but it is allowed to force a"
" different type if required.")
l10n_ar_afip_service_start = fields.Date(string='AFIP Service Start Date', readonly=True, states={'draft': [('readonly', False)]})
l10n_ar_afip_service_end = fields.Date(string='AFIP Service End Date', readonly=True, states={'draft': [('readonly', False)]})
@api.constrains('move_type', 'journal_id')
def _check_moves_use_documents(self):
""" Do not let to create not invoices entries in journals that use documents """
not_invoices = self.filtered(lambda x: x.company_id.account_fiscal_country_id.code == "AR" and x.journal_id.type in ['sale', 'purchase'] and x.l10n_latam_use_documents and not x.is_invoice())
if not_invoices:
raise ValidationError(_("The selected Journal can't be used in this transaction, please select one that doesn't use documents as these are just for Invoices."))
@api.constrains('move_type', 'l10n_latam_document_type_id')
def _check_invoice_type_document_type(self):
""" LATAM module define that we are not able to use debit_note or invoice document types in an invoice refunds,
However for Argentinian Document Type's 99 (internal type = invoice) we are able to used in a refund invoices.
In this method we exclude the argentinian documents that can be used as invoice and refund from the generic
constraint """
docs_used_for_inv_and_ref = self.filtered(
lambda x: x.country_code == 'AR' and
x.l10n_latam_document_type_id.code in self._get_l10n_ar_codes_used_for_inv_and_ref() and
x.move_type in ['out_refund', 'in_refund'])
super(AccountMove, self - docs_used_for_inv_and_ref)._check_invoice_type_document_type()
def _get_afip_invoice_concepts(self):
""" Return the list of values of the selection field. """
return [('1', 'Products / Definitive export of goods'), ('2', 'Services'), ('3', 'Products and Services'),
('4', '4-Other (export)')]
@api.depends('invoice_line_ids', 'invoice_line_ids.product_id', 'invoice_line_ids.product_id.type', 'journal_id')
def _compute_l10n_ar_afip_concept(self):
recs_afip = self.filtered(lambda x: x.company_id.account_fiscal_country_id.code == "AR" and x.l10n_latam_use_documents)
for rec in recs_afip:
rec.l10n_ar_afip_concept = rec._get_concept()
remaining = self - recs_afip
remaining.l10n_ar_afip_concept = ''
def _get_concept(self):
""" Method to get the concept of the invoice considering the type of the products on the invoice """
self.ensure_one()
invoice_lines = self.invoice_line_ids.filtered(lambda x: x.display_type not in ('line_note', 'line_section'))
product_types = set([x.product_id.type for x in invoice_lines if x.product_id])
consumable = set(['consu', 'product'])
service = set(['service'])
# on expo invoice you can mix services and products
expo_invoice = self.l10n_latam_document_type_id.code in ['19', '20', '21']
# WSFEX 1668 - If Expo invoice and we have a "IVA Liberado Ley Nº 19.640" (Zona Franca) partner
# then AFIP concept to use should be type "Others (4)"
is_zona_franca = self.partner_id.l10n_ar_afip_responsibility_type_id == self.env.ref("l10n_ar.res_IVA_LIB")
# Default value "product"
afip_concept = '1'
if expo_invoice and is_zona_franca:
afip_concept = '4'
elif product_types == service:
afip_concept = '2'
elif product_types - consumable and product_types - service and not expo_invoice:
afip_concept = '3'
return afip_concept
@api.model
def _get_l10n_ar_codes_used_for_inv_and_ref(self):
""" List of document types that can be used as an invoice and refund. This list can be increased once needed
and demonstrated. As far as we've checked document types of wsfev1 don't allow negative amounts so, for example
document 61 could not be used as refunds. """
return ['99', '186', '188', '189', '60']
def _get_l10n_latam_documents_domain(self):
self.ensure_one()
domain = super()._get_l10n_latam_documents_domain()
if self.journal_id.company_id.account_fiscal_country_id.code == "AR":
letters = self.journal_id._get_journal_letter(counterpart_partner=self.partner_id.commercial_partner_id)
domain += ['|', ('l10n_ar_letter', '=', False), ('l10n_ar_letter', 'in', letters)]
codes = self.journal_id._get_journal_codes()
if codes:
domain.append(('code', 'in', codes))
if self.move_type == 'in_refund':
domain = ['|', ('code', 'in', self._get_l10n_ar_codes_used_for_inv_and_ref())] + domain
return domain
def _check_argentinean_invoice_taxes(self):
# check vat on companies thats has it (Responsable inscripto)
for inv in self.filtered(lambda x: x.company_id.l10n_ar_company_requires_vat):
purchase_aliquots = 'not_zero'
# we require a single vat on each invoice line except from some purchase documents
if inv.move_type in ['in_invoice', 'in_refund'] and inv.l10n_latam_document_type_id.purchase_aliquots == 'zero':
purchase_aliquots = 'zero'
for line in inv.mapped('invoice_line_ids').filtered(lambda x: x.display_type not in ('line_section', 'line_note')):
vat_taxes = line.tax_ids.filtered(lambda x: x.tax_group_id.l10n_ar_vat_afip_code)
if len(vat_taxes) != 1:
raise UserError(_('There should be a single tax from the "VAT" tax group per line, add it to "%s". If you already have it, please check the tax configuration, in advanced options, in the corresponding field "Tax Group".') % line.name)
elif purchase_aliquots == 'zero' and vat_taxes.tax_group_id.l10n_ar_vat_afip_code != '0':
raise UserError(_('On invoice id "%s" you must use VAT Not Applicable on every line.') % inv.id)
elif purchase_aliquots == 'not_zero' and vat_taxes.tax_group_id.l10n_ar_vat_afip_code == '0':
raise UserError(_('On invoice id "%s" you must use VAT taxes different than VAT Not Applicable.') % inv.id)
def _set_afip_service_dates(self):
for rec in self.filtered(lambda m: m.invoice_date and m.l10n_ar_afip_concept in ['2', '3', '4']):
if not rec.l10n_ar_afip_service_start:
rec.l10n_ar_afip_service_start = rec.invoice_date + relativedelta(day=1)
if not rec.l10n_ar_afip_service_end:
rec.l10n_ar_afip_service_end = rec.invoice_date + relativedelta(day=1, days=-1, months=+1)
def _set_afip_responsibility(self):
""" We save the information about the receptor responsability at the time we validate the invoice, this is
necessary because the user can change the responsability after that any time """
for rec in self:
rec.l10n_ar_afip_responsibility_type_id = rec.commercial_partner_id.l10n_ar_afip_responsibility_type_id.id
def _set_afip_rate(self):
""" We set the l10n_ar_currency_rate value with the accounting date. This should be done
after invoice has been posted in order to have the proper accounting date"""
for rec in self:
if rec.company_id.currency_id == rec.currency_id:
rec.l10n_ar_currency_rate = 1.0
elif not rec.l10n_ar_currency_rate:
rec.l10n_ar_currency_rate = self.env['res.currency']._get_conversion_rate(
from_currency=rec.currency_id,
to_currency=rec.company_id.currency_id,
company=rec.company_id,
date=rec.invoice_date,
)
@api.onchange('partner_id')
def _onchange_afip_responsibility(self):
if self.company_id.account_fiscal_country_id.code == 'AR' and self.l10n_latam_use_documents and self.partner_id \
and not self.partner_id.l10n_ar_afip_responsibility_type_id:
return {'warning': {
'title': _('Missing Partner Configuration'),
'message': _('Please configure the AFIP Responsibility for "%s" in order to continue') % (
self.partner_id.name)}}
@api.onchange('partner_id')
def _onchange_partner_journal(self):
""" This method is used when the invoice is created from the sale or subscription """
expo_journals = ['FEERCEL', 'FEEWS', 'FEERCELP']
for rec in self.filtered(lambda x: x.company_id.account_fiscal_country_id.code == "AR" and x.journal_id.type == 'sale'
and x.l10n_latam_use_documents and x.partner_id.l10n_ar_afip_responsibility_type_id):
res_code = rec.partner_id.l10n_ar_afip_responsibility_type_id.code
domain = [('company_id', '=', rec.company_id.id), ('l10n_latam_use_documents', '=', True), ('type', '=', 'sale')]
journal = self.env['account.journal']
msg = False
if res_code in ['9', '10'] and rec.journal_id.l10n_ar_afip_pos_system not in expo_journals:
# if partner is foregin and journal is not of expo, we try to change to expo journal
journal = journal.search(domain + [('l10n_ar_afip_pos_system', 'in', expo_journals)], limit=1)
msg = _('You are trying to create an invoice for foreign partner but you don\'t have an exportation journal')
elif res_code not in ['9', '10'] and rec.journal_id.l10n_ar_afip_pos_system in expo_journals:
# if partner is NOT foregin and journal is for expo, we try to change to local journal
journal = journal.search(domain + [('l10n_ar_afip_pos_system', 'not in', expo_journals)], limit=1)
msg = _('You are trying to create an invoice for domestic partner but you don\'t have a domestic market journal')
if journal:
rec.journal_id = journal.id
elif msg:
# Throw an error to user in order to proper configure the journal for the type of operation
action = self.env.ref('account.action_account_journal_form')
raise RedirectWarning(msg, action.id, _('Go to Journals'))
def _post(self, soft=True):
ar_invoices = self.filtered(lambda x: x.company_id.account_fiscal_country_id.code == "AR" and x.l10n_latam_use_documents)
# We make validations here and not with a constraint because we want validation before sending electronic
# data on l10n_ar_edi
ar_invoices._check_argentinean_invoice_taxes()
posted = super()._post(soft=soft)
posted_ar_invoices = posted & ar_invoices
posted_ar_invoices._set_afip_responsibility()
posted_ar_invoices._set_afip_rate()
posted_ar_invoices._set_afip_service_dates()
return posted
def _reverse_moves(self, default_values_list=None, cancel=False):
if not default_values_list:
default_values_list = [{} for move in self]
for move, default_values in zip(self, default_values_list):
default_values.update({
'l10n_ar_afip_service_start': move.l10n_ar_afip_service_start,
'l10n_ar_afip_service_end': move.l10n_ar_afip_service_end,
})
return super()._reverse_moves(default_values_list=default_values_list, cancel=cancel)
@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()
to_review = self.filtered(lambda x: (
x.journal_id.type == 'sale'
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)
and x.l10n_latam_document_type_id.country_id.code == 'AR'
))
for rec in to_review:
number = rec.l10n_latam_document_type_id._format_document_number(rec.l10n_latam_document_number)
current_pos = int(number.split("-")[0])
if current_pos != rec.journal_id.l10n_ar_afip_pos_number:
invoices = self.search([('journal_id', '=', rec.journal_id.id), ('posted_before', '=', True)], limit=1)
# If there is no posted before invoices the user can change the POS number (x.l10n_latam_document_number)
if (not invoices):
rec.journal_id.l10n_ar_afip_pos_number = current_pos
rec.journal_id._onchange_set_short_name()
# If not, avoid that the user change the POS number
else:
raise UserError(_('The document number can not be changed for this journal, you can only modify'
' the POS number if there is not posted (or posted before) invoices'))
def _get_formatted_sequence(self, number=0):
return "%s %05d-%08d" % (self.l10n_latam_document_type_id.doc_code_prefix,
self.journal_id.l10n_ar_afip_pos_number, 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.account_fiscal_country_id.code == "AR":
if self.l10n_latam_document_type_id:
return self._get_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.company_id.account_fiscal_country_id.code == "AR" 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 _l10n_ar_get_amounts(self, company_currency=False):
""" Method used to prepare data to present amounts and taxes related amounts when creating an
electronic invoice for argentinean and the txt files for digital VAT books. Only take into account the argentinean taxes """
self.ensure_one()
amount_field = company_currency and 'balance' or 'amount_currency'
# if we use balance we need to correct sign (on price_subtotal is positive for refunds and invoices)
sign = -1 if self.is_inbound() else 1
# if we are on a document that works invoice and refund and it's a refund, we need to export it as negative
sign = -sign if self.move_type in ('out_refund', 'in_refund') and\
self.l10n_latam_document_type_id.code in self._get_l10n_ar_codes_used_for_inv_and_ref() else sign
tax_lines = self.line_ids.filtered('tax_line_id')
vat_taxes = tax_lines.filtered(lambda r: r.tax_line_id.tax_group_id.l10n_ar_vat_afip_code)
vat_taxable = self.env['account.move.line']
for line in self.invoice_line_ids:
if any(tax.tax_group_id.l10n_ar_vat_afip_code and tax.tax_group_id.l10n_ar_vat_afip_code not in ['0', '1', '2'] for tax in line.tax_ids):
vat_taxable |= line
profits_tax_group = self.env.ref('l10n_ar.tax_group_percepcion_ganancias')
return {'vat_amount': sign * sum(vat_taxes.mapped(amount_field)),
# For invoices of letter C should not pass VAT
'vat_taxable_amount': sign * sum(vat_taxable.mapped(amount_field)) if self.l10n_latam_document_type_id.l10n_ar_letter != 'C' else self.amount_untaxed,
'vat_exempt_base_amount': sign * sum(self.invoice_line_ids.filtered(lambda x: x.tax_ids.filtered(lambda y: y.tax_group_id.l10n_ar_vat_afip_code == '2')).mapped(amount_field)),
'vat_untaxed_base_amount': sign * sum(self.invoice_line_ids.filtered(lambda x: x.tax_ids.filtered(lambda y: y.tax_group_id.l10n_ar_vat_afip_code == '1')).mapped(amount_field)),
# used on FE
'not_vat_taxes_amount': sign * sum((tax_lines - vat_taxes).mapped(amount_field)),
# used on BFE + TXT
'iibb_perc_amount': sign * sum(tax_lines.filtered(lambda r: r.tax_line_id.tax_group_id.l10n_ar_tribute_afip_code == '07').mapped(amount_field)),
'mun_perc_amount': sign * sum(tax_lines.filtered(lambda r: r.tax_line_id.tax_group_id.l10n_ar_tribute_afip_code == '08').mapped(amount_field)),
'intern_tax_amount': sign * sum(tax_lines.filtered(lambda r: r.tax_line_id.tax_group_id.l10n_ar_tribute_afip_code == '04').mapped(amount_field)),
'other_taxes_amount': sign * sum(tax_lines.filtered(lambda r: r.tax_line_id.tax_group_id.l10n_ar_tribute_afip_code == '99').mapped(amount_field)),
'profits_perc_amount': sign * sum(tax_lines.filtered(lambda r: r.tax_line_id.tax_group_id == profits_tax_group).mapped(amount_field)),
'vat_perc_amount': sign * sum(tax_lines.filtered(lambda r: r.tax_line_id.tax_group_id.l10n_ar_tribute_afip_code == '06').mapped(amount_field)),
'other_perc_amount': sign * sum(tax_lines.filtered(lambda r: r.tax_line_id.tax_group_id.l10n_ar_tribute_afip_code == '09' and r.tax_line_id.tax_group_id != profits_tax_group).mapped(amount_field)),
}
def _get_vat(self):
""" Applies on wsfe web service and in the VAT digital books """
# if we are on a document that works invoice and refund and it's a refund, we need to export it as negative
sign = -1 if self.move_type in ('out_refund', 'in_refund') and\
self.l10n_latam_document_type_id.code in self._get_l10n_ar_codes_used_for_inv_and_ref() else 1
res = []
vat_taxable = self.env['account.move.line']
# get all invoice lines that are vat taxable
for line in self.line_ids:
if any(tax.tax_group_id.l10n_ar_vat_afip_code and tax.tax_group_id.l10n_ar_vat_afip_code not in ['0', '1', '2'] for tax in line.tax_line_id) and line['amount_currency']:
vat_taxable |= line
for tax_group in vat_taxable.mapped('tax_group_id'):
base_imp = sum(self.invoice_line_ids.filtered(lambda x: x.tax_ids.filtered(lambda y: y.tax_group_id.l10n_ar_vat_afip_code == tax_group.l10n_ar_vat_afip_code)).mapped('price_subtotal'))
imp = abs(sum(vat_taxable.filtered(lambda x: x.tax_group_id.l10n_ar_vat_afip_code == tax_group.l10n_ar_vat_afip_code).mapped('amount_currency')))
res += [{'Id': tax_group.l10n_ar_vat_afip_code,
'BaseImp': sign * base_imp,
'Importe': sign * imp}]
# Report vat 0%
vat_base_0 = sum(self.invoice_line_ids.filtered(lambda x: x.tax_ids.filtered(lambda y: y.tax_group_id.l10n_ar_vat_afip_code == '3')).mapped('price_subtotal'))
if vat_base_0:
res += [{'Id': '3', 'BaseImp': vat_base_0, 'Importe': 0.0}]
return res if res else []
def _get_name_invoice_report(self):
self.ensure_one()
if self.l10n_latam_use_documents and self.company_id.account_fiscal_country_id.code == 'AR':
return 'l10n_ar.report_invoice_document'
return super()._get_name_invoice_report()
def _l10n_ar_get_invoice_totals_for_report(self):
"""If the invoice document type indicates that vat should not be detailed in the printed report (result of _l10n_ar_include_vat()) then we overwrite tax_totals field so that includes taxes in the total amount, otherwise it would be showing amount_untaxed in the amount_total"""
self.ensure_one()
include_vat = self._l10n_ar_include_vat()
base_lines = self.line_ids.filtered(lambda x: x.display_type == 'product')
tax_lines = self.line_ids.filtered(lambda x: x.display_type == 'tax')
involved_tax_group_ids = []
for subtotals in self.tax_totals['groups_by_subtotal'].values():
for subtotal in subtotals:
involved_tax_group_ids.append(subtotal['tax_group_id'])
involved_tax_groups = self.env['account.tax.group'].browse(involved_tax_group_ids)
nat_int_tax_groups = involved_tax_groups.filtered(lambda tax_group: tax_group.l10n_ar_tribute_afip_code in ('01', '04'))
vat_tax_groups = involved_tax_groups.filtered('l10n_ar_vat_afip_code')
both_tax_group_ids = nat_int_tax_groups.ids + vat_tax_groups.ids
# Base lines.
base_line_vals_list = [x._convert_to_tax_base_line_dict() for x in base_lines]
if include_vat:
for vals in base_line_vals_list:
vals['taxes'] = vals['taxes']\
.flatten_taxes_hierarchy()\
.filtered(lambda tax: tax.tax_group_id.id not in both_tax_group_ids)
# Tax lines.
tax_line_vals_list = [x._convert_to_tax_line_dict() for x in tax_lines]
if include_vat:
tax_line_vals_list = [
x
for x in tax_line_vals_list
if x['tax_repartition_line'].tax_id.tax_group_id.id not in both_tax_group_ids
]
tax_totals = self.env['account.tax']._prepare_tax_totals(
base_line_vals_list,
self.currency_id,
tax_lines=tax_line_vals_list,
)
temp = self.tax_totals
if include_vat:
tax_totals['amount_total'] = temp['amount_total']
tax_totals['formatted_amount_total'] = temp['formatted_amount_total']
# RG 5614/2024: Show ARCA VAT and Other National Internal Taxes
if self.l10n_latam_document_type_id.code in ['6', '7', '8']:
# Prepare the subtotals to show in the report
currency_symbol = self.currency_id.symbol
detail_info = {
'vat_taxes': {'name': _("VAT Content %s", currency_symbol), 'tax_amount': 0.0, 'group': 'vat'},
'other_taxes': {'name': _("Other National Ind. Taxes %s", currency_symbol), 'tax_amount': 0.0,
'group': 'other'},
}
for subtotals in temp['groups_by_subtotal'].values():
for subtotal in subtotals:
tax_group_id = subtotal['tax_group_id']
if tax_group_id in nat_int_tax_groups.ids:
key = 'other_taxes'
elif tax_group_id in vat_tax_groups.ids:
key = 'vat_taxes'
else:
continue # If not belongs to the needed groups we ignore them
detail_info[key]["tax_amount"] += subtotal['tax_group_amount']
if detail_info['other_taxes']["tax_amount"] == 0.0:
detail_info.pop('other_taxes')
# Format the amounts to show in the report
for _item, values in detail_info.items():
values["formatted_amount_tax"] = formatLang(self.env, values["tax_amount"])
tax_totals["detail_ar_tax"] = list(detail_info.values())
return tax_totals
def _l10n_ar_include_vat(self):
self.ensure_one()
return self.l10n_latam_document_type_id.l10n_ar_letter in ['B', 'C', 'X', 'R']

View file

@ -0,0 +1,31 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class AccountMoveLine(models.Model):
_inherit = 'account.move.line'
def _l10n_ar_prices_and_taxes(self):
self.ensure_one()
invoice = self.move_id
included_taxes = self.tax_ids.filtered('tax_group_id.l10n_ar_vat_afip_code') if self.move_id._l10n_ar_include_vat() else False
price_digits = 10**self.env['decimal.precision'].precision_get('Product Price')
if not included_taxes:
price_unit = self.tax_ids.compute_all(
self.price_unit * price_digits, invoice.currency_id, 1.0, self.product_id, invoice.partner_id)
price_unit = price_unit['total_excluded'] / price_digits
price_subtotal = self.price_subtotal
else:
price_unit = included_taxes.compute_all(
self.price_unit * price_digits, invoice.currency_id, 1.0, self.product_id, invoice.partner_id)['total_included'] / price_digits
price = self.price_unit * (1 - (self.discount or 0.0) / 100.0)
price_subtotal = included_taxes.compute_all(
price, invoice.currency_id, self.quantity, self.product_id, invoice.partner_id)['total_included']
price_net = price_unit * (1 - (self.discount or 0.0) / 100.0)
return {
'price_unit': price_unit,
'price_subtotal': price_subtotal,
'price_net': price_net,
}

View file

@ -0,0 +1,32 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, api
class AccountTaxGroup(models.Model):
_inherit = 'account.tax.group'
# values from http://www.afip.gob.ar/fe/documentos/otros_Tributos.xlsx
l10n_ar_tribute_afip_code = fields.Selection([
('01', '01 - National Taxes'),
('02', '02 - Provincial Taxes'),
('03', '03 - Municipal Taxes'),
('04', '04 - Internal Taxes'),
('06', '06 - VAT perception'),
('07', '07 - IIBB perception'),
('08', '08 - Municipal Taxes Perceptions'),
('09', '09 - Other Perceptions'),
('99', '99 - Others'),
], string='Tribute AFIP Code', index=True, readonly=True)
# values from http://www.afip.gob.ar/fe/documentos/OperacionCondicionIVA.xls
l10n_ar_vat_afip_code = fields.Selection([
('0', 'Not Applicable'),
('1', 'Untaxed'),
('2', 'Exempt'),
('3', '0%'),
('4', '10.5%'),
('5', '21%'),
('6', '27%'),
('8', '5%'),
('9', '2,5%'),
], string='VAT AFIP Code', index=True, readonly=True)

View file

@ -0,0 +1,18 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields
class L10nArAfipResponsibilityType(models.Model):
_name = 'l10n_ar.afip.responsibility.type'
_description = 'AFIP Responsibility Type'
_order = 'sequence'
name = fields.Char(required=True, index='trigram')
sequence = fields.Integer()
code = fields.Char(required=True, index=True)
active = fields.Boolean(default=True)
_sql_constraints = [('name', 'unique(name)', 'Name must be unique!'),
('code', 'unique(code)', 'Code must be unique!')]

View file

@ -0,0 +1,76 @@
from odoo import models, api, fields, _
from odoo.exceptions import UserError
class L10nLatamDocumentType(models.Model):
_inherit = 'l10n_latam.document.type'
l10n_ar_letter = fields.Selection(
selection='_get_l10n_ar_letters',
string='Letters',
help='Letters defined by the AFIP that can be used to identify the'
' documents presented to the government and that depends on the'
' operation type, the responsibility of both the issuer and the'
' receptor of the document')
purchase_aliquots = fields.Selection(
[('not_zero', 'Not Zero'), ('zero', 'Zero')], help='Raise an error if a vendor bill is miss encoded. "Not Zero"'
' means the VAT taxes are required for the invoices related to this document type, and those with "Zero" means'
' that only "VAT Not Applicable" tax is allowed.')
def _get_l10n_ar_letters(self):
""" Return the list of values of the selection field. """
return [
('A', 'A'),
('B', 'B'),
('C', 'C'),
('E', 'E'),
('M', 'M'),
('T', 'T'),
('R', 'R'),
('X', 'X'),
('I', 'I'), # used for mapping of imports
]
def _format_document_number(self, document_number):
""" Make validation of Import Dispatch Number
* making validations on the document_number. If it is wrong it should raise an exception
* format the document_number against a pattern and return it
"""
self.ensure_one()
if self.country_id.code != "AR":
return super()._format_document_number(document_number)
if not document_number:
return False
msg = "'%s' " + _("is not a valid value for") + " '%s'.<br/>%s"
if not self.code:
return document_number
# Import Dispatch Number Validator
if self.code in ['66', '67']:
if len(document_number) != 16:
raise UserError(msg % (document_number, self.name, _('The number of import Dispatch must be 16 characters')))
return document_number
# Invoice Number Validator (For Eg: 123-123)
failed = False
args = document_number.split('-')
if len(args) != 2:
failed = True
else:
pos, number = args
if len(pos) > 5 or not pos.isdigit():
failed = True
elif len(number) > 8 or not number.isdigit():
failed = True
document_number = '{:>05s}-{:>08s}'.format(pos, number)
if failed:
raise UserError(msg % (document_number, self.name, _(
'The document number must be entered with a dash (-) and a maximum of 5 characters for the first part'
'and 8 for the second. The following are examples of valid numbers:\n* 1-1\n* 0001-00000001'
'\n* 00001-00000001')))
return document_number

View file

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

View file

@ -0,0 +1,44 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, api, _
from odoo.exceptions import ValidationError
class ResCompany(models.Model):
_inherit = "res.company"
l10n_ar_gross_income_number = fields.Char(
related='partner_id.l10n_ar_gross_income_number', string='Gross Income Number', readonly=False,
help="This field is required in order to print the invoice report properly")
l10n_ar_gross_income_type = fields.Selection(
related='partner_id.l10n_ar_gross_income_type', string='Gross Income', readonly=False,
help="This field is required in order to print the invoice report properly")
l10n_ar_afip_responsibility_type_id = fields.Many2one(
domain="[('code', 'in', [1, 4, 6])]", related='partner_id.l10n_ar_afip_responsibility_type_id', readonly=False)
l10n_ar_company_requires_vat = fields.Boolean(compute='_compute_l10n_ar_company_requires_vat', string='Company Requires Vat?')
l10n_ar_afip_start_date = fields.Date('Activities Start')
@api.onchange('country_id')
def onchange_country(self):
""" Argentinean companies use round_globally as tax_calculation_rounding_method """
for rec in self.filtered(lambda x: x.country_id.code == "AR"):
rec.tax_calculation_rounding_method = 'round_globally'
@api.depends('l10n_ar_afip_responsibility_type_id')
def _compute_l10n_ar_company_requires_vat(self):
recs_requires_vat = self.filtered(lambda x: x.l10n_ar_afip_responsibility_type_id.code == '1')
recs_requires_vat.l10n_ar_company_requires_vat = True
remaining = self - recs_requires_vat
remaining.l10n_ar_company_requires_vat = False
def _localization_use_documents(self):
""" Argentinean localization use documents """
self.ensure_one()
return self.account_fiscal_country_id.code == "AR" or super()._localization_use_documents()
@api.constrains('l10n_ar_afip_responsibility_type_id')
def _check_accounting_info(self):
""" Do not let to change the AFIP Responsibility of the company if there is already installed a chart of
account and if there has accounting entries """
if self.env['account.chart.template'].existing_accounting(self):
raise ValidationError(_(
'Could not change the AFIP Responsibility of this company because there are already accounting entries.'))

View file

@ -0,0 +1,19 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class ResCountry(models.Model):
_inherit = 'res.country'
l10n_ar_afip_code = fields.Char('AFIP Code', size=3, help='This code will be used on electronic invoice')
l10n_ar_natural_vat = fields.Char(
'Natural Person VAT', size=11, help="Generic VAT number defined by AFIP in order to recognize partners from"
" this country that are natural persons")
l10n_ar_legal_entity_vat = fields.Char(
'Legal Entity VAT', size=11, help="Generic VAT number defined by AFIP in order to recognize partners from this"
" country that are legal entity")
l10n_ar_other_vat = fields.Char(
'Other VAT', size=11, help="Generic VAT number defined by AFIP in order to recognize partners from this"
" country that are not natural persons or legal entities")

View file

@ -0,0 +1,9 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class ResCurrency(models.Model):
_inherit = "res.currency"
l10n_ar_afip_code = fields.Char('AFIP Code', size=4, help='This code will be used on electronic invoice')

View file

@ -0,0 +1,128 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, api, _
from odoo.exceptions import UserError, ValidationError
import stdnum.ar
import re
import logging
_logger = logging.getLogger(__name__)
class ResPartner(models.Model):
_inherit = 'res.partner'
l10n_ar_vat = fields.Char(
compute='_compute_l10n_ar_vat', string="VAT", help='Computed field that returns VAT or nothing if this one'
' is not set for the partner')
l10n_ar_formatted_vat = fields.Char(
compute='_compute_l10n_ar_formatted_vat', string="Formatted VAT", help='Computed field that will convert the'
' given VAT number to the format {person_category:2}-{number:10}-{validation_number:1}')
l10n_ar_gross_income_number = fields.Char('Gross Income Number')
l10n_ar_gross_income_type = fields.Selection(
[('multilateral', 'Multilateral'), ('local', 'Local'), ('exempt', 'Exempt')],
'Gross Income Type', help='Type of gross income: exempt, local, multilateral')
l10n_ar_afip_responsibility_type_id = fields.Many2one(
'l10n_ar.afip.responsibility.type', string='AFIP Responsibility Type', index='btree_not_null', help='Defined by AFIP to'
' identify the type of responsibilities that a person or a legal entity could have and that impacts in the'
' type of operations and requirements they need.')
l10n_ar_special_purchase_document_type_ids = fields.Many2many(
'l10n_latam.document.type', 'res_partner_document_type_rel', 'partner_id', 'document_type_id',
string='Other Purchase Documents', help='Set here if this partner can issue other documents further than'
' invoices, credit notes and debit notes')
@api.depends('l10n_ar_vat')
def _compute_l10n_ar_formatted_vat(self):
""" This will add some dash to the CUIT number (VAT AR) in order to show in his natural format:
{person_category}-{number}-{validation_number} """
recs_ar_vat = self.filtered('l10n_ar_vat')
for rec in recs_ar_vat:
try:
rec.l10n_ar_formatted_vat = stdnum.ar.cuit.format(rec.l10n_ar_vat)
except Exception as error:
rec.l10n_ar_formatted_vat = rec.l10n_ar_vat
_logger.runbot("Argentinean VAT was not formatted: %s", repr(error))
remaining = self - recs_ar_vat
remaining.l10n_ar_formatted_vat = False
@api.depends('vat', 'l10n_latam_identification_type_id')
def _compute_l10n_ar_vat(self):
""" We add this computed field that returns cuit (VAT AR) or nothing if this one is not set for the partner.
This Validation can be also done by calling ensure_vat() method that returns the cuit (VAT AR) or error if this
one is not found """
recs_ar_vat = self.filtered(lambda x: x.l10n_latam_identification_type_id.l10n_ar_afip_code == '80' and x.vat)
for rec in recs_ar_vat:
rec.l10n_ar_vat = stdnum.ar.cuit.compact(rec.vat)
remaining = self - recs_ar_vat
remaining.l10n_ar_vat = False
@api.constrains('vat', 'l10n_latam_identification_type_id')
def check_vat(self):
""" Since we validate more documents than the vat for Argentinean partners (CUIT - VAT AR, CUIL, DNI) we
extend this method in order to process it. """
# NOTE by the moment we include the CUIT (VAT AR) validation also here because we extend the messages
# errors to be more friendly to the user. In a future when Odoo improve the base_vat message errors
# we can change this method and use the base_vat.check_vat_ar method.s
l10n_ar_partners = self.filtered(lambda p: p.l10n_latam_identification_type_id.l10n_ar_afip_code or p.country_code == 'AR')
l10n_ar_partners.l10n_ar_identification_validation()
return super(ResPartner, self - l10n_ar_partners).check_vat()
@api.model
def _commercial_fields(self):
return super()._commercial_fields() + ['l10n_ar_afip_responsibility_type_id']
def ensure_vat(self):
""" This method is a helper that returns the VAT number is this one is defined if not raise an UserError.
VAT is not mandatory field but for some Argentinean operations the VAT is required, for eg validate an
electronic invoice, build a report, etc.
This method can be used to validate is the VAT is proper defined in the partner """
self.ensure_one()
if not self.l10n_ar_vat:
raise UserError(_('No VAT configured for partner [%i] %s') % (self.id, self.name))
return self.l10n_ar_vat
def _get_validation_module(self):
self.ensure_one()
if self.l10n_latam_identification_type_id.l10n_ar_afip_code in ['80', '86']:
return stdnum.ar.cuit
elif self.l10n_latam_identification_type_id.l10n_ar_afip_code == '96':
return stdnum.ar.dni
def l10n_ar_identification_validation(self):
for rec in self.filtered('vat'):
try:
module = rec._get_validation_module()
except Exception as error:
module = False
_logger.runbot("Argentinean document was not validated: %s", repr(error))
if not module:
continue
try:
module.validate(rec.vat)
except module.InvalidChecksum:
raise ValidationError(_('The validation digit is not valid for "%s"',
rec.l10n_latam_identification_type_id.name))
except module.InvalidLength:
raise ValidationError(_('Invalid length for "%s"', rec.l10n_latam_identification_type_id.name))
except module.InvalidFormat:
raise ValidationError(_('Only numbers allowed for "%s"', rec.l10n_latam_identification_type_id.name))
except Exception as error:
raise ValidationError(repr(error))
def _get_id_number_sanitize(self):
""" Sanitize the identification number. Return the digits/integer value of the identification number
If not vat number defined return 0 """
self.ensure_one()
if not self.vat:
return 0
if self.l10n_latam_identification_type_id.l10n_ar_afip_code in ['80', '86']:
# Compact is the number clean up, remove all separators leave only digits
res = int(stdnum.ar.cuit.compact(self.vat))
else:
id_number = re.sub('[^0-9]', '', self.vat)
res = int(id_number)
return res

View file

@ -0,0 +1,49 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, api, _
from odoo.exceptions import ValidationError
import logging
_logger = logging.getLogger(__name__)
try:
from stdnum.ar.cbu import validate as validate_cbu
except ImportError:
import stdnum
_logger.warning("stdnum.ar.cbu is avalaible from stdnum >= 1.6. The one installed is %s" % stdnum.__version__)
def validate_cbu(number):
def _check_digit(number):
"""Calculate the check digit."""
weights = (3, 1, 7, 9)
check = sum(int(n) * weights[i % 4] for i, n in enumerate(reversed(number)))
return str((10 - check) % 10)
number = stdnum.util.clean(number, ' -').strip()
if len(number) != 22:
raise ValidationError(_('Invalid Length'))
if not number.isdigit():
raise ValidationError(_('Invalid Format'))
if _check_digit(number[:7]) != number[7]:
raise ValidationError(_('Invalid Checksum'))
if _check_digit(number[8:-1]) != number[-1]:
raise ValidationError(_('Invalid Checksum'))
return number
class ResPartnerBank(models.Model):
_inherit = 'res.partner.bank'
@api.model
def _get_supported_account_types(self):
""" Add new account type named cbu used in Argentina """
res = super()._get_supported_account_types()
res.append(('cbu', _('CBU')))
return res
@api.model
def retrieve_acc_type(self, acc_number):
try:
validate_cbu(acc_number)
except Exception:
return super().retrieve_acc_type(acc_number)
return 'cbu'

View file

@ -0,0 +1,9 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class Uom(models.Model):
_inherit = 'uom.uom'
l10n_ar_afip_code = fields.Char('AFIP Code', help='This code will be used on electronic invoice')