diff --git a/addons/l10n_ar/models/account_journal.py b/addons/l10n_ar/models/account_journal.py index b0af352981b..e15eab7b0ff 100644 --- a/addons/l10n_ar/models/account_journal.py +++ b/addons/l10n_ar/models/account_journal.py @@ -9,7 +9,8 @@ class AccountJournal(models.Model): _inherit = "account.journal" l10n_ar_afip_pos_system = fields.Selection( - selection='_get_l10n_ar_afip_pos_types_selection', string='AFIP POS System') + selection='_get_l10n_ar_afip_pos_types_selection', string='AFIP POS System', + compute='_compute_l10n_ar_afip_pos_system', store=True, readonly=False) 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') @@ -17,6 +18,18 @@ class AccountJournal(models.Model): '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_is_pos = fields.Boolean( + compute="_compute_l10n_ar_is_pos", store=True, readonly=False, string="Is AFIP POS?") + + @api.depends('country_code', 'type', 'l10n_latam_use_documents') + def _compute_l10n_ar_is_pos(self): + for journal in self: + journal.l10n_ar_is_pos = journal.country_code == 'AR' and journal.type == 'sale' and journal.l10n_latam_use_documents + + @api.depends('l10n_ar_is_pos') + def _compute_l10n_ar_afip_pos_system(self): + for journal in self: + journal.l10n_ar_afip_pos_system = journal.l10n_ar_is_pos and journal.l10n_ar_afip_pos_system def _get_l10n_ar_afip_pos_types_selection(self): """ Return the list of values of the selection field. """ @@ -65,18 +78,16 @@ class AccountJournal(models.Model): 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'][ + letters = letters_data['issued' if self.l10n_ar_is_pos else 'received'][ self.company_id.l10n_ar_afip_responsibility_type_id.code] if counterpart_partner: - counterpart_letters = letters_data['issued' if self.type == 'purchase' else 'received'].get( + counterpart_letters = letters_data['issued' if not self.l10n_ar_is_pos else 'received'].get( counterpart_partner.l10n_ar_afip_responsibility_type_id.code, []) letters = list(set(letters) & set(counterpart_letters)) return letters - def _get_journal_codes(self): + def _get_journal_codes_domain(self): self.ensure_one() - if self.type != 'sale': - return [] return self._get_codes_per_journal_type(self.l10n_ar_afip_pos_system) @api.model @@ -87,20 +98,32 @@ class AccountJournal(models.Model): receipt_m_code = ['54'] receipt_codes = ['4', '9', '15'] expo_codes = ['19', '20', '21'] - if afip_pos_system == 'II_IM': + codes_issuer_is_supplier = [ + '23', '24', '25', '26', '27', '28', '33', '43', '45', '46', '48', '58', '60', '61', '150', '151', '157', + '158', '161', '162', '164', '166', '167', '171', '172', '180', '182', '186', '188', '332'] + codes = [] + if (self.type == 'sale' and not self.l10n_ar_is_pos) or (self.type == 'purchase' and afip_pos_system in ['II_IM', 'RLI_RLM']): + codes = codes_issuer_is_supplier + elif self.type == 'purchase' and afip_pos_system == 'RAW_MAW': + # electronic invoices (wsfev1) (intersection between available docs on ws and codes_issuer_is_supplier) + codes = ['60', '61'] + elif self.type == 'purchase': + return [('code', 'not in', codes_issuer_is_supplier)] + elif afip_pos_system == 'II_IM': # pre-printed invoice - return usual_codes + receipt_codes + expo_codes + invoice_m_code + receipt_m_code + codes = usual_codes + receipt_codes + expo_codes + invoice_m_code + receipt_m_code elif afip_pos_system in ['RAW_MAW', 'RLI_RLM']: # electronic/online invoice - return usual_codes + receipt_codes + invoice_m_code + receipt_m_code + mipyme_codes + codes = usual_codes + receipt_codes + invoice_m_code + receipt_m_code + mipyme_codes elif afip_pos_system in ['CPERCEL', 'CPEWS']: # invoice with detail - return usual_codes + invoice_m_code + codes = usual_codes + invoice_m_code elif afip_pos_system in ['BFERCEL', 'BFEWS']: # Bonds invoice - return usual_codes + mipyme_codes + codes = usual_codes + mipyme_codes elif afip_pos_system in ['FEERCEL', 'FEEWS', 'FEERCELP']: - return expo_codes + codes = expo_codes + return [('code', 'in', codes)] @api.constrains('type', 'l10n_ar_afip_pos_system', 'l10n_ar_afip_pos_number', 'l10n_latam_use_documents') def _check_afip_configurations(self): @@ -112,16 +135,22 @@ class AccountJournal(models.Model): _("You can not change the journal's configuration if it already has validated invoices") + ' (' + ', '.join(invoices.mapped('journal_id').mapped('name')) + ')') + @api.constrains('l10n_ar_afip_pos_system') + def _check_afip_pos_system(self): + journals = self.filtered( + lambda j: j.l10n_ar_is_pos and j.type == 'purchase' and + j.l10n_ar_afip_pos_system not in ['II_IM', 'RLI_RLM', 'RAW_MAW']) + if journals: + raise ValidationError("\n".join([_( + "The pos system %s can not be used on a purchase journal (id %s)" + ) % (x.l10n_ar_afip_pos_system, x.id) for x in journals])) + @api.constrains('l10n_ar_afip_pos_number') def _check_afip_pos_number(self): - to_review = self.filtered( - lambda x: x.type == 'sale' and x.l10n_latam_use_documents and - x.company_id.account_fiscal_country_id.code == "AR") - - if to_review.filtered(lambda x: x.l10n_ar_afip_pos_number == 0): + if self.filtered(lambda j: j.l10n_ar_is_pos and j.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): + if self.filtered(lambda j: j.l10n_ar_is_pos and j.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') diff --git a/addons/l10n_ar/models/account_move.py b/addons/l10n_ar/models/account_move.py index ea292383f90..656da797106 100644 --- a/addons/l10n_ar/models/account_move.py +++ b/addons/l10n_ar/models/account_move.py @@ -1,5 +1,6 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import models, fields, api, _ +from odoo.osv import expression from odoo.exceptions import UserError, RedirectWarning, ValidationError from dateutil.relativedelta import relativedelta import logging @@ -34,6 +35,23 @@ class AccountMove(models.Model): 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)]}) + def _is_manual_document_number(self): + """ Document number should be manual input by user when the journal use documents and + + * if sales journal and not a AFIP pos (liquido producto case) + * if purchase journal and not a AFIP pos (regular case of vendor bills) + + All the other cases the number should be automatic set, wiht only one exception, for pre-printed/online AFIP + POS type, the first numeber will be always set manually by the user and then will be computed automatically + from there """ + if self.country_code != 'AR': + return super()._is_manual_document_number() + + # NOTE: There is a corner case where 2 sales documents can have the same number for the same DOC from a + # different vendor, in that case, the user can create a new Sales Liquido Producto Journal + return self.l10n_latam_use_documents and self.journal_id.type in ['purchase', 'sale'] and \ + not self.journal_id.l10n_ar_is_pos + @api.constrains('move_type', 'journal_id') def _check_moves_use_documents(self): """ Do not let to create not invoices entries in journals that use documents """ @@ -99,10 +117,11 @@ class AccountMove(models.Model): 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 = expression.AND([ + domain or [], + self.journal_id._get_journal_codes_domain(), + ]) + if self.move_type in ['out_refund', 'in_refund']: domain = ['|', ('code', 'in', self._get_l10n_ar_codes_used_for_inv_and_ref())] + domain return domain @@ -209,7 +228,7 @@ class AccountMove(models.Model): super()._inverse_l10n_latam_document_number() to_review = self.filtered(lambda x: ( - x.journal_id.type == 'sale' + x.journal_id.l10n_ar_is_pos and x.l10n_latam_document_type_id and x.l10n_latam_document_number and (x.l10n_latam_manual_document_number or not x.highest_name) @@ -308,7 +327,7 @@ class AccountMove(models.Model): # Report vat 0% vat_base_0 = sum(self.invoice_line_ids.filtered(lambda x: x.tax_ids.filtered(lambda y: y.tax_group_id.l10n_ar_vat_afip_code == '3')).mapped('price_subtotal')) if vat_base_0: - res += [{'Id': '3', 'BaseImp': vat_base_0, 'Importe': 0.0}] + res += [{'Id': '3', 'BaseImp': sign * vat_base_0, 'Importe': 0.0}] return res if res else [] diff --git a/addons/l10n_ar/tests/common.py b/addons/l10n_ar/tests/common.py index 07a139a534e..04b9196d35f 100644 --- a/addons/l10n_ar/tests/common.py +++ b/addons/l10n_ar/tests/common.py @@ -628,7 +628,7 @@ class TestAr(AccountTestInvoicingCommon): pos_number = data.get('l10n_ar_afip_pos_number') values = {'name': '%s %s' % (afip_ws.replace('WS', ''), pos_number), 'type': 'sale', - 'code': afip_ws, + 'code': pos_number, 'l10n_ar_afip_pos_system': self._get_afip_pos_system_real_name().get(afip_ws), 'l10n_ar_afip_pos_number': pos_number, 'l10n_latam_use_documents': True, diff --git a/addons/l10n_ar/tests/test_manual.py b/addons/l10n_ar/tests/test_manual.py index faaa61a0bae..d8104eee041 100644 --- a/addons/l10n_ar/tests/test_manual.py +++ b/addons/l10n_ar/tests/test_manual.py @@ -1,6 +1,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import common from odoo.tests import tagged +from odoo.tests.common import Form @tagged('post_install_l10n', '-at_install', 'post_install') @@ -97,3 +98,78 @@ class TestManual(common.TestAr): """ Responsable Inscripto" in USD and VAT 21 """ self._prepare_multicurrency_values() self._post(self.demo_invoices['test_invoice_10']) + + def test_15_liquido_producto_sales(self): + """ Manual Numbering: Sales and not POS (Liquido Producto) """ + + # Verify that the default sales journals ara created as is AFIP POS + self.assertTrue(self.journal.l10n_ar_is_pos) + + # If we create an invoice it will not use manual numbering + invoice = self._create_invoice({'partner': self.partner}) + self.assertFalse(invoice.l10n_latam_manual_document_number) + + # Create a new sale journal that is not AFIP POS + self.journal = self._create_journal('preprinted', data={'l10n_ar_is_pos': False}) + self.assertFalse(self.journal.l10n_ar_is_pos) + + doc_27_lu_a = self.env.ref('l10n_ar.dc_liq_uci_a') + payment_term_id = self.env.ref("account.account_payment_term_end_following_month") + + # 60, 61, 27, 28, 45, 46 + # In this case manual numbering should be True and the latam document numer should be required + with self.assertRaisesRegex(AssertionError, 'l10n_latam_document_number is a required field'): + with Form(self.env['account.move'].with_context(default_move_type='out_invoice')) as invoice_form: + invoice_form.ref = "demo_liquido_producto_1: Vendor bill liquido producto (DOC 186)" + invoice_form.partner_id = self.res_partner_adhoc + invoice_form.invoice_payment_term_id = payment_term_id + invoice_form.journal_id = self.journal + invoice_form.l10n_latam_document_type_id = doc_27_lu_a + invoice = invoice_form.save() + + # Adding the document number will let us to save and validate the number without any problems + with Form(self.env['account.move'].with_context(default_move_type='out_invoice')) as invoice_form: + invoice_form.ref = "demo_liquido_producto_1: Vendor bill liquido producto (DOC 186)" + invoice_form.partner_id = self.res_partner_adhoc + invoice_form.invoice_payment_term_id = payment_term_id + invoice_form.journal_id = self.journal + invoice_form.l10n_latam_document_type_id = doc_27_lu_a + invoice_form.l10n_latam_document_number = "00077-00000077" + invoice = invoice_form.save() + + def test_16_liquido_producto_purchase(self): + """ Manual Numbering: Purchase POS/ NOT POS (Liquido Producto) """ + + # By default purchase journals ar not AFIP POS journal + purchase_not_pos_journal = self.env["account.journal"].search([ + ('type', '=', 'purchase'), ('company_id', '=', self.env.company.id), ('l10n_latam_use_documents', '=', True)]) + self.assertFalse(purchase_not_pos_journal.l10n_ar_is_pos) + + doc_60_lp_a = self.env.ref('l10n_ar.dc_a_cvl') + payment_term_id = self.env.ref("account.account_payment_term_end_following_month") + + with self.assertRaisesRegex(AssertionError, 'l10n_latam_document_number is a required field'): + with Form(self.env['account.move'].with_context(default_move_type='in_invoice')) as bill_form: + bill_form.ref = "demo_liquido_producto_1: Vendor bill liquido producto (DOC 186)" + bill_form.partner_id = self.res_partner_adhoc + bill_form.invoice_payment_term_id = payment_term_id + bill_form.invoice_date = '2023-02-09' + bill_form.journal_id = purchase_not_pos_journal + bill_form.l10n_latam_document_type_id = doc_60_lp_a + bill = bill_form.save() + + # Create a new journal that is an AFIP POS + purchase_pos_journal = self._create_journal('preprinted', data={'type': 'purchase', 'l10n_ar_is_pos': True}) + + with Form(self.env['account.move'].with_context(default_move_type='in_invoice')) as bill_form: + bill_form.ref = "demo_liquido_producto_1: Vendor bill liquido producto (DOC 186)" + bill_form.partner_id = self.res_partner_adhoc + bill_form.invoice_payment_term_id = payment_term_id + bill_form.invoice_date = '2023-02-09' + bill_form.journal_id = purchase_pos_journal + bill_form.l10n_latam_document_type_id = doc_60_lp_a + bill_form.l10n_latam_document_number = "00077-00000077" + bill = bill_form.save() + + # If we create an invoice it will not use manual numbering + self.assertFalse(bill.l10n_latam_manual_document_number) diff --git a/addons/l10n_ar/views/account_journal_view.xml b/addons/l10n_ar/views/account_journal_view.xml index 665cfa13558..3078ae5c082 100644 --- a/addons/l10n_ar/views/account_journal_view.xml +++ b/addons/l10n_ar/views/account_journal_view.xml @@ -7,10 +7,11 @@ + - - - + + +