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

@ -1,11 +1,12 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import template_ar_ex
from . import template_ar_ri
from . import template_ar_base
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

View file

@ -1,73 +1,67 @@
# 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
from odoo.exceptions import ValidationError
from odoo.addons.account.models.chart_template import template
class AccountChartTemplate(models.Model):
class AccountChartTemplate(models.AbstractModel):
_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
def _get_ar_responsibility_match(self, chart_template):
""" return responsibility type that match with the given chart_template code
"""
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'),
'ar_base': self.env.ref('l10n_ar.res_RM'),
'ar_ex': self.env.ref('l10n_ar.res_IVAE'),
'ar_ri': self.env.ref('l10n_ar.res_IVARI'),
}
return match.get(chart_template_id)
return match.get(chart_template)
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.
def _load(self, template_code, company, install_demo,force_create=True):
""" Set companies ARCA Responsibility and Country if AR CoA is installed, also set tax calculation rounding
method required in order to properly validate match ARCA invoices.
Also, raise a warning if the user is trying to install a CoA that does not match with the defined AFIP
Also, raise a warning if the user is trying to install a CoA that does not match with the defined ARCA
Responsibility defined in the company
"""
self.ensure_one()
coa_responsibility = self._get_ar_responsibility_match(self.id)
coa_responsibility = self._get_ar_responsibility_match(template_code)
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)
current_identification_type = company.partner_id.l10n_latam_identification_type_id
try:
# 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')
except ValidationError:
# put back previous value if we could not validate the CUIT
company.partner_id.l10n_latam_identification_type_id = current_identification_type
res = super()._load(template_code, company, install_demo,force_create)
# 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'):
if template_code in ('ar_base', 'ar_ex'):
company.account_purchase_tax_id = self.env['account.tax']
return res
def try_loading(self, template_code, company, install_demo=False, force_create=True):
# During company creation load template code corresponding to the ARCA Responsibility
if not company:
return
if isinstance(company, int):
company = self.env['res.company'].browse([company])
if company.country_code == 'AR' and not company.chart_template:
match = {
self.env.ref('l10n_ar.res_RM'): 'ar_base',
self.env.ref('l10n_ar.res_IVAE'): 'ar_ex',
self.env.ref('l10n_ar.res_IVARI'): 'ar_ri',
}
template_code = match.get(company.l10n_ar_afip_responsibility_type_id, template_code)
return super().try_loading(template_code, company, install_demo, force_create)

View file

@ -3,24 +3,17 @@ 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 '
string='ARCA Responsibility Types', help='List of ARCA responsibilities where this fiscal position '
'should be auto-detected')
@api.model
def _get_fiscal_position(self, partner, delivery=None):
def _get_fpos_validation_functions(self, partner):
functions = super()._get_fpos_validation_functions(partner)
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
return functions
return [
lambda fpos: partner.l10n_ar_afip_responsibility_type_id in fpos.l10n_ar_afip_responsibility_type_ids,
] + functions

View file

@ -1,11 +0,0 @@
# 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

@ -1,22 +1,39 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, api, _
from odoo.exceptions import ValidationError, RedirectWarning
from odoo.exceptions import UserError, 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')
selection='_get_l10n_ar_afip_pos_types_selection', string='ARCA POS System',
compute='_compute_l10n_ar_afip_pos_system', store=True, readonly=False,
help="Argentina: Specify which type of system will be used to create the electronic invoice. This will depend on the type of invoice to be created.",
)
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')
'ARCA POS Number', help='This is the point of sale number assigned by ARCA 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',
'res.partner', 'ARCA 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')]"
)
l10n_ar_is_pos = fields.Boolean(
compute="_compute_l10n_ar_is_pos", store=True, readonly=False,
string="Is ARCA POS?",
help="Argentina: Specify if this Journal will be used to send electronic invoices to ARCA.",
)
@api.depends('country_code', 'type', 'l10n_latam_use_documents')
def _compute_l10n_ar_is_pos(self):
for journal in self:
journal.l10n_ar_is_pos = journal.country_code == 'AR' and journal.type == 'sale' and journal.l10n_latam_use_documents
@api.depends('l10n_ar_is_pos')
def _compute_l10n_ar_afip_pos_system(self):
for journal in self:
journal.l10n_ar_afip_pos_system = journal.l10n_ar_is_pos and journal.l10n_ar_afip_pos_system
def _get_l10n_ar_afip_pos_types_selection(self):
""" Return the list of values of the selection field. """
@ -27,10 +44,11 @@ class AccountJournal(models.Model):
('FEERCELP', _('Export Voucher - Billing Plus')),
('FEERCEL', _('Export Voucher - Online Invoice')),
('CPERCEL', _('Product Coding - Online Voucher')),
('CF', _('External Fiscal Controller')),
]
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
""" Regarding the ARCA 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
@ -43,7 +61,7 @@ class AccountJournal(models.Model):
'5': [],
'6': ['C', 'E'],
'7': ['B', 'C', 'I'],
'8': ['B', 'C', 'I'],
'8': ['I'],
'9': ['I'],
'10': [],
'13': ['C', 'E'],
@ -66,21 +84,19 @@ class AccountJournal(models.Model):
}
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.')
msg = _('Can not create chart of account until you configure your company ARCA Responsibility and VAT.')
raise RedirectWarning(msg, action.id, _('Go to Companies'))
letters = letters_data['issued' if self.type == 'sale' else 'received'][
letters = letters_data['issued' if self.l10n_ar_is_pos 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_letters = letters_data['issued' if not self.l10n_ar_is_pos 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):
def _get_journal_codes_domain(self):
self.ensure_one()
if self.type != 'sale':
return []
return self._get_codes_per_journal_type(self.l10n_ar_afip_pos_system)
@api.model
@ -91,51 +107,85 @@ class AccountJournal(models.Model):
receipt_m_code = ['54']
receipt_codes = ['4', '9', '15']
expo_codes = ['19', '20', '21']
zeta_codes = ['80', '83']
if afip_pos_system == 'II_IM':
tique_codes = ['81', '82', '83', '110', '112', '113', '115', '116', '118', '119', '120']
lsg_codes = ['331']
no_pos_docs = [
'23', '24', '25', '26', '27', '28', '33', '43', '45', '46', '48', '58', '60', '61', '150', '151', '157',
'158', '161', '162', '164', '166', '167', '171', '172', '180', '182', '186', '188', '332']
codes = []
if (self.type == 'sale' and not self.l10n_ar_is_pos) or (self.type == 'purchase' and afip_pos_system in ['II_IM', 'RLI_RLM']):
codes = no_pos_docs + lsg_codes
elif self.type == 'purchase' and afip_pos_system == 'RAW_MAW':
# electronic invoices (wsfev1) (intersection between available docs on ws and no_pos_docs)
codes = ['60', '61']
elif self.type == 'purchase':
return [('code', 'not in', no_pos_docs)]
elif 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':
codes = usual_codes + receipt_codes + expo_codes + invoice_m_code + receipt_m_code
elif afip_pos_system in ['RAW_MAW', 'RLI_RLM']:
# 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
codes = usual_codes + receipt_codes + invoice_m_code + receipt_m_code + mipyme_codes
elif afip_pos_system in ['CPERCEL', 'CPEWS']:
# invoice with detail
return usual_codes + invoice_m_code
codes = usual_codes + invoice_m_code
elif afip_pos_system in ['BFERCEL', 'BFEWS']:
# Bonds invoice
return usual_codes + mipyme_codes
codes = usual_codes + mipyme_codes
elif afip_pos_system in ['FEERCEL', 'FEEWS', 'FEERCELP']:
return expo_codes
codes = expo_codes
elif afip_pos_system == 'CF':
codes = tique_codes
return [('code', 'in', 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_system')
def _check_afip_pos_system(self):
journals = self.filtered(
lambda j: j.l10n_ar_is_pos and j.type == 'purchase' and
j.l10n_ar_afip_pos_system not in ['II_IM', 'RLI_RLM', 'RAW_MAW'])
if journals:
raise ValidationError("\n".join(
_("The pos system %(system)s can not be used on a purchase journal (id %(id)s)", system=x.l10n_ar_afip_pos_system, id=x.id)
for x in journals
))
@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 self.filtered(lambda j: j.l10n_ar_is_pos and j.l10n_ar_afip_pos_number == 0):
raise ValidationError(_('Please define an ARCA POS number'))
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)'))
if self.filtered(lambda j: j.l10n_ar_is_pos and j.l10n_ar_afip_pos_number > 99999):
raise ValidationError(_('Please define a valid ARCA 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
""" Will define the ARCA 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
def write(self, vals):
protected_fields = ('type', 'l10n_ar_afip_pos_system', 'l10n_ar_afip_pos_number', 'l10n_latam_use_documents')
fields_to_check = [field for field in protected_fields if field in vals]
if fields_to_check:
self.env.cr.execute("SELECT DISTINCT(journal_id) FROM account_move WHERE posted_before = True")
res = self.env.cr.fetchall()
journal_with_entry_ids = [journal_id for journal_id, in res]
for journal in self:
if (
journal.company_id.account_fiscal_country_id.code != "AR"
or journal.type not in ['sale', 'purchase']
or journal.id not in journal_with_entry_ids
):
continue
for field in fields_to_check:
# Wouldn't work if there was a relational field, as we would compare an id with a recordset.
if vals[field] != journal[field]:
raise UserError(_("You can not change %s journal's configuration if it already has validated invoices", journal.name))
return super().write(vals)

View file

@ -1,6 +1,7 @@
# 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.fields import Domain
from odoo.tools.misc import formatLang
from dateutil.relativedelta import relativedelta
import logging
@ -8,7 +9,6 @@ _logger = logging.getLogger(__name__)
class AccountMove(models.Model):
_inherit = 'account.move'
@api.model
@ -21,19 +21,33 @@ class AccountMove(models.Model):
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'
'l10n_ar.afip.responsibility.type', string='ARCA Responsibility Type', help='Defined by ARCA 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)]})
compute='_compute_l10n_ar_afip_concept', selection='_get_afip_invoice_concepts', string="ARCA Concept",
help="A concept is suggested regarding the type of the products on the invoice.")
l10n_ar_afip_service_start = fields.Date(string='ARCA Service Start Date')
l10n_ar_afip_service_end = fields.Date(string='ARCA Service End Date')
def _is_manual_document_number(self):
""" Document number should be manual input by user when the journal use documents and
* if sales journal and not a ARCA pos (liquido producto case)
* if purchase journal and not a ARCA pos (regular case of vendor bills)
All the other cases the number should be automatic set, wiht only one exception, for pre-printed/online ARCA
POS type, the first numeber will be always set manually by the user and then will be computed automatically
from there """
if self.country_code != 'AR':
return super()._is_manual_document_number()
# NOTE: There is a corner case where 2 sales documents can have the same number for the same DOC from a
# different vendor, in that case, the user can create a new Sales Liquido Producto Journal
return self.l10n_latam_use_documents and self.journal_id.type in ['purchase', 'sale'] and \
not self.journal_id.l10n_ar_is_pos
@api.constrains('move_type', 'journal_id')
def _check_moves_use_documents(self):
@ -72,15 +86,15 @@ class AccountMove(models.Model):
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'))
invoice_lines = self.invoice_line_ids.filtered(lambda x: x.display_type not in ('line_section', 'line_subsection', 'line_note'))
product_types = set([x.product_id.type for x in invoice_lines if x.product_id])
consumable = set(['consu', 'product'])
consumable = {'consu'}
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)"
# then ARCA 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'
@ -104,12 +118,11 @@ class AccountMove(models.Model):
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
domain = Domain(domain)
domain &= Domain('l10n_ar_letter', '=', False) | Domain('l10n_ar_letter', 'in', letters)
domain &= Domain(self.journal_id._get_journal_codes_domain())
if self.move_type in ['out_refund', 'in_refund']:
domain = Domain('code', 'in', self._get_l10n_ar_codes_used_for_inv_and_ref()) | domain
return domain
def _check_argentinean_invoice_taxes(self):
@ -120,15 +133,15 @@ class AccountMove(models.Model):
# 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')):
for line in inv.mapped('invoice_line_ids').filtered(lambda x: x.display_type not in ('line_section', 'line_subsection', '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)
raise UserError(_("There should be a single tax from the “VAT“ tax group per line, but this is not the case for line “%s”. Please add a tax to this line or check the tax configuration's advanced options for 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)
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)
raise UserError(_('On invoice id %s” you must use a VAT tax that is not 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']):
@ -143,27 +156,13 @@ class AccountMove(models.Model):
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') % (
'message': _('Please configure the ARCA Responsibility for "%s" in order to continue',
self.partner_id.name)}}
@api.onchange('partner_id')
@ -173,15 +172,19 @@ class AccountMove(models.Model):
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')]
domain = [
*self.env['account.journal']._check_company_domain(rec.company_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
if res_code in ['8', '9', '10'] and rec.journal_id.l10n_ar_afip_pos_system not in expo_journals:
# if it is a foreign partner and journal is not for expo, we try to change it to an 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
elif res_code not in ['8', '9', '10'] and rec.journal_id.l10n_ar_afip_pos_system in expo_journals:
# if it is NOT a foreign partner and journal is for expo, we try to change it to a 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:
@ -191,6 +194,21 @@ class AccountMove(models.Model):
action = self.env.ref('account.action_account_journal_form')
raise RedirectWarning(msg, action.id, _('Go to Journals'))
def _compute_l10n_latam_document_type(self):
"""We correct the default document type in vendor bills in case the partner is foreign (code 8)
so that it is always 'Foreign invoices and receipts'.
"""
super()._compute_l10n_latam_document_type()
foreign_vendor_bills = self.filtered(lambda x: (
x.company_id.account_fiscal_country_id.code == "AR"
and x.state == 'draft'
and x.move_type in ['in_invoice', 'in_refund']
and x.l10n_latam_document_type_id
and x.partner_id.l10n_ar_afip_responsibility_type_id.code == '8'))
doctype_fa_exterior = self.env.ref('l10n_ar.fa_exterior', raise_if_not_found=False)
if doctype_fa_exterior:
foreign_vendor_bills.l10n_latam_document_type_id = doctype_fa_exterior
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
@ -200,7 +218,6 @@ class AccountMove(models.Model):
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
@ -219,7 +236,7 @@ class AccountMove(models.Model):
super()._inverse_l10n_latam_document_number()
to_review = self.filtered(lambda x: (
x.journal_id.type == 'sale'
x.journal_id.l10n_ar_is_pos
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)
@ -246,7 +263,7 @@ class AccountMove(models.Model):
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_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()
@ -278,7 +295,22 @@ class AccountMove(models.Model):
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')
profits_tax_group = self.env['account.chart.template'].with_company(self.company_id).ref(
'tax_group_percepcion_ganancias',
raise_if_not_found=False,
)
if not profits_tax_group:
raise RedirectWarning(
message=_(
"A required tax group could not be found (XML ID: %s).\n"
"Please reload your chart template in order to reinstall the required tax group.\n\n"
"Note: You might have to relink your existing taxes to this new tax group.",
'tax_group_percepcion_ganancias',
),
action=self.env.ref('account.action_account_config').id,
button_text=_("Accounting Settings"),
)
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,
@ -318,7 +350,7 @@ class AccountMove(models.Model):
# 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}]
res += [{'Id': '3', 'BaseImp': sign * vat_base_0, 'Importe': 0.0}]
return res if res else []
@ -331,81 +363,74 @@ class AccountMove(models.Model):
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()
tax_totals = self.tax_totals
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')
if not include_vat:
return tax_totals
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
tax_group_ids = {
tax_group['id']
for subtotal in tax_totals['subtotals']
for tax_group in subtotal['tax_groups']
}
tax_group_ids_to_exclude = self.env['account.tax.group']\
.browse(tax_group_ids)\
.filtered(lambda tax_group: (
self._l10n_ar_is_tax_group_other_national_ind_tax(tax_group)
or self._l10n_ar_is_tax_group_vat(tax_group)
)).ids
if tax_group_ids_to_exclude:
tax_totals = self.env['account.tax']._exclude_tax_groups_from_tax_totals_summary(tax_totals, tax_group_ids_to_exclude)
return tax_totals
# 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)
def _l10n_ar_get_invoice_custom_tax_summary_for_report(self):
""" Get a new tax details for RG 5614/2024 to show ARCA VAT and Other National Internal Taxes. """
if self.l10n_latam_document_type_id.code not in ('6', '7', '8'):
return []
# 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
]
base_lines, _tax_lines = self._get_rounded_base_and_tax_lines()
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'},
def grouping_function(base_line, tax_data):
if not tax_data:
return None
tax_group = tax_data['tax'].tax_group_id
skip = False
name = None
if self._l10n_ar_is_tax_group_other_national_ind_tax(tax_group):
name = _("Other National Ind. Taxes %s", base_line['currency_id'].symbol)
elif self._l10n_ar_is_tax_group_vat(tax_group):
name = _("VAT Content %s", base_line['currency_id'].symbol)
else:
skip = True
return {
'name': name,
'skip': skip,
}
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
AccountTax = self.env['account.tax']
base_lines_aggregated_values = AccountTax._aggregate_base_lines_tax_details(base_lines, grouping_function)
values_per_grouping_key = AccountTax._aggregate_base_lines_aggregated_values(base_lines_aggregated_values)
results = []
for grouping_key, values in values_per_grouping_key.items():
if (
grouping_key
and not grouping_key['skip']
):
results.append({
'name': grouping_key['name'],
'tax_amount_currency': values['tax_amount_currency'],
'formatted_tax_amount_currency': formatLang(self.env, values['tax_amount_currency']),
})
return results
def _l10n_ar_include_vat(self):
self.ensure_one()
return self.l10n_latam_document_type_id.l10n_ar_letter in ['B', 'C', 'X', 'R']
@api.model
def _l10n_ar_is_tax_group_other_national_ind_tax(self, tax_group):
return tax_group.l10n_ar_tribute_afip_code in ('01', '04')
@api.model
def _l10n_ar_is_tax_group_vat(self, tax_group):
return bool(tax_group.l10n_ar_vat_afip_code)

View file

@ -3,29 +3,46 @@ 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
include_vat = invoice._l10n_ar_include_vat()
AccountTax = self.env['account.tax']
base_line = invoice._prepare_product_base_line_for_taxes_computation(self)
if include_vat:
base_line['tax_ids'] = self.tax_ids.filtered('tax_group_id.l10n_ar_vat_afip_code')
AccountTax._add_tax_details_in_base_line(base_line, self.company_id, rounding_method='round_globally')
tax_details = base_line['tax_details']
discount = base_line['discount']
price_unit = base_line['price_unit']
quantity = base_line['quantity']
if include_vat:
raw_total = tax_details['raw_total_included_currency']
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)
raw_total = tax_details['raw_total_excluded_currency']
if discount == 100.0:
price_subtotal_before_discount = price_unit * quantity
else:
price_subtotal_before_discount = raw_total / (1 - discount / 100.0)
if quantity:
price_unit = price_subtotal_before_discount / quantity
price_net = raw_total / quantity
else:
price_unit = 0.0
price_net = 0.0
return {
'price_unit': price_unit,
'price_subtotal': price_subtotal,
'price_subtotal': invoice.currency_id.round(raw_total),
'price_net': price_net,
}
# TODO: deprecated, remove in master
def get_column_to_exclude_for_colspan_calculation(self, taxes=None):
return super().get_column_to_exclude_for_colspan_calculation(taxes)

View file

@ -1,9 +1,9 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, api
from odoo import _, api, fields, models
from odoo.exceptions import UserError
class AccountTaxGroup(models.Model):
_inherit = 'account.tax.group'
# values from http://www.afip.gob.ar/fe/documentos/otros_Tributos.xlsx
@ -17,7 +17,7 @@ class AccountTaxGroup(models.Model):
('08', '08 - Municipal Taxes Perceptions'),
('09', '09 - Other Perceptions'),
('99', '99 - Others'),
], string='Tribute AFIP Code', index=True, readonly=True)
], string='Tribute ARCA 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'),
@ -29,4 +29,22 @@ class AccountTaxGroup(models.Model):
('6', '27%'),
('8', '5%'),
('9', '2,5%'),
], string='VAT AFIP Code', index=True, readonly=True)
], string='VAT ARCA Code', index=True, readonly=True)
@api.ondelete(at_uninstall=False)
def check_uninstall_required(self):
"""
Make sure we don't uninstall a required tax group
"""
ar_companies = self.filtered(lambda g: g.company_id.chart_template.startswith('ar_')).mapped('company_id')
profits_tax_group_ids = self.env['ir.model.data'].search([
('name', 'in', [f'{company.id}_tax_group_percepcion_ganancias' for company in ar_companies]),
('module', '=', 'account'),
]).mapped('res_id')
if profit_tax_groups_to_be_deleted := self.filtered(lambda g: g.id in profits_tax_group_ids):
raise UserError(
_(
"The tax group '%s' can't be removed, since it is required in the Argentinian localization.",
profit_tax_groups_to_be_deleted[0].name,
)
)

View file

@ -3,10 +3,10 @@
from odoo import models, fields
class L10nArAfipResponsibilityType(models.Model):
class L10n_ArAfipResponsibilityType(models.Model):
_name = 'l10n_ar.afip.responsibility.type'
_description = 'AFIP Responsibility Type'
_description = 'ARCA Responsibility Type'
_order = 'sequence'
name = fields.Char(required=True, index='trigram')
@ -14,5 +14,5 @@ class L10nArAfipResponsibilityType(models.Model):
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!')]
_name_uniq = models.Constraint('unique(name)', 'Name must be unique!')
_code_uniq = models.Constraint('unique(code)', 'Code must be unique!')

View file

@ -2,14 +2,13 @@ from odoo import models, api, fields, _
from odoo.exceptions import UserError
class L10nLatamDocumentType(models.Model):
class L10n_LatamDocumentType(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'
help='Letters defined by the ARCA 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')
@ -44,15 +43,19 @@ class L10nLatamDocumentType(models.Model):
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')))
raise UserError(
_(
"%(value)s is not a valid value for %(field)s.\nThe number of import Dispatch must be 16 characters.",
value=document_number,
field=self.name,
),
)
return document_number
# Invoice Number Validator (For Eg: 123-123)
@ -68,9 +71,12 @@ class L10nLatamDocumentType(models.Model):
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')))
raise UserError(
_(
"%(value)s is not a valid value for %(field)s.\nThe 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",
value=document_number,
field=self.name,
),
)
return document_number

View file

@ -2,8 +2,7 @@
from odoo import models, fields
class L10nLatamIdentificationType(models.Model):
class L10n_LatamIdentificationType(models.Model):
_inherit = "l10n_latam.identification.type"
l10n_ar_afip_code = fields.Char("AFIP Code")
l10n_ar_afip_code = fields.Char("ARCA Code")

View file

@ -1,9 +1,9 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, api, _
from odoo.exceptions import ValidationError
from odoo.exceptions import UserError
class ResCompany(models.Model):
_inherit = "res.company"
l10n_ar_gross_income_number = fields.Char(
@ -33,12 +33,15 @@ class ResCompany(models.Model):
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()
return self.chart_template in {'ar_base', 'ar_ex', 'ar_ri'} or 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.'))
def write(self, vals):
if 'l10n_ar_afip_responsibility_type_id' in vals:
for company in self:
if vals['l10n_ar_afip_responsibility_type_id'] != company.l10n_ar_afip_responsibility_type_id.id and company.sudo()._existing_accounting():
raise UserError(_('Could not change the ARCA Responsibility of this company because there are already accounting entries.'))
return super().write(vals)
def _is_latam(self):
return super()._is_latam() or self.country_code == 'AR'

View file

@ -4,16 +4,15 @@ 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_afip_code = fields.Char('ARCA 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"
'Natural Person VAT', size=11, help="Generic VAT number defined by ARCA 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"
'Legal Entity VAT', size=11, help="Generic VAT number defined by ARCA 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"
'Other VAT', size=11, help="Generic VAT number defined by ARCA in order to recognize partners from this"
" country that are not natural persons or legal entities")

View file

@ -3,7 +3,6 @@ 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')
l10n_ar_afip_code = fields.Char('ARCA Code', size=4, help='This code will be used on electronic invoice')

View file

@ -9,7 +9,6 @@ _logger = logging.getLogger(__name__)
class ResPartner(models.Model):
_inherit = 'res.partner'
l10n_ar_vat = fields.Char(
@ -22,15 +21,11 @@ class ResPartner(models.Model):
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')
'Gross Income Type', help='Argentina: 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'
'l10n_ar.afip.responsibility.type', string='ARCA Responsibility Type', index='btree_not_null', help='Defined by ARCA 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):
@ -57,16 +52,20 @@ class ResPartner(models.Model):
remaining = self - recs_ar_vat
remaining.l10n_ar_vat = False
@api.constrains('vat', 'l10n_latam_identification_type_id')
def check_vat(self):
def _run_check_identification(self, validation='error'):
""" 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()
l10n_ar_partners = self.filtered(lambda p: p.vat and (
p.l10n_latam_identification_type_id.l10n_ar_afip_code
or p.country_code == 'AR'
))
for partner in l10n_ar_partners:
if id_number := partner._get_id_number_sanitize():
partner.vat = str(id_number)
if validation == 'error':
partner._l10n_ar_identification_validation()
return super(ResPartner, self - l10n_ar_partners)._run_check_identification(validation=validation)
@api.model
def _commercial_fields(self):
@ -81,9 +80,15 @@ class ResPartner(models.Model):
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))
raise UserError(_('No VAT configured for partner [%i] %s', self.id, self.name))
return self.l10n_ar_vat
def _get_frontend_writable_fields(self):
frontend_writable_fields = super()._get_frontend_writable_fields()
frontend_writable_fields.add('l10n_ar_afip_responsibility_type_id')
return frontend_writable_fields
def _get_validation_module(self):
self.ensure_one()
if self.l10n_latam_identification_type_id.l10n_ar_afip_code in ['80', '86']:
@ -91,7 +96,7 @@ class ResPartner(models.Model):
elif self.l10n_latam_identification_type_id.l10n_ar_afip_code == '96':
return stdnum.ar.dni
def l10n_ar_identification_validation(self):
def _l10n_ar_identification_validation(self):
for rec in self.filtered('vat'):
try:
module = rec._get_validation_module()
@ -110,6 +115,9 @@ class ResPartner(models.Model):
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 module.InvalidComponent:
valid_cuit = ('20', '23', '24', '27', '30', '33', '34', '50', '51', '55')
raise ValidationError(_('CUIT number must be prefixed with one of the following: %s', ', '.join(valid_cuit)))
except Exception as error:
raise ValidationError(repr(error))
@ -124,5 +132,5 @@ class ResPartner(models.Model):
res = int(stdnum.ar.cuit.compact(self.vat))
else:
id_number = re.sub('[^0-9]', '', self.vat)
res = int(id_number)
res = id_number and int(id_number)
return res

View file

@ -1,36 +1,14 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, api, _
from odoo.exceptions import ValidationError
import logging
from stdnum.ar.cbu import validate
from odoo import models, api, _
_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
@ -43,7 +21,7 @@ class ResPartnerBank(models.Model):
@api.model
def retrieve_acc_type(self, acc_number):
try:
validate_cbu(acc_number)
validate(acc_number)
except Exception:
return super().retrieve_acc_type(acc_number)
return 'cbu'

View file

@ -0,0 +1,58 @@
# 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('ar_base')
def _get_ar_base_template_data(self):
return {
'property_account_receivable_id': 'base_deudores_por_ventas',
'property_account_payable_id': 'base_proveedores',
'name': _('Generic Chart of Accounts Argentina Single Taxpayer / Basis'),
'code_digits': '12',
'sequence': 1,
}
@template('ar_base', 'res.company')
def _get_ar_base_res_company(self):
return {
self.env.company.id: {
'account_fiscal_country_id': 'base.ar',
'bank_account_code_prefix': '1.1.1.02.',
'cash_account_code_prefix': '1.1.1.01.',
'transfer_account_code_prefix': '6.0.00.00.',
'account_default_pos_receivable_account_id': 'base_deudores_por_ventas_pos',
'income_currency_exchange_account_id': 'base_diferencias_de_cambio',
'expense_currency_exchange_account_id': 'base_diferencias_de_cambio',
'expense_account_id': 'base_compra_mercaderia',
'income_account_id': 'base_venta_de_mercaderia',
'account_stock_journal_id': 'inventory_valuation',
'account_stock_valuation_id': 'base_mercaderia_reventa',
},
}
@template('ar_base', 'account.journal')
def _get_ar_account_journal(self):
""" In case of an Argentinean CoA, we modify the default values of the sales journal to be a preprinted journal"""
return {
'sale': {
"name": self.env._("Ventas Preimpreso"),
"code": "0001",
"l10n_ar_afip_pos_number": 1,
"l10n_ar_afip_pos_partner_id": self.env.company.partner_id.id,
"l10n_ar_afip_pos_system": 'II_IM',
"refund_sequence": False,
},
}
@template('ar_base', 'account.account')
def _get_ar_base_account_account(self):
return {
'base_mercaderia_reventa': {
'account_stock_expense_id': 'base_compra_mercaderia',
'account_stock_variation_id': 'base_variacion_mercaderia_reventa',
},
}

View file

@ -0,0 +1,30 @@
# 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('ar_ex')
def _get_ar_ex_template_data(self):
return {
'name': _('Argentine Generic Chart of Accounts for Exempt Individuals'),
'parent': 'ar_base',
'code_digits': '12',
'sequence': 2,
}
@template('ar_ex', 'res.company')
def _get_ar_ex_res_company(self):
return {
self.env.company.id: {
'account_fiscal_country_id': 'base.ar',
'bank_account_code_prefix': '1.1.1.02.',
'cash_account_code_prefix': '1.1.1.01.',
'transfer_account_code_prefix': '6.0.00.00.',
'account_default_pos_receivable_account_id': 'base_deudores_por_ventas_pos',
'income_currency_exchange_account_id': 'base_diferencias_de_cambio',
'expense_currency_exchange_account_id': 'base_diferencias_de_cambio',
},
}

View file

@ -0,0 +1,32 @@
# 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('ar_ri')
def _get_ar_ri_template_data(self):
return {
'name': _('Argentine Generic Chart of Accounts for Registered Accountants'),
'parent': 'ar_ex',
'code_digits': '12',
'sequence': 0,
}
@template('ar_ri', 'res.company')
def _get_ar_ri_res_company(self):
return {
self.env.company.id: {
'account_fiscal_country_id': 'base.ar',
'bank_account_code_prefix': '1.1.1.02.',
'cash_account_code_prefix': '1.1.1.01.',
'transfer_account_code_prefix': '6.0.00.00.',
'account_default_pos_receivable_account_id': 'base_deudores_por_ventas_pos',
'income_currency_exchange_account_id': 'base_diferencias_de_cambio',
'expense_currency_exchange_account_id': 'base_diferencias_de_cambio',
'account_sale_tax_id': 'ri_tax_vat_21_ventas',
'account_purchase_tax_id': 'ri_tax_vat_21_compras',
},
}

View file

@ -2,8 +2,7 @@
from odoo import fields, models
class Uom(models.Model):
class UomUom(models.Model):
_inherit = 'uom.uom'
l10n_ar_afip_code = fields.Char('AFIP Code', help='This code will be used on electronic invoice')
l10n_ar_afip_code = fields.Char('Code', help='Argentina: This code will be used on electronic invoice.')