Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[12.0] l10n_it_reverse_charge supporting "with_supplier_self_invoice" for e-invoices + link supplier self invoice to e-invoice #2790

Merged
merged 8 commits into from
Jul 19, 2022
16 changes: 16 additions & 0 deletions l10n_it_fatturapa_in/models/attachment.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ class FatturaPAAttachmentIn(models.Model):
is_self_invoice = fields.Boolean(
"Contains self invoices", compute="_compute_is_self_invoice", store=True
)
linked_invoice_id_xml = fields.Char(
compute="_compute_linked_invoice_id_xml", store=True)

_sql_constraints = [(
'ftpa_attachment_in_name_uniq',
Expand Down Expand Up @@ -134,6 +136,20 @@ def _compute_is_self_invoice(self):
att.is_self_invoice = True
break

@api.multi
@api.depends('ir_attachment_id.datas')
def _compute_linked_invoice_id_xml(self):
for att in self:
att.linked_invoice_id_xml = ""
fatt = att.get_invoice_obj()
if fatt:
for invoice_body in fatt.FatturaElettronicaBody:
if len(invoice_body.DatiGenerali.DatiFattureCollegate) == 1:
att.linked_invoice_id_xml = (
invoice_body.DatiGenerali.DatiFattureCollegate[0].
IdDocumento
)

@api.multi
@api.depends('ir_attachment_id.datas')
def _compute_e_invoice_parsing_error(self):
Expand Down
3 changes: 2 additions & 1 deletion l10n_it_fatturapa_in/views/account_view.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
</xpath>
<field name="ftpa_preview_link" position="after">
<button type="object" name="recompute_xml_fields" string="Recompute XML fields" icon="fa-refresh"
attrs="{'invisible': [('invoices_number', '!=', 0), ('xml_supplier_id', '!=', False), ('invoices_date', '!=', False)]}"/>
attrs="{'invisible': [('invoices_number', '!=', 0), ('xml_supplier_id', '!=', False), ('invoices_date', '!=', False), ('linked_invoice_id_xml', '!=', False)]}"/>
</field>
<group name="preview" position="after">
<group name="e_bill_data">
Expand All @@ -32,6 +32,7 @@
<field name="registered"/>
<field name="invoices_total"/>
<field name="invoices_date"/>
<field name="linked_invoice_id_xml"/>
</group>
</group>
<notebook position="inside">
Expand Down
1 change: 1 addition & 0 deletions l10n_it_fatturapa_in_rc/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from . import account_rc_type
from . import account_invoice
from . import attachment
24 changes: 24 additions & 0 deletions l10n_it_fatturapa_in_rc/models/attachment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from odoo import models, api


class Attachment(models.Model):
_inherit = "fatturapa.attachment.in"

@api.model
def create(self, vals):
attachments = super(Attachment, self).create(vals)
rc_invoices = self.env["account.invoice"].search(
[
("type", "in", ("in_invoice", "in_refund")),
("rc_self_invoice_id", "!=", False),
("fatturapa_attachment_in_id", "=", False),
],
)
for attachment in attachments:
if attachment.linked_invoice_id_xml and attachment.is_self_invoice:
rc_invoice = rc_invoices.filtered(
lambda i: i.number == attachment.linked_invoice_id_xml
)
if len(rc_invoice) == 1:
rc_invoice.fatturapa_attachment_in_id = attachment
return attachments
2 changes: 1 addition & 1 deletion l10n_it_fatturapa_out_rc/wizard/wizard_export_e_invoice.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def exportInvoiceXML(
"select invoices exclusively of type 'TD17', 'TD18', 'TD19' "
"or exclusively of other types."
))
rc_suppliers = invoices.mapped("rc_purchase_invoice_id.partner_id")
rc_suppliers = invoices._get_original_suppliers()
if len(rc_suppliers) > 1:
raise UserError(_(
"Selected reverse charge invoices have different suppliers. Please "
Expand Down
56 changes: 40 additions & 16 deletions l10n_it_reverse_charge/models/account_invoice.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# Copyright 2017 Alex Comba - Agile Business Group
# Copyright 2017 Lorenzo Battistini - Agile Business Group
# Copyright 2017 Marco Calcagni - Dinamiche Aziendali srl
# Copyright 2022 Simone Rubino - TAKOBI

from odoo import api, fields, models
from odoo.exceptions import Warning as UserError
Expand Down Expand Up @@ -58,6 +59,20 @@ def _onchange_partner_id(self):
self.onchange_rc_fiscal_position_id()
return res

@api.multi
def _get_original_suppliers(self):
rc_purchase_invoices = self.mapped("rc_purchase_invoice_id")
supplier_invoices = self.env["account.invoice"]
for rc_purchase_invoice in rc_purchase_invoices:
current_supplier_invoices = self.search([
("rc_self_purchase_invoice_id", "=", rc_purchase_invoice.id)
])
if current_supplier_invoices:
supplier_invoices |= current_supplier_invoices
else:
supplier_invoices |= rc_purchase_invoice
return supplier_invoices.mapped("partner_id")

def rc_inv_line_vals(self, line):
return {
'product_id': line.product_id.id,
Expand All @@ -73,14 +88,20 @@ def rc_inv_vals(self, partner, account, rc_type, lines, currency):
type = 'out_invoice'
else:
type = 'out_refund'
supplier = self.partner_id
original_invoice = self.search([
("rc_self_purchase_invoice_id", "=", self.id)
], limit=1)
if original_invoice:
supplier = original_invoice.partner_id

comment = _(
"Reverse charge self invoice.\n"
"Supplier: %s\n"
"Reference: %s\n"
"Date: %s\n"
"Internal reference: %s") % (
self.partner_id.display_name, self.reference or '', self.date,
supplier.display_name, self.reference or '', self.date,
self.number
)
return {
Expand Down Expand Up @@ -328,17 +349,14 @@ def generate_self_invoice(self):
raise UserError(_(
"Invoice %s, line\n%s\nis RC but has not tax"
) % ((self.reference or self.partner_id.display_name), line.name))
tax_ids = list()
for tax_mapping in rc_type.tax_ids:
for line_tax_id in line_tax_ids:
if tax_mapping.purchase_tax_id == line_tax_id:
tax_ids.append(tax_mapping.sale_tax_id.id)
if not tax_ids:
raise UserError(_("Tax code used is not a RC tax.\nCan't "
"find tax mapping"))
if line_tax_ids:
mapped_taxes = rc_type.map_tax(
line_tax_ids,
'purchase_tax_id',
'sale_tax_id',
)
if line_tax_ids and mapped_taxes:
rc_invoice_line['invoice_line_tax_ids'] = [
(6, False, tax_ids)]
(6, False, mapped_taxes.ids)]
rc_invoice_line[
'account_id'] = rc_type.transitory_account_id.id
rc_invoice_lines.append([0, False, rc_invoice_line])
Expand Down Expand Up @@ -370,9 +388,6 @@ def generate_self_invoice(self):

def generate_supplier_self_invoice(self):
rc_type = self.fiscal_position_id.rc_type_id
if not len(rc_type.tax_ids) == 1:
raise UserError(_(
"Can't find 1 tax mapping for %s" % rc_type.name))
if not self.rc_self_purchase_invoice_id:
supplier_invoice = self.copy()
else:
Expand All @@ -390,8 +405,17 @@ def generate_supplier_self_invoice(self):
supplier_invoice.partner_id = rc_type.partner_id.id
supplier_invoice.journal_id = rc_type.supplier_journal_id.id
for inv_line in supplier_invoice.invoice_line_ids:
inv_line.invoice_line_tax_ids = [
(6, 0, [rc_type.tax_ids[0].purchase_tax_id.id])]
line_tax_ids = inv_line.invoice_line_tax_ids
mapped_taxes = rc_type.map_tax(
line_tax_ids,
'original_purchase_tax_id',
'purchase_tax_id',
)
if line_tax_ids and mapped_taxes:
inv_line.invoice_line_tax_ids = [
(6, False, mapped_taxes.ids),
]

inv_line.account_id = rc_type.transitory_account_id.id
self.rc_self_purchase_invoice_id = supplier_invoice.id

Expand Down
86 changes: 73 additions & 13 deletions l10n_it_reverse_charge/models/account_rc_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
# Copyright 2017 Alex Comba - Agile Business Group
# Copyright 2017 Lorenzo Battistini - Agile Business Group
# Copyright 2017 Marco Calcagni - Dinamiche Aziendali srl
# Copyright 2022 Simone Rubino - TAKOBI

from odoo import fields, models, _, api
from odoo.exceptions import ValidationError
from odoo import _, api, fields, models
from odoo.exceptions import UserError, ValidationError


class AccountRCTypeTax(models.Model):
Expand All @@ -16,6 +17,10 @@ class AccountRCTypeTax(models.Model):
string='RC type',
required=True,
ondelete='cascade')
original_purchase_tax_id = fields.Many2one(
'account.tax',
string='Original Purchase Tax',
required=False)
purchase_tax_id = fields.Many2one(
'account.tax',
string='Purchase Tax',
Expand All @@ -30,9 +35,33 @@ class AccountRCTypeTax(models.Model):
_sql_constraints = [
('purchase_sale_tax_uniq',
'unique (rc_type_id,purchase_tax_id,sale_tax_id)',
'Tax mappings can be defined only once per rc type.')
'Tax mappings from Purchase Tax to Sale Tax '
'can be defined only once per Reverse Charge Type.'),
('original_purchase_sale_tax_uniq',
'unique (rc_type_id,'
'original_purchase_tax_id,purchase_tax_id,sale_tax_id)',
'Tax mappings from Original Purchase Tax to Purchase Tax to Sale Tax '
'can be defined only once per Reverse Charge Type.'),
]

@api.constrains(
'original_purchase_tax_id',
'rc_type_id',
)
def _constrain_supplier_self_invoice_mapping(self):
for mapping in self:
rc_type = mapping.rc_type_id
if rc_type.with_supplier_self_invoice:
if not mapping.original_purchase_tax_id:
raise ValidationError(
_("Original Purchase Tax is required "
"for Reverse Charge Type {rc_type_name} having "
"With additional supplier self invoice enabled")
.format(
rc_type_name=rc_type.display_name,
)
)


class AccountRCType(models.Model):
_name = 'account.rc.type'
Expand Down Expand Up @@ -86,13 +115,44 @@ class AccountRCType(models.Model):
'res.company', string='Company', required=True,
default=lambda self: self.env.user.company_id)

@api.multi
@api.constrains('with_supplier_self_invoice', 'tax_ids')
def _check_tax_ids(self):
for rctype in self:
if rctype.with_supplier_self_invoice and len(rctype.tax_ids) > 1:
raise ValidationError(_(
'When "With additional supplier self invoice" you must set'
' only one tax mapping line: only 1 tax per invoice is '
'supported'
))
@api.constrains(
'with_supplier_self_invoice',
'tax_ids',
)
def _constrain_with_supplier_self_invoice(self):
with_supplier_types = self.filtered('with_supplier_self_invoice')
if with_supplier_types:
with_supplier_types.mapped('tax_ids') \
._constrain_supplier_self_invoice_mapping()

def map_tax(self, taxes, key_tax_field, value_tax_field):
"""
Map each tax in `taxes`, based on the mapping defined by `self.tax_ids`.

Raise an exception if a mapping is not found for some of `taxes`.

:param key_tax_field: Field of the mapping lines
to be used as key for searching the tax
:param value_tax_field: Field of the mapping lines
to be used as value for mapping the tax
:param taxes: Taxes to be mapped
"""
self.ensure_one()
mapped_taxes = self.env['account.tax'].browse()
for tax in taxes:
for tax_mapping in self.tax_ids:
if tax_mapping[key_tax_field] == tax:
mapped_taxes |= tax_mapping[value_tax_field]
break
else:
# Tax not found in mapping
raise UserError(
_("Can't find tax mapping for {tax_name} "
"in Reverse Charge Type {rc_type_name}, "
"please check the configuration.")
.format(
tax_name=tax.display_name,
rc_type_name=self.display_name,
)
)
return mapped_taxes
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions l10n_it_reverse_charge/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from . import test_rc
from . import test_account_rc_type
1 change: 1 addition & 0 deletions l10n_it_reverse_charge/tests/rc_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ def _create_rc_type_taxes(self):

self.rc_type_tax_eeu = rc_type_tax_model.create({
'rc_type_id': self.rc_type_eeu.id,
'original_purchase_tax_id': self.tax_0_pur.id,
'purchase_tax_id': self.tax_22ae.id,
'sale_tax_id': self.tax_22ve.id
})
Expand Down
73 changes: 73 additions & 0 deletions l10n_it_reverse_charge/tests/test_account_rc_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Copyright 2022 Simone Rubino - TAKOBI
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from odoo.exceptions import UserError, ValidationError
from odoo.fields import first

from .rc_common import ReverseChargeCommon


class TestAccountRCType(ReverseChargeCommon):

def test_with_supplier_self_invoice(self):
"""
Check that Reverse Charge Types
that require a supplier self invoice,
must have an Original Purchase Tax in the mapping.
"""
rc_type = self.rc_type_eeu
rc_type_mapping = first(rc_type.tax_ids)
self.assertTrue(rc_type.with_supplier_self_invoice)
self.assertTrue(rc_type_mapping.original_purchase_tax_id)

with self.assertRaises(ValidationError) as ve:
rc_type_mapping.original_purchase_tax_id = False
exc_message = ve.exception.args[0]

self.assertIn("Original Purchase Tax is required", exc_message)
self.assertIn(rc_type.display_name, exc_message)

def test_tax_map_find(self):
"""
Check that Reverse Charge Types can map
Original Purchase Tax to Purchase Tax.
"""
rc_type = self.rc_type_eeu
rc_type_mapping = first(rc_type.tax_ids)
key_tax = rc_type_mapping.original_purchase_tax_id

mapped_tax = rc_type.map_tax(
key_tax,
'original_purchase_tax_id',
'purchase_tax_id',
)

value_tax = rc_type_mapping.purchase_tax_id
self.assertEqual(value_tax, mapped_tax)

def test_tax_map_not_found(self):
"""
Check that Reverse Charge Types
raise an Error when can't map a tax.
"""
rc_type = self.rc_type_eeu
key_taxes = rc_type.tax_ids.mapped('original_purchase_tax_id')
other_tax = self.env['account.tax'].search(
[
('id', 'not in', key_taxes.ids),
],
limit=1,
)
self.assertTrue(other_tax)

with self.assertRaises(UserError) as ue:
rc_type.map_tax(
other_tax,
'original_purchase_tax_id',
'purchase_tax_id',
)
exc_message = ue.exception.args[0]

self.assertIn("Can't find tax mapping", exc_message)
self.assertIn(rc_type.display_name, exc_message)
self.assertIn(other_tax.display_name, exc_message)
Loading