diff --git a/l10n_it_reverse_charge/__manifest__.py b/l10n_it_reverse_charge/__manifest__.py index 44a25832a65f..7df348bcdb1a 100644 --- a/l10n_it_reverse_charge/__manifest__.py +++ b/l10n_it_reverse_charge/__manifest__.py @@ -2,11 +2,12 @@ # Copyright 2017 Alex Comba - Agile Business Group # Copyright 2017 Lorenzo Battistini - Agile Business Group # Copyright 2017 Marco Calcagni - Dinamiche Aziendali srl +# Copyright 2023 Simone Rubino - TAKOBI # License LGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { "name": "ITA - Inversione contabile", - "version": "14.0.1.2.4", + "version": "16.0.1.0.0", "category": "Localization/Italy", "summary": "Inversione contabile", "author": "Odoo Italia Network, Odoo Community Association (OCA)", diff --git a/l10n_it_reverse_charge/data/rc_type.xml b/l10n_it_reverse_charge/data/rc_type.xml index a20ca390cec0..8c1362be7966 100644 --- a/l10n_it_reverse_charge/data/rc_type.xml +++ b/l10n_it_reverse_charge/data/rc_type.xml @@ -1,38 +1,38 @@ - - + + + + Intra-EU (VAT Integration) + integration + Configuration of Intra-EU trade using the VAT Integration method + - - Intra-EU (VAT Integration) - integration - Configuration of Intra-EU trade using the VAT Integration method - + + Extra-EU (VAT Integration) + integration + Configuration of Extra-EU trade using the VAT Integration method + - - Extra-EU (VAT Integration) - integration - Configuration of Extra-EU trade using the VAT Integration method - + + Extra-EU (Self-invoice) + selfinvoice + Configuration of Extra-EU trade using the self-invoice method + - - Extra-EU (Self-invoice) - selfinvoice - Configuration of Extra-EU trade using the self-invoice method - - - - Intra-EU (Self-invoice) - selfinvoice - Configuration of Intra-EU trade using the self-invoice method - - - + + Intra-EU (Self-invoice) + selfinvoice + Configuration of Intra-EU trade using the self-invoice method + diff --git a/l10n_it_reverse_charge/migrations/13.0.1.0.0/noupdate_changes.xml b/l10n_it_reverse_charge/migrations/13.0.1.0.0/noupdate_changes.xml deleted file mode 100644 index 26e11936d55d..000000000000 --- a/l10n_it_reverse_charge/migrations/13.0.1.0.0/noupdate_changes.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - ['|',('company_id','=',False),('company_id','in',company_ids)] - - - ['|',('company_id','=',False),('company_id','in',company_ids)] - - diff --git a/l10n_it_reverse_charge/migrations/13.0.1.0.0/post-migration.py b/l10n_it_reverse_charge/migrations/13.0.1.0.0/post-migration.py deleted file mode 100644 index 362486262ee6..000000000000 --- a/l10n_it_reverse_charge/migrations/13.0.1.0.0/post-migration.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright 2021 Simone Vanin - Agile Business Group -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). - -from openupgradelib import openupgrade - - -def migrate(cr, installed_version): - openupgrade.load_data( - cr, "l10n_it_reverse_charge", "migrations/13.0.1.0.0/noupdate_changes.xml" - ) - - openupgrade.logged_query( - cr, - """ -update account_move -set - rc_self_invoice_id = inv.rc_self_invoice_id, - rc_purchase_invoice_id = inv.rc_purchase_invoice_id, - rc_self_purchase_invoice_id = inv.rc_self_purchase_invoice_id -from account_invoice inv -where - account_move.id = inv.move_id; - """, - ) - - openupgrade.logged_query( - cr, - """ -update account_move_line aml -set - rc = invl.rc -from account_invoice_line invl - join account_invoice inv on inv.id = invl.invoice_id -where - aml.move_id = inv.move_id; - """, - ) diff --git a/l10n_it_reverse_charge/migrations/13.0.1.0.0/pre-migration.py b/l10n_it_reverse_charge/migrations/13.0.1.0.0/pre-migration.py deleted file mode 100644 index f2ab1d154780..000000000000 --- a/l10n_it_reverse_charge/migrations/13.0.1.0.0/pre-migration.py +++ /dev/null @@ -1,187 +0,0 @@ -# Copyright 2021 Simone Vanin - Agile Business Group -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). - -from openupgradelib import openupgrade - - -@openupgrade.migrate() -def migrate(env, version): - if not version: - return - cr = env.cr - - # list of account_move fields for further insert - openupgrade.logged_query( - cr, - """ - SELECT STRING_AGG(column_name, ', ') - FROM information_schema.columns - WHERE table_name = 'account_move' - AND table_schema = 'public' - AND column_name NOT IN('id') - ; - """, - ) - - move_fields = "".join(cr.fetchone()) - query_move = ( - "select " - + move_fields.replace("create_date", "NOW()").replace("write_date", "NOW()") - + " from account_move " - ) - - # List of rc invoices - openupgrade.logged_query( - cr, - """ - select - am.company_id, - full_reconcile_id, - am.id, - inv.rc_purchase_invoice_id, - aml.account_id - from account_move_line aml - join account_move am - on am.id = aml.move_id - join account_invoice inv - on inv.move_id = am.id - join account_full_reconcile afr - on afr.id = aml.full_reconcile_id - where inv.rc_purchase_invoice_id is not null - and aml.move_id not in (select move_id from account_payment); - """, - ) - - res = cr.fetchall() - - for r in res: - company_id, fr_id, rc_inv, supp_inv, rc_dest_acc_id = r - openupgrade.logged_query( - cr, - """ - select am.currency_id, am.partner_id, am.create_uid, aml.account_id - from account_move am join account_move_line aml - on am.id = aml.move_id - where am.id = {supp_inv} - and aml.account_id in ( - select aa.id from account_account aa - where aa.internal_type = 'payable' - ); - """.format( - supp_inv=supp_inv - ), - ) - payment_vals = cr.fetchall()[0] - currency_id = payment_vals[0] - partner_id = payment_vals[1] - create_uid = payment_vals[2] - supp_dest_acc_id = payment_vals[3] - - openupgrade.logged_query( - cr, - """ - select move_id, full_reconcile_id, id, abs(amount_currency) - from account_move_line - where move_id in ( - select move_id from account_move_line - where full_reconcile_id = {fr_id} - and journal_id in ( - select payment_journal_id - from account_rc_type - where method = 'selfinvoice' - )) - order by full_reconcile_id; - """.format( - fr_id=fr_id - ), - ) - - # split payment move in two moves - move_vals = {} - # format [(inv_move, payment_move)] - move_ids = [] - supp_amount = 0 - rc_amount = 0 - for m, k, v, amnt in cr.fetchall(): - if k == fr_id: - k = rc_inv - rc_amount = amnt - else: - if rc_amount and rc_amount == amnt: - k = rc_inv - else: - k = supp_inv - supp_amount = amnt - if not move_ids: - move_ids = [(k, m)] - move_vals[k] = [v] if k not in move_vals else move_vals[k] + [v] - - # clone payment move and append to list - openupgrade.logged_query( - cr, - """ - insert into account_move ({move_fields}) - {query_move} where id = {move_id} - returning id; - """.format( - move_fields=move_fields, query_move=query_move, move_id=move_ids[0][1] - ), - ) - move_ids.append( - (supp_inv if move_ids[0] == rc_inv else rc_inv, cr.fetchone()[0]) - ) - - # create an account_payment record for every payment move - # update move lines with new move id - for inv, move_id in move_ids: - openupgrade.logged_query( - cr, - """ - insert into account_payment - (move_id, is_reconciled, is_matched, is_internal_transfer, - payment_method_id, amount, payment_type, partner_type, - currency_id, partner_id, destination_account_id, create_uid, - create_date, write_uid, write_date) - values - ({move_id}, 't', 't', 'f', {method}, {amount}, {payment_type}, - {partner_type}, {currency_id}, {partner_id}, {dest_acc_id}, - {create_uid}, NOW(), {write_uid}, NOW()) - returning id; - """.format( - move_id=move_id, - method=1 if inv == supp_inv else 2, - amount=supp_amount if inv == supp_inv else rc_amount, - payment_type="'outbound'" if inv == supp_inv else "'inbound'", - partner_type="'supplier'" if inv == supp_inv else "'customer'", - currency_id=currency_id, - partner_id=partner_id, - dest_acc_id=supp_dest_acc_id if inv == supp_inv else rc_dest_acc_id, - create_uid=create_uid, - write_uid=create_uid, - ), - ) - payment_id = cr.fetchone()[0] - - line_ids = ",".join([str(line_id) for line_id in move_vals[inv]]) - openupgrade.logged_query( - cr, - """ - update account_move_line - set move_id = {move_id}, - payment_id = {payment_id} - where id in ({line_ids}); - """.format( - move_id=move_id, payment_id=payment_id, line_ids=line_ids - ), - ) - - openupgrade.logged_query( - cr, - """ - update account_move - set payment_id = {payment_id} - where id = {move_id}; - """.format( - payment_id=payment_id, move_id=move_id - ), - ) diff --git a/l10n_it_reverse_charge/models/account_move.py b/l10n_it_reverse_charge/models/account_move.py index b015b704195c..56b00dcca083 100644 --- a/l10n_it_reverse_charge/models/account_move.py +++ b/l10n_it_reverse_charge/models/account_move.py @@ -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 2023 Simone Rubino - TAKOBI from odoo import api, fields, models from odoo.exceptions import UserError @@ -19,9 +20,15 @@ class AccountMoveLine(models.Model): "tax_ids", ) def _compute_rc_flag(self): - for line in self.filtered(lambda r: not r.exclude_from_invoice_tab): - if line.move_id.is_purchase_document(): - line.rc = bool(line.move_id.fiscal_position_id.rc_type_id) + for line in self: + move = line.move_id + is_invoice_line = line in move.invoice_line_ids + is_rc = ( + move.is_purchase_document() + and move.fiscal_position_id.rc_type_id + and is_invoice_line + ) + line.rc = is_rc rc = fields.Boolean( "RC", compute="_compute_rc_flag", store=True, readonly=False, default=False @@ -87,15 +94,14 @@ def rc_inv_vals(self, partner, rc_type, lines, currency): narration = _( "Reverse charge self invoice.\n" - "Supplier: %s\n" - "Reference: %s\n" - "Date: %s\n" - "Internal reference: %s" - ) % ( - supplier.display_name, - self.invoice_origin or self.ref or "", - self.date, - self.name, + "Supplier: %(supplier)s\n" + "Reference: %(reference)s\n" + "Date: %(date)s\n" + "Internal reference: %(internal_reference)s", + supplier=supplier.display_name, + reference=self.invoice_origin or self.ref or "", + date=self.date, + internal_reference=self.name, ) return { "partner_id": partner.id, @@ -127,8 +133,10 @@ def _rc_get_move_line_to_reconcile(self): else: raise UserError(_("Only inbound and outbound moves are supported")) + lines = self.line_ids + already_reconciled_lines = lines.filtered("reconciled") is_zero = self.currency_id.is_zero - for move_line in self.line_ids: + for move_line in lines - already_reconciled_lines: field_value = getattr(move_line, line_field) if not is_zero(field_value): break @@ -339,8 +347,11 @@ def _rc_reconcile_same_account_line(self, payment): ) ) - rc_lines_to_rec = line_to_reconcile | payment_line_to_reconcile - rc_lines_to_rec.reconcile() + if not payment_line_to_reconcile.reconciled: + # In some cases the payment line is already reconciled + # simply because it has 0 amount + rc_lines_to_rec = line_to_reconcile | payment_line_to_reconcile + rc_lines_to_rec.reconcile() def _reconcile_rc_invoice_payment(self, rc_invoice, rc_payment): """Reconcile the RC Payment.""" @@ -373,8 +384,13 @@ def generate_self_invoice(self): line_tax_ids = line.tax_ids if not line_tax_ids: raise UserError( - _("Invoice %s, line\n%s\nis RC but has not tax") - % ((self.name or self.partner_id.display_name), line.name) + _( + "Invoice %(invoice)s, line\n" + " %(line)s\n" + " is RC but has not tax", + invoice=self.name or self.partner_id.display_name, + line=line.name, + ) ) mapped_taxes = rc_type.map_tax( line_tax_ids, @@ -421,7 +437,7 @@ def generate_supplier_self_invoice(self): supplier_invoice = self.rc_self_purchase_invoice_id for line in supplier_invoice.line_ids: line.remove_move_reconcile() - supplier_invoice.line_ids.unlink() + supplier_invoice.line_ids.with_context(dynamic_unlink=True).unlink() # temporary disabling self invoice automations supplier_invoice.fiscal_position_id = False diff --git a/l10n_it_reverse_charge/models/account_rc_type.py b/l10n_it_reverse_charge/models/account_rc_type.py index 507f61e9b333..a7f0f866b19d 100644 --- a/l10n_it_reverse_charge/models/account_rc_type.py +++ b/l10n_it_reverse_charge/models/account_rc_type.py @@ -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 2023 Simone Rubino - TAKOBI from odoo import _, api, fields, models from odoo.exceptions import UserError, ValidationError @@ -63,10 +64,9 @@ class AccountRCType(models.Model): _name = "account.rc.type" _description = "Reverse Charge Type" - name = fields.Char("Name", required=True) + name = fields.Char(required=True) method = fields.Selection( selection=[("integration", "VAT Integration"), ("selfinvoice", "Self Invoice")], - string="Method", required=True, ) partner_type = fields.Selection( @@ -113,7 +113,7 @@ class AccountRCType(models.Model): string="Self Invoice Tax Mapping", copy=False, ) - description = fields.Text("Description") + description = fields.Text() self_invoice_text = fields.Text("Text in Self Invoice") company_id = fields.Many2one( "res.company", diff --git a/l10n_it_reverse_charge/readme/ROADMAP.rst b/l10n_it_reverse_charge/readme/ROADMAP.rst index cd172094eb4b..c97f2959122e 100644 --- a/l10n_it_reverse_charge/readme/ROADMAP.rst +++ b/l10n_it_reverse_charge/readme/ROADMAP.rst @@ -1 +1,3 @@ Only the **self-invoice** method is managed, **VAT integration** method is not managed yet. + +In test case test_intra_EU_exempt, the payment line to be reconciled is reconciled without having any reconciliation; it should be reconciled with the invoice. diff --git a/l10n_it_reverse_charge/security/reverse_charge_security.xml b/l10n_it_reverse_charge/security/reverse_charge_security.xml index 7fde9e5d65ef..33c725282c78 100644 --- a/l10n_it_reverse_charge/security/reverse_charge_security.xml +++ b/l10n_it_reverse_charge/security/reverse_charge_security.xml @@ -1,25 +1,25 @@ - - - - - Reverse Charge Type multi-company - - - ['|',('company_id','=',False),('company_id','in',company_ids)] - - - - Tax Mapping for self invoices multi-company - - - ['|',('company_id','=',False),('company_id','in',company_ids)] - - - - + + + + Reverse Charge Type multi-company + + + ['|',('company_id','=',False),('company_id','in',company_ids)] + + + + Tax Mapping for self invoices multi-company + + + ['|',('company_id','=',False),('company_id','in',company_ids)] + + diff --git a/l10n_it_reverse_charge/tests/rc_common.py b/l10n_it_reverse_charge/tests/rc_common.py index ad1c47c193ad..15b878818bb1 100644 --- a/l10n_it_reverse_charge/tests/rc_common.py +++ b/l10n_it_reverse_charge/tests/rc_common.py @@ -1,8 +1,23 @@ -from odoo.tests import tagged +# Copyright 2023 Simone Rubino - TAKOBI +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo.tests import Form, tagged from odoo.addons.account.tests.common import AccountTestInvoicingCommon +def _create_form_record(model, record_values, view=None): + """Create a record using Form. + + This allows to check required/invisible fields that cannot + as if the user were creating the record. + """ + form = Form(model, view=view) + for field, value in record_values.items(): + setattr(form, field, value) + return form.save() + + @tagged("post_install", "-at_install") class ReverseChargeCommon(AccountTestInvoicingCommon): @classmethod @@ -43,9 +58,9 @@ def setUpClass(cls, chart_template_ref=None): cls.env["account.account"].search( [ ( - "user_type_id", + "account_type", "=", - cls.env.ref("account.data_account_type_payable").id, + "liability_payable", ) ], limit=1, @@ -54,9 +69,9 @@ def setUpClass(cls, chart_template_ref=None): cls.invoice_line_account = cls.env["account.account"].search( [ ( - "user_type_id", + "account_type", "=", - cls.env.ref("account.data_account_type_expenses").id, + "expense", ) ], limit=1, @@ -72,7 +87,6 @@ def setUpClass(cls, chart_template_ref=None): "value": "percent", "value_amount": 50, "days": 15, - "sequence": 1, }, ), ( @@ -81,7 +95,6 @@ def setUpClass(cls, chart_template_ref=None): { "value": "balance", "days": 30, - "sequence": 2, }, ), ], @@ -104,9 +117,7 @@ def _create_account(cls): { "code": "295000", "name": "selfinvoice temporary", - "user_type_id": cls.env.ref( - "account.data_account_type_current_liabilities" - ).id, + "account_type": "liability_current", } ) @@ -146,24 +157,43 @@ def _create_taxes(cls): @classmethod def _create_journals(cls): journal_model = cls.env["account.journal"] - cls.journal_selfinvoice = journal_model.create( - {"name": "selfinvoice", "type": "sale", "code": "SLF"} + cls.journal_selfinvoice = _create_form_record( + journal_model, + { + "name": "selfinvoice", + "type": "sale", + "code": "SLF", + "default_account_id": cls.account_selfinvoice, + }, ) - cls.journal_reconciliation = journal_model.create( + cls.journal_reconciliation = _create_form_record( + journal_model, { "name": "RC reconciliation", "type": "general", "code": "SLFRC", - } + }, ) - cls.journal_selfinvoice_extra = journal_model.create( - {"name": "Extra selfinvoice", "type": "sale", "code": "SLFEX"} + cls.journal_selfinvoice_extra = _create_form_record( + journal_model, + { + "name": "Extra selfinvoice", + "type": "sale", + "code": "SLFEX", + "default_account_id": cls.account_selfinvoice, + }, ) - cls.journal_cee_extra = journal_model.create( - {"name": "Extra CEE", "type": "purchase", "code": "EXCEE"} + cls.journal_cee_extra = _create_form_record( + journal_model, + { + "name": "Extra CEE", + "type": "purchase", + "code": "EXCEE", + "default_account_id": cls.account_selfinvoice, + }, ) @classmethod diff --git a/l10n_it_reverse_charge/tests/test_rc.py b/l10n_it_reverse_charge/tests/test_rc.py index ca27ae395bcc..f2865cb7805e 100644 --- a/l10n_it_reverse_charge/tests/test_rc.py +++ b/l10n_it_reverse_charge/tests/test_rc.py @@ -1,7 +1,6 @@ # Copyright 2018 Simone Rubino - Agile Business Group # Copyright 2019 Alex Comba - Agile Business Group - -import json +# Copyright 2023 Simone Rubino - TAKOBI from odoo.exceptions import UserError from odoo.tests import tagged @@ -65,7 +64,7 @@ def test_intra_EU_amount_tax_amount_payments_widget_discrepancy(self): self.assertEqual(invoice.rc_self_invoice_id.state, "posted") self.assertEqual(invoice.rc_self_invoice_id.rc_payment_move_id.state, "posted") # compare amount_tax with amount show on paymenys_widget - info = json.loads(invoice.invoice_payments_widget)["content"][0] + info = invoice.invoice_payments_widget["content"][0] self.assertEqual(info["amount"], invoice.amount_tax) def test_extra_EU(self): @@ -85,7 +84,6 @@ def test_extra_EU(self): invoice.button_draft() # see what done with "with invoice.env.do_in_draft()" in # button_draft - invoice.refresh() self.assertEqual(invoice.state, "draft") def test_intra_EU_cancel_and_draft(self): @@ -94,7 +92,6 @@ def test_intra_EU_cancel_and_draft(self): invoice.button_cancel() self.assertEqual(invoice.state, "cancel") invoice.button_draft() - invoice.refresh() self.assertEqual(invoice.state, "draft") def test_intra_EU_zero_total(self): @@ -111,7 +108,6 @@ def test_intra_EU_zero_total(self): self.assertEqual(invoice.state, "cancel") self.assertFalse(invoice.rc_self_invoice_id) invoice.button_draft() - invoice.refresh() self.assertEqual(invoice.state, "draft") def test_new_refund_flag(self): @@ -134,7 +130,6 @@ def test_intra_EU_exempt(self): invoice.button_cancel() self.assertEqual(invoice.state, "cancel") invoice.button_draft() - invoice.refresh() self.assertEqual(invoice.state, "draft") def test_intra_EU_draft_and_reconfirm(self): @@ -175,3 +170,22 @@ def test_supplier_extraEU_no_outstanding_payment(self): payments_lines = (self_purchase_payment | self_purchase_rc_payment).line_ids self.assertTrue(all(payments_lines.mapped("reconciled"))) + + def test_extra_EU_draft_and_reconfirm(self): + """Check that an invoice with RC Self Purchase Invoice + can be reset to draft and confirmed again.""" + # Arrange + invoice = self.create_invoice( + self.supplier_extraEU, + amounts=[100], + taxes=self.tax_0_pur, + ) + # pre-condition + self.assertTrue(invoice.rc_self_purchase_invoice_id) + + # Act + invoice.button_draft() + invoice.action_post() + + # Assert + self.assertEqual(invoice.state, "posted") diff --git a/l10n_it_reverse_charge/views/account_move_views.xml b/l10n_it_reverse_charge/views/account_move_views.xml index 3d82527007b1..284a41448e05 100644 --- a/l10n_it_reverse_charge/views/account_move_views.xml +++ b/l10n_it_reverse_charge/views/account_move_views.xml @@ -2,7 +2,9 @@ + Copyright 2017 Marco Calcagni - Dinamiche Aziendali srl + Copyright 2023 Simone Rubino - TAKOBI + --> account.invoice.supplier.form.rc @@ -26,12 +28,6 @@ > - - - diff --git a/l10n_it_reverse_charge/views/account_rc_type_view.xml b/l10n_it_reverse_charge/views/account_rc_type_view.xml index 4e69b7b0bf7e..b7dd203c6192 100644 --- a/l10n_it_reverse_charge/views/account_rc_type_view.xml +++ b/l10n_it_reverse_charge/views/account_rc_type_view.xml @@ -2,7 +2,9 @@ + Copyright 2017 Marco Calcagni - Dinamiche Aziendali srl + Copyright 2023 Simone Rubino - TAKOBI + --> @@ -79,7 +81,7 @@ 'readonly': [('method', '!=', 'selfinvoice')], 'required': [('method', '=', 'selfinvoice')]}" > - + @@ -102,7 +104,7 @@ account.rc.type tree - +