mirror of
https://github.com/bringout/oca-ocb-l10n_americas.git
synced 2026-04-26 13:32:04 +02:00
Initial commit: L10N_Americas packages
This commit is contained in:
commit
12b27ce151
714 changed files with 79328 additions and 0 deletions
18
odoo-bringout-oca-ocb-l10n_ar/l10n_ar/models/__init__.py
Normal file
18
odoo-bringout-oca-ocb-l10n_ar/l10n_ar/models/__init__.py
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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')
|
||||
141
odoo-bringout-oca-ocb-l10n_ar/l10n_ar/models/account_journal.py
Normal file
141
odoo-bringout-oca-ocb-l10n_ar/l10n_ar/models/account_journal.py
Normal 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
|
||||
411
odoo-bringout-oca-ocb-l10n_ar/l10n_ar/models/account_move.py
Normal file
411
odoo-bringout-oca-ocb-l10n_ar/l10n_ar/models/account_move.py
Normal 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']
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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!')]
|
||||
|
|
@ -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
|
||||
|
|
@ -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")
|
||||
44
odoo-bringout-oca-ocb-l10n_ar/l10n_ar/models/res_company.py
Normal file
44
odoo-bringout-oca-ocb-l10n_ar/l10n_ar/models/res_company.py
Normal 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.'))
|
||||
19
odoo-bringout-oca-ocb-l10n_ar/l10n_ar/models/res_country.py
Normal file
19
odoo-bringout-oca-ocb-l10n_ar/l10n_ar/models/res_country.py
Normal 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")
|
||||
|
|
@ -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')
|
||||
128
odoo-bringout-oca-ocb-l10n_ar/l10n_ar/models/res_partner.py
Normal file
128
odoo-bringout-oca-ocb-l10n_ar/l10n_ar/models/res_partner.py
Normal 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
|
||||
|
|
@ -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'
|
||||
9
odoo-bringout-oca-ocb-l10n_ar/l10n_ar/models/uom_uom.py
Normal file
9
odoo-bringout-oca-ocb-l10n_ar/l10n_ar/models/uom_uom.py
Normal 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')
|
||||
Loading…
Add table
Add a link
Reference in a new issue