[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:
@@ -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')
|
||||
|
||||
@@ -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 []
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user