[ADD] l10n_ar: Liquido Product document management

* Add refunds with liquido docs
* Add Is AFIP POS field (l10n_ar_is_pos)
* Adapt letter mapping
* Add unit test

closes odoo/odoo#111076

Related: odoo/enterprise#36322
Signed-off-by: Josse Colpaert <jco@odoo.com>
This commit is contained in:
Katherine Zaoral
2023-03-03 19:10:01 +01:00
parent 44e9930557
commit 17d7b5199f
5 changed files with 153 additions and 28 deletions
+47 -18
View File
@@ -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')
+25 -6
View File
@@ -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 []
+1 -1
View File
@@ -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,
+76
View File
@@ -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)
@@ -7,10 +7,11 @@
<field name="inherit_id" ref="l10n_latam_invoice_document.view_account_journal_form"/>
<field name="arch" type="xml">
<field name="l10n_latam_use_documents" position="after">
<field name="l10n_ar_is_pos" attrs="{'invisible':['|', '|', ('country_code', '!=', 'AR'), ('l10n_latam_use_documents', '=', False), ('type', 'not in', ['sale', 'purchase'])]}"/>
<field name="company_partner" invisible="1"/>
<field name="l10n_ar_afip_pos_system" attrs="{'invisible':['|', '|', ('country_code', '!=', 'AR'), ('l10n_latam_use_documents', '=', False), ('type', '!=', 'sale')], 'required':[('country_code', '=', 'AR'), ('l10n_latam_use_documents', '=', True), ('type', '=', 'sale')]}"/>
<field name="l10n_ar_afip_pos_number" attrs="{'invisible':['|', '|', ('country_code', '!=', 'AR'), ('l10n_latam_use_documents', '=', False), ('type', '!=', 'sale')], 'required':[('country_code', '=', 'AR'), ('l10n_latam_use_documents', '=', True), ('type', '=', 'sale')]}"/>
<field name="l10n_ar_afip_pos_partner_id" attrs="{'invisible':['|', '|', ('country_code', '!=', 'AR'), ('l10n_latam_use_documents', '=', False), ('type', '!=', 'sale')], 'required':[('country_code', '=', 'AR'), ('l10n_latam_use_documents', '=', True), ('type', '=', 'sale')]}"/>
<field name="l10n_ar_afip_pos_system" attrs="{'invisible':[('l10n_ar_is_pos', '=', False)], 'required':[('l10n_ar_is_pos', '=', True)]}"/>
<field name="l10n_ar_afip_pos_number" attrs="{'invisible':[('l10n_ar_is_pos', '=', False)], 'required':[('l10n_ar_is_pos', '=', True)]}"/>
<field name="l10n_ar_afip_pos_partner_id" attrs="{'invisible':[('l10n_ar_is_pos', '=', False)], 'required':[('l10n_ar_is_pos', '=', True)]}"/>
</field>
</field>
</record>