diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/l10n_ar/models | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/l10n_ar/models')
| -rw-r--r-- | addons/l10n_ar/models/__init__.py | 17 | ||||
| -rw-r--r-- | addons/l10n_ar/models/account_chart_template.py | 75 | ||||
| -rw-r--r-- | addons/l10n_ar/models/account_fiscal_position.py | 47 | ||||
| -rw-r--r-- | addons/l10n_ar/models/account_fiscal_position_template.py | 11 | ||||
| -rw-r--r-- | addons/l10n_ar/models/account_journal.py | 139 | ||||
| -rw-r--r-- | addons/l10n_ar/models/account_move.py | 295 | ||||
| -rw-r--r-- | addons/l10n_ar/models/account_tax_group.py | 32 | ||||
| -rw-r--r-- | addons/l10n_ar/models/l10n_ar_afip_responsibility_type.py | 18 | ||||
| -rw-r--r-- | addons/l10n_ar/models/l10n_latam_document_type.py | 82 | ||||
| -rw-r--r-- | addons/l10n_ar/models/l10n_latam_identification_type.py | 9 | ||||
| -rw-r--r-- | addons/l10n_ar/models/res_company.py | 44 | ||||
| -rw-r--r-- | addons/l10n_ar/models/res_country.py | 19 | ||||
| -rw-r--r-- | addons/l10n_ar/models/res_currency.py | 9 | ||||
| -rw-r--r-- | addons/l10n_ar/models/res_partner.py | 124 | ||||
| -rw-r--r-- | addons/l10n_ar/models/res_partner_bank.py | 49 | ||||
| -rw-r--r-- | addons/l10n_ar/models/uom_uom.py | 9 |
16 files changed, 979 insertions, 0 deletions
diff --git a/addons/l10n_ar/models/__init__.py b/addons/l10n_ar/models/__init__.py new file mode 100644 index 00000000..4f720533 --- /dev/null +++ b/addons/l10n_ar/models/__init__.py @@ -0,0 +1,17 @@ +# 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 diff --git a/addons/l10n_ar/models/account_chart_template.py b/addons/l10n_ar/models/account_chart_template.py new file mode 100644 index 00000000..a5370925 --- /dev/null +++ b/addons/l10n_ar/models/account_chart_template.py @@ -0,0 +1,75 @@ +# 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): + """ If Argentinian chart, we modify the defaults values of 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', + "l10n_ar_share_sequences": True, + "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, sale_tax_rate, purchase_tax_rate, 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 argentinian 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(sale_tax_rate, purchase_tax_rate, 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 diff --git a/addons/l10n_ar/models/account_fiscal_position.py b/addons/l10n_ar/models/account_fiscal_position.py new file mode 100644 index 00000000..db42d2ce --- /dev/null +++ b/addons/l10n_ar/models/account_fiscal_position.py @@ -0,0 +1,47 @@ +# 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_id, delivery_id=None): + """ Take into account the partner afip responsibility in order to auto-detect the fiscal position """ + company = self.env.company + if company.country_id.code == "AR": + PartnerObj = self.env['res.partner'] + partner = PartnerObj.browse(partner_id) + + # if no delivery use invoicing + if delivery_id: + delivery = PartnerObj.browse(delivery_id) + else: + delivery = partner + + # partner manually set fiscal position always win + if delivery.property_account_position_id or partner.property_account_position_id: + return delivery.property_account_position_id or partner.property_account_position_id + domain = [ + ('auto_apply', '=', True), + ('l10n_ar_afip_responsibility_type_ids', '=', self.env['res.partner'].browse( + partner_id).l10n_ar_afip_responsibility_type_id.id), + ('company_id', '=', company.id), + ] + return self.sudo().search(domain, limit=1) + return super().get_fiscal_position(partner_id, delivery_id=delivery_id) + + @api.onchange('l10n_ar_afip_responsibility_type_ids', 'country_group_id', 'country_id', 'zip_from', 'zip_to') + def _onchange_afip_responsibility(self): + if self.company_id.country_id.code == "AR": + if self.l10n_ar_afip_responsibility_type_ids and any(['country_group_id', 'country_id', 'zip_from', 'zip_to']): + return {'warning': { + 'title': _("Warning"), + 'message': _('If use AFIP Responsibility then the country / zip codes will be not take into account'), + }} diff --git a/addons/l10n_ar/models/account_fiscal_position_template.py b/addons/l10n_ar/models/account_fiscal_position_template.py new file mode 100644 index 00000000..8b6e3c0f --- /dev/null +++ b/addons/l10n_ar/models/account_fiscal_position_template.py @@ -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') diff --git a/addons/l10n_ar/models/account_journal.py b/addons/l10n_ar/models/account_journal.py new file mode 100644 index 00000000..113f2f24 --- /dev/null +++ b/addons/l10n_ar/models/account_journal.py @@ -0,0 +1,139 @@ +# 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')]" + ) + l10n_ar_share_sequences = fields.Boolean( + 'Unified Book', help='Use same sequence for documents with the same letter') + + 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'], + '3': [], + '4': ['C'], + '5': [], + '6': ['C', 'E'], + '9': ['I'], + '10': [], + '13': ['C', 'E'], + '99': [] + }, + 'received': { + '1': ['A', 'B', 'C', 'M', 'I'], + '3': ['B', 'C', 'I'], + '4': ['B', 'C', 'I'], + '5': ['B', 'C', 'I'], + '6': ['A', 'B', 'C', 'I'], + '9': ['E'], + '10': ['E'], + '13': ['A', 'B', 'C', 'I'], + '99': ['B', 'C', 'I'] + }, + } + 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() + 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'] + liq_product_codes = ['60', '61'] + if self.type != 'sale': + return [] + elif self.l10n_ar_afip_pos_system == 'II_IM': + # pre-printed invoice + return usual_codes + receipt_codes + expo_codes + invoice_m_code + receipt_m_code + elif self.l10n_ar_afip_pos_system in ['RAW_MAW', 'RLI_RLM']: + # electronic/online invoice + return usual_codes + receipt_codes + invoice_m_code + receipt_m_code + mipyme_codes + liq_product_codes + elif self.l10n_ar_afip_pos_system in ['CPERCEL', 'CPEWS']: + # invoice with detail + return usual_codes + invoice_m_code + elif self.l10n_ar_afip_pos_system in ['BFERCEL', 'BFEWS']: + # Bonds invoice + return usual_codes + mipyme_codes + elif self.l10n_ar_afip_pos_system in ['FEERCEL', 'FEEWS', 'FEERCELP']: + return expo_codes + + @api.constrains('type', 'l10n_ar_afip_pos_system', 'l10n_ar_afip_pos_number', 'l10n_ar_share_sequences', + '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.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.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_system') + def _onchange_l10n_ar_afip_pos_system(self): + """ On 'Pre-printed Invoice' the usual is to share sequences. On other types, do not share """ + self.l10n_ar_share_sequences = bool(self.l10n_ar_afip_pos_system == 'II_IM') + + @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 diff --git a/addons/l10n_ar/models/account_move.py b/addons/l10n_ar/models/account_move.py new file mode 100644 index 00000000..9d185424 --- /dev/null +++ b/addons/l10n_ar/models/account_move.py @@ -0,0 +1,295 @@ +# 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 dateutil.relativedelta import relativedelta +from lxml import etree +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.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 document type 99 from the generic constraint """ + ar_doctype_99 = self.filtered( + lambda x: x.country_code == 'AR' and + x.l10n_latam_document_type_id.code == '99' and + x.move_type in ['out_refund', 'in_refund']) + + super(AccountMove, self - ar_doctype_99)._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.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: not x.display_type) + product_types = set([x.product_id.type for x in invoice_lines if x.product_id]) + consumable = set(['consu', 'product']) + service = set(['service']) + mixed = set(['consu', 'service', 'product']) + # on expo invoice you can mix services and products + expo_invoice = self.l10n_latam_document_type_id.code in ['19', '20', '21'] + + # Default value "product" + afip_concept = '1' + if product_types == service: + afip_concept = '2' + elif product_types - consumable and product_types - service and not expo_invoice: + afip_concept = '3' + return afip_concept + + def _get_l10n_latam_documents_domain(self): + self.ensure_one() + domain = super()._get_l10n_latam_documents_domain() + if self.journal_id.company_id.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', ['99'])] + domain + return domain + + def _check_argentinian_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 must be one and only one VAT tax per line. Check line "%s"', 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) + + @api.onchange('partner_id') + def _onchange_afip_responsibility(self): + if self.company_id.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.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.country_id.code == "AR" and x.l10n_latam_use_documents) + for rec in ar_invoices: + rec.l10n_ar_afip_responsibility_type_id = rec.commercial_partner_id.l10n_ar_afip_responsibility_type_id.id + if rec.company_id.currency_id == rec.currency_id: + l10n_ar_currency_rate = 1.0 + else: + l10n_ar_currency_rate = rec.currency_id._convert( + 1.0, rec.company_id.currency_id, rec.company_id, rec.invoice_date or fields.Date.today(), round=False) + rec.l10n_ar_currency_rate = l10n_ar_currency_rate + + # We make validations here and not with a constraint because we want validation before sending electronic + # data on l10n_ar_edi + ar_invoices._check_argentinian_invoice_taxes() + posted = super()._post(soft) + posted._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') + 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)) + 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.env.company.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.country_id.code == "AR" and self.l10n_latam_use_documents: + if not self.journal_id.l10n_ar_share_sequences: + 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 + elif self.journal_id.l10n_ar_share_sequences: + where_string += " AND l10n_latam_document_type_id in %(l10n_latam_document_type_ids)s" + param['l10n_latam_document_type_ids'] = tuple(self.l10n_latam_document_type_id.search( + [('l10n_ar_letter', '=', self.l10n_latam_document_type_id.l10n_ar_letter)]).ids) + 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 argentinian and the txt files for digital VAT books. Only take into account the argentinian taxes """ + self.ensure_one() + amount_field = company_currency and 'balance' or 'price_subtotal' + # if we use balance we need to correct sign (on price_subtotal is positive for refunds and invoices) + sign = -1 if (company_currency and self.is_inbound()) else 1 + 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, company_currency=False): + """ Applies on wsfe web service and in the VAT digital books """ + amount_field = company_currency and 'balance' or 'price_subtotal' + # if we use balance we need to correct sign (on price_subtotal is positive for refunds and invoices) + sign = -1 if (company_currency and self.is_inbound()) 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_field]: + vat_taxable |= line + for vat in vat_taxable: + 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 == vat.tax_line_id.tax_group_id.l10n_ar_vat_afip_code)).mapped(amount_field)) + res += [{'Id': vat.tax_line_id.tax_group_id.l10n_ar_vat_afip_code, + 'BaseImp': sign * base_imp, + 'Importe': sign * vat[amount_field]}] + + # Report vat 0% + vat_base_0 = sign * 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(amount_field)) + 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.country_id.code == 'AR': + return 'l10n_ar.report_invoice_document' + return super()._get_name_invoice_report() diff --git a/addons/l10n_ar/models/account_tax_group.py b/addons/l10n_ar/models/account_tax_group.py new file mode 100644 index 00000000..f59fcc47 --- /dev/null +++ b/addons/l10n_ar/models/account_tax_group.py @@ -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) diff --git a/addons/l10n_ar/models/l10n_ar_afip_responsibility_type.py b/addons/l10n_ar/models/l10n_ar_afip_responsibility_type.py new file mode 100644 index 00000000..1ce42cca --- /dev/null +++ b/addons/l10n_ar/models/l10n_ar_afip_responsibility_type.py @@ -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=True) + 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!')] diff --git a/addons/l10n_ar/models/l10n_latam_document_type.py b/addons/l10n_ar/models/l10n_latam_document_type.py new file mode 100644 index 00000000..5e15ad77 --- /dev/null +++ b/addons/l10n_ar/models/l10n_latam_document_type.py @@ -0,0 +1,82 @@ +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 _filter_taxes_included(self, taxes): + """ In argentina we include taxes depending on document letter """ + self.ensure_one() + if self.country_id.code == "AR" and self.l10n_ar_letter in ['B', 'C', 'X', 'R']: + return taxes.filtered(lambda x: x.tax_group_id.l10n_ar_vat_afip_code) + return super()._filter_taxes_included(taxes) + + 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 diff --git a/addons/l10n_ar/models/l10n_latam_identification_type.py b/addons/l10n_ar/models/l10n_latam_identification_type.py new file mode 100644 index 00000000..7158fe2f --- /dev/null +++ b/addons/l10n_ar/models/l10n_latam_identification_type.py @@ -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") diff --git a/addons/l10n_ar/models/res_company.py b/addons/l10n_ar/models/res_company.py new file mode 100644 index 00000000..62fe7563 --- /dev/null +++ b/addons/l10n_ar/models/res_company.py @@ -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): + """ Argentinian 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): + """ Argentinian localization use documents """ + self.ensure_one() + return True if self.country_id.code == "AR" else 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.')) diff --git a/addons/l10n_ar/models/res_country.py b/addons/l10n_ar/models/res_country.py new file mode 100644 index 00000000..15c1aaf6 --- /dev/null +++ b/addons/l10n_ar/models/res_country.py @@ -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") diff --git a/addons/l10n_ar/models/res_currency.py b/addons/l10n_ar/models/res_currency.py new file mode 100644 index 00000000..24ef449c --- /dev/null +++ b/addons/l10n_ar/models/res_currency.py @@ -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') diff --git a/addons/l10n_ar/models/res_partner.py b/addons/l10n_ar/models/res_partner.py new file mode 100644 index 00000000..e7b089c8 --- /dev/null +++ b/addons/l10n_ar/models/res_partner.py @@ -0,0 +1,124 @@ +# 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=True, 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("Argentinian 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 Argentinian 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 x: x.l10n_latam_identification_type_id.l10n_ar_afip_code) + 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 Argentinian 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("Argentinian 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)) + + @api.model + def _get_id_number_sanitize(self): + """ Sanitize the identification number. Return the digits/integer value of the identification number """ + 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 diff --git a/addons/l10n_ar/models/res_partner_bank.py b/addons/l10n_ar/models/res_partner_bank.py new file mode 100644 index 00000000..5f5104c3 --- /dev/null +++ b/addons/l10n_ar/models/res_partner_bank.py @@ -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' diff --git a/addons/l10n_ar/models/uom_uom.py b/addons/l10n_ar/models/uom_uom.py new file mode 100644 index 00000000..6b865563 --- /dev/null +++ b/addons/l10n_ar/models/uom_uom.py @@ -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') |
