diff --git a/l10n_it_delivery_note/README.rst b/l10n_it_delivery_note/README.rst index b8c9ecbd0957..18e64874b8fd 100644 --- a/l10n_it_delivery_note/README.rst +++ b/l10n_it_delivery_note/README.rst @@ -156,6 +156,15 @@ permessi dell'utente. Le fatture generate dai DDT contengono i riferimenti al DDT stesso nelle righe nota. +Fatturazione da DN +------------------ + +E' possibile creare una fattura selezionando una o più DN dello stesso +partner dalla tree view tramite il wizard "crea fattura". Si può +scegliere se includere anche i servizi non ancora fatturati dell'ordine +di vendita correlato o considerare solo le righe nei DN. In maniera +predefinita vengono dedotti gli eventuali anticipi fatturati. + Accesso da portale ------------------ diff --git a/l10n_it_delivery_note/models/account_invoice.py b/l10n_it_delivery_note/models/account_invoice.py index 0a2b299a46ba..0c29a0d4a09d 100644 --- a/l10n_it_delivery_note/models/account_invoice.py +++ b/l10n_it_delivery_note/models/account_invoice.py @@ -4,6 +4,7 @@ # @author: Gianmarco Conte # Copyright (c) 2019, Link IT Europe Srl # @author: Matteo Bilotta +# Copyright (c) 2024, Nextev Srl # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from odoo import _, fields, models @@ -148,13 +149,11 @@ def unlink(self): # Ripristino il valore delle delivery note # per poterle rifatturare inv_lines = self.mapped("invoice_line_ids") - all_dnls = inv_lines.mapped("sale_line_ids").mapped("delivery_note_line_ids") - inv_dnls = self.mapped("delivery_note_ids").mapped("line_ids") - dnls_to_unlink = all_dnls & inv_dnls + inv_dnls = inv_lines.mapped("delivery_note_line_id") res = super().unlink() - dnls_to_unlink.sync_invoice_status() - dnls_to_unlink.mapped("delivery_note_id")._compute_invoice_status() - for dn in dnls_to_unlink.mapped("delivery_note_id"): + inv_dnls.sync_invoice_status() + inv_dnls.mapped("delivery_note_id")._compute_invoice_status() + for dn in inv_dnls.mapped("delivery_note_id"): dn.state = "confirm" return res diff --git a/l10n_it_delivery_note/models/account_invoice_line.py b/l10n_it_delivery_note/models/account_invoice_line.py index f13171454345..54e40ae9371d 100644 --- a/l10n_it_delivery_note/models/account_invoice_line.py +++ b/l10n_it_delivery_note/models/account_invoice_line.py @@ -12,4 +12,10 @@ class AccountInvoiceLine(models.Model): delivery_note_id = fields.Many2one( "stock.delivery.note", string="Delivery Note", readonly=True, copy=False ) + delivery_note_line_id = fields.Many2one( + "stock.delivery.note.line", + string="Delivery Note Line", + readonly=True, + copy=False, + ) note_dn = fields.Boolean(string="Note DN") diff --git a/l10n_it_delivery_note/models/stock_delivery_note.py b/l10n_it_delivery_note/models/stock_delivery_note.py index eade81493a06..2f2de59e177a 100644 --- a/l10n_it_delivery_note/models/stock_delivery_note.py +++ b/l10n_it_delivery_note/models/stock_delivery_note.py @@ -1,9 +1,10 @@ # Copyright (c) 2019, Link IT Europe Srl # @author: Matteo Bilotta +# Copyright (c) 2024, Nextev Srl import datetime -from odoo import _, api, fields, models +from odoo import Command, _, api, fields, models from odoo.exceptions import UserError from ..mixins.picking_checker import ( @@ -657,30 +658,6 @@ def _check_delivery_notes_before_invoicing(self): } ) - def _fix_quantities_to_invoice(self, lines, invoice_method): - cache = {} - - pickings_lines = lines.retrieve_pickings_lines(self.picking_ids) - other_lines = lines - pickings_lines - - if not invoice_method or invoice_method == "dn": - for line in other_lines: - cache[line] = line.fix_qty_to_invoice() - elif invoice_method == "service": - for line in other_lines: - if line.product_id.type != "service": - cache[line] = line.fix_qty_to_invoice() - - pickings_move_ids = self.mapped("picking_ids.move_ids") - for line in pickings_lines.filtered(lambda line: len(line.move_ids) > 1): - move_ids = line.move_ids & pickings_move_ids - qty_to_invoice = sum(move_ids.mapped("quantity_done")) - - if qty_to_invoice < line.qty_to_invoice: - cache[line] = line.fix_qty_to_invoice(qty_to_invoice) - - return cache - def action_invoice(self, invoice_method=False): self._check_delivery_notes_before_invoicing() @@ -695,34 +672,58 @@ def action_invoice(self, invoice_method=False): ) if not sale_ids: continue - orders_lines = sale_ids.mapped("order_line").filtered( - lambda l: l.product_id # noqa: E741 + downpayment_lines_to_invoice = sale_ids.mapped("order_line").filtered( + lambda l: l.product_id and l.is_downpayment and l.qty_to_invoice < 0 # noqa: E741 ) - downpayment_lines = orders_lines.filtered(lambda l: l.is_downpayment) # noqa: E741 - invoiceable_lines = orders_lines.filtered(lambda l: l.is_invoiceable) # noqa: E741 - - cache = self._fix_quantities_to_invoice( - invoiceable_lines - downpayment_lines, invoice_method - ) - - for downpayment in downpayment_lines: - order = downpayment.order_id - order_lines = order.order_line.filtered( - lambda l: l.product_id and not l.is_downpayment # noqa: E741 - ) - - if order_lines.filtered(lambda l: l.need_to_be_invoiced): # noqa: E741 - cache[downpayment] = downpayment.fix_qty_to_invoice() - invoice_ids = sale_ids.filtered( lambda o: o.invoice_status == DOMAIN_INVOICE_STATUSES[1] )._create_invoices(final=True) - for line, vals in cache.items(): - line.write(vals) + invoice_line_ids = invoice_ids.invoice_line_ids + if invoice_method == "service": + invoice_line_ids = invoice_line_ids.filtered( + lambda il: il.product_id.type != "service" + ) + + invoice_line_ids.unlink() + + vals_list = [] + sequence = 1 + account_move = self.env["account.move"] + for dn in self: + vals_list.append( + Command.create(account_move._prepare_note_dn_value(sequence, dn)) + ) + sequence += 1 + for delivery_note_line in dn.line_ids: + vals_list.append( + Command.create( + delivery_note_line._prepare_invoice_line(sequence=sequence) + ) + ) + sequence += 1 + + if downpayment_lines_to_invoice: + vals_list.append( + Command.create( + { + "sequence": sequence, + "display_type": "line_section", + "name": _("Down Payments"), + "note_dn": True, + "quantity": 0, + } + ) + ) + sequence += 1 + for line in downpayment_lines_to_invoice: + vals_list.append( + Command.create(line._prepare_invoice_line(sequence=sequence)) + ) + sequence += 1 - orders_lines._compute_qty_to_invoice() + invoice_ids.line_ids = vals_list for line in self.mapped("line_ids"): line.write({"invoice_status": "invoiced"}) @@ -740,8 +741,6 @@ def action_invoice(self, invoice_method=False): } ) self._compute_invoice_status() - invoices = self.env["account.move"].browse(invoice_ids.ids) - invoices.update_delivery_note_lines() def action_done(self): self.write({"state": DOMAIN_DELIVERY_NOTE_STATES[3]}) diff --git a/l10n_it_delivery_note/models/stock_delivery_note_line.py b/l10n_it_delivery_note/models/stock_delivery_note_line.py index 6809ab167346..52df2dbb5d5c 100644 --- a/l10n_it_delivery_note/models/stock_delivery_note_line.py +++ b/l10n_it_delivery_note/models/stock_delivery_note_line.py @@ -1,9 +1,10 @@ # Copyright 2022 Dinamiche Aziendali srl # (http://www.dinamicheaziendali.it/) # @author: Giuseppe Borruso +# Copyright 2024 Nextev srl # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from odoo import _, api, fields, models +from odoo import Command, _, api, fields, models from odoo.exceptions import UserError DATE_FORMAT = "%d/%m/%Y" @@ -197,3 +198,45 @@ def sync_invoice_status(self): if invoice_status == "upselling" else invoice_status ) + + def _prepare_invoice_line(self, **optional_values): + """Prepare the values to create the new invoice line for a DN line. + + :param optional_values: any parameter that should be added to the + returned invoice line + :rtype: dict + """ + self.ensure_one() + res = { + "display_type": "product", + "name": self.name, + "product_id": self.product_id.id, + "product_uom_id": self.product_uom_id.id, + "quantity": self.product_qty, + "discount": self.discount, + "price_unit": self.price_unit, + "tax_ids": [Command.set(self.tax_ids.ids)], + "sale_line_ids": [Command.link(self.sale_line_id.id)], + "delivery_note_line_id": self.id, + } + if optional_values.get("sequence"): + res["sequence"] = optional_values["sequence"] + analytic_account_id = self.sale_line_id.order_id.analytic_account_id.id + if ( + self.sale_line_id.analytic_distribution + and not self.sale_line_id.display_type + ): + res["analytic_distribution"] = self.sale_line_id.analytic_distribution + if analytic_account_id and not self.sale_line_id.display_type: + analytic_account_id = str(analytic_account_id) + if "analytic_distribution" in res: + res["analytic_distribution"][analytic_account_id] = ( + res["analytic_distribution"].get(analytic_account_id, 0) + 100 + ) + else: + res["analytic_distribution"] = {analytic_account_id: 100} + if optional_values: + res.update(optional_values) + if self.display_type: + res["account_id"] = False + return res diff --git a/l10n_it_delivery_note/readme/USAGE.md b/l10n_it_delivery_note/readme/USAGE.md index 34ca0b4bb553..e52b121a6079 100644 --- a/l10n_it_delivery_note/readme/USAGE.md +++ b/l10n_it_delivery_note/readme/USAGE.md @@ -39,6 +39,14 @@ permessi dell'utente. Le fatture generate dai DDT contengono i riferimenti al DDT stesso nelle righe nota. +## Fatturazione da DN + +E' possibile creare una fattura selezionando una o più DN dello stesso partner +dalla tree view tramite il wizard "crea fattura". +Si può scegliere se includere anche i servizi non ancora fatturati dell'ordine +di vendita correlato o considerare solo le righe nei DN. +In maniera predefinita vengono dedotti gli eventuali anticipi fatturati. + ## Accesso da portale Gli utenti portal hanno la possibilità di scaricare i report dei DDT di cui loro o la loro azienda padre sono impostati come destinatari o indirizzo di spedizione. diff --git a/l10n_it_delivery_note/static/description/index.html b/l10n_it_delivery_note/static/description/index.html index 78dce28e0049..ef89ecba2fe5 100644 --- a/l10n_it_delivery_note/static/description/index.html +++ b/l10n_it_delivery_note/static/description/index.html @@ -8,11 +8,10 @@ /* :Author: David Goodger (goodger@python.org) -:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $ +:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $ :Copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. -Despite the name, some widely supported CSS2 features are used. See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to customize this style sheet. @@ -275,7 +274,7 @@ margin-left: 2em ; margin-right: 2em } -pre.code .ln { color: gray; } /* line numbers */ +pre.code .ln { color: grey; } /* line numbers */ pre.code, code { background-color: #eeeeee } pre.code .comment, code .comment { color: #5C6576 } pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold } @@ -301,7 +300,7 @@ span.pre { white-space: pre } -span.problematic, pre.problematic { +span.problematic { color: red } span.section-subtitle { @@ -404,14 +403,15 @@

ITA - Documento di trasporto

  • Usage
  • -
  • Bug Tracker
  • -
  • Credits @@ -488,15 +488,23 @@

    Funzionalità avanzata

    Le fatture generate dai DDT contengono i riferimenti al DDT stesso nelle righe nota.

    +
    +

    Fatturazione da DN

    +

    E’ possibile creare una fattura selezionando una o più DN dello stesso +partner dalla tree view tramite il wizard “crea fattura”. Si può +scegliere se includere anche i servizi non ancora fatturati dell’ordine +di vendita correlato o considerare solo le righe nei DN. In maniera +predefinita vengono dedotti gli eventuali anticipi fatturati.

    +
    -

    Accesso da portale

    +

    Accesso da portale

    Gli utenti portal hanno la possibilità di scaricare i report dei DDT di cui loro o la loro azienda padre sono impostati come destinatari o indirizzo di spedizione.

    -

    Bug Tracker

    +

    Bug Tracker

    Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed @@ -504,9 +512,9 @@

    Bug Tracker

    Do not contact contributors directly about support or help with technical issues.

    -

    Credits

    +

    Credits

    -

    Authors

    +

    Authors

    • Marco Calcagni
    • Gianmarco Conte
    • @@ -514,7 +522,7 @@

      Authors

    -

    Contributors

    +

    Contributors

    -

    Maintainers

    +

    Maintainers

    This module is maintained by the OCA.

    - -Odoo Community Association - +Odoo Community Association

    OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.

    diff --git a/l10n_it_delivery_note/tests/test_stock_delivery_note_invoicing.py b/l10n_it_delivery_note/tests/test_stock_delivery_note_invoicing.py index 020fb3116381..db1db4f2f792 100644 --- a/l10n_it_delivery_note/tests/test_stock_delivery_note_invoicing.py +++ b/l10n_it_delivery_note/tests/test_stock_delivery_note_invoicing.py @@ -98,7 +98,7 @@ def test_complete_invoicing_single_so(self): self.assertEqual(delivery_note_line.sale_line_id, order_line) self.assertEqual(delivery_note_line.product_qty, 1) - invoice_line = final_invoice.invoice_line_ids[0] + invoice_line = final_invoice.invoice_line_ids[1] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, 1) @@ -120,7 +120,7 @@ def test_complete_invoicing_single_so(self): self.assertEqual(delivery_note_line.sale_line_id, order_line) self.assertEqual(delivery_note_line.product_qty, 2) - invoice_line = final_invoice.invoice_line_ids[1] + invoice_line = final_invoice.invoice_line_ids[2] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, 2) @@ -142,7 +142,7 @@ def test_complete_invoicing_single_so(self): self.assertEqual(delivery_note_line.sale_line_id, order_line) self.assertEqual(delivery_note_line.product_qty, 11) - invoice_line = final_invoice.invoice_line_ids[2] + invoice_line = final_invoice.invoice_line_ids[3] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, 11) @@ -164,7 +164,7 @@ def test_complete_invoicing_single_so(self): self.assertEqual(delivery_note_line.sale_line_id, order_line) self.assertEqual(delivery_note_line.product_qty, 1) - invoice_line = final_invoice.invoice_line_ids[3] + invoice_line = final_invoice.invoice_line_ids[4] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, 1) @@ -187,18 +187,18 @@ def test_complete_invoicing_single_so(self): self.assertEqual(len(delivery_note_line), 0) - invoice_line = final_invoice.invoice_line_ids[4] + invoice_line = final_invoice.invoice_line_ids[-2] self.assertEqual(invoice_line.display_type, "line_section") self.assertEqual(invoice_line.name, "Down Payments") - invoice_line = final_invoice.invoice_line_ids[5] + invoice_line = final_invoice.invoice_line_ids[-1] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, -1) # # Fattura - Linea 7 (DdT in fattura) # - invoice_line = final_invoice.invoice_line_ids[6] + invoice_line = final_invoice.invoice_line_ids[0] self.assertEqual(invoice_line.display_type, "line_note") self.assertEqual(invoice_line.quantity, 0) self.assertEqual(invoice_line.delivery_note_id, delivery_note) @@ -447,7 +447,7 @@ def test_partial_invoicing_single_so(self): self.assertEqual(delivery_note_line.sale_line_id, order_line) self.assertEqual(delivery_note_line.product_qty, 1) - invoice_line = final_invoice.invoice_line_ids[0] + invoice_line = final_invoice.invoice_line_ids[1] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, 1) @@ -469,7 +469,7 @@ def test_partial_invoicing_single_so(self): self.assertEqual(delivery_note_line.sale_line_id, order_line) self.assertEqual(delivery_note_line.product_qty, 5) - invoice_line = final_invoice.invoice_line_ids[1] + invoice_line = final_invoice.invoice_line_ids[2] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, 5) @@ -491,7 +491,7 @@ def test_partial_invoicing_single_so(self): self.assertEqual(delivery_note_line.sale_line_id, order_line) self.assertEqual(delivery_note_line.product_qty, 2) - invoice_line = final_invoice.invoice_line_ids[2] + invoice_line = final_invoice.invoice_line_ids[3] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, 2) @@ -515,18 +515,18 @@ def test_partial_invoicing_single_so(self): self.assertEqual(len(delivery_note_line), 0) - invoice_line = final_invoice.invoice_line_ids[3] + invoice_line = final_invoice.invoice_line_ids[-2] self.assertEqual(invoice_line.display_type, "line_section") self.assertEqual(invoice_line.name, "Down Payments") - invoice_line = final_invoice.invoice_line_ids[4] + invoice_line = final_invoice.invoice_line_ids[-1] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, -1) # # Fattura - Linea 6 (DdT in fattura) # - invoice_line = final_invoice.invoice_line_ids[5] + invoice_line = final_invoice.invoice_line_ids[0] self.assertEqual(invoice_line.display_type, "line_note") self.assertEqual(invoice_line.quantity, 0) self.assertEqual(invoice_line.delivery_note_id, second_delivery_note) @@ -635,7 +635,7 @@ def test_complete_invoicing_multiple_so(self): invoices = sales_orders.mapped("invoice_ids") self.assertEqual(len(invoices), 2) - final_invoice = invoices[0] + final_invoice = invoices.sorted("id", reverse=True)[0] self.assertEqual(len(final_invoice.invoice_line_ids), 9) self.assertEqual(final_invoice.delivery_note_ids, delivery_note) @@ -659,7 +659,7 @@ def test_complete_invoicing_multiple_so(self): self.assertEqual(delivery_note_line.sale_line_id, order_line) self.assertEqual(delivery_note_line.product_qty, 1) - invoice_line = final_invoice.invoice_line_ids[0] + invoice_line = final_invoice.invoice_line_ids[1] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, 1) @@ -681,7 +681,7 @@ def test_complete_invoicing_multiple_so(self): self.assertEqual(delivery_note_line.sale_line_id, order_line) self.assertEqual(delivery_note_line.product_qty, 3) - invoice_line = final_invoice.invoice_line_ids[1] + invoice_line = final_invoice.invoice_line_ids[2] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, 3) @@ -703,7 +703,7 @@ def test_complete_invoicing_multiple_so(self): self.assertEqual(delivery_note_line.sale_line_id, order_line) self.assertEqual(delivery_note_line.product_qty, 2) - invoice_line = final_invoice.invoice_line_ids[2] + invoice_line = final_invoice.invoice_line_ids[3] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, 2) @@ -726,11 +726,11 @@ def test_complete_invoicing_multiple_so(self): self.assertEqual(len(delivery_note_line), 0) - invoice_line = final_invoice.invoice_line_ids[3] + invoice_line = final_invoice.invoice_line_ids[7] self.assertEqual(invoice_line.display_type, "line_section") self.assertEqual(invoice_line.name, "Down Payments") - invoice_line = final_invoice.invoice_line_ids[4] + invoice_line = final_invoice.invoice_line_ids[-1] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, -1) @@ -752,7 +752,7 @@ def test_complete_invoicing_multiple_so(self): self.assertEqual(delivery_note_line.sale_line_id, order_line) self.assertEqual(delivery_note_line.product_qty, 11) - invoice_line = final_invoice.invoice_line_ids[5] + invoice_line = final_invoice.invoice_line_ids[4] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, 11) @@ -774,7 +774,7 @@ def test_complete_invoicing_multiple_so(self): self.assertEqual(delivery_note_line.sale_line_id, order_line) self.assertEqual(delivery_note_line.product_qty, 5) - invoice_line = final_invoice.invoice_line_ids[6] + invoice_line = final_invoice.invoice_line_ids[5] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, 5) @@ -796,14 +796,14 @@ def test_complete_invoicing_multiple_so(self): self.assertEqual(delivery_note_line.sale_line_id, order_line) self.assertEqual(delivery_note_line.product_qty, 1) - invoice_line = final_invoice.invoice_line_ids[7] + invoice_line = final_invoice.invoice_line_ids[6] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, 1) # # Fattura - Linea 9 (DdT in fattura) # - invoice_line = final_invoice.invoice_line_ids[8] + invoice_line = final_invoice.invoice_line_ids[0] self.assertEqual(invoice_line.display_type, "line_note") self.assertEqual(invoice_line.quantity, 0) self.assertEqual(invoice_line.delivery_note_id, delivery_note) @@ -1175,7 +1175,7 @@ def test_partial_invoicing_multiple_so(self): self.assertEqual(delivery_note_line.sale_line_id, order_line) self.assertEqual(delivery_note_line.product_qty, 1) - invoice_line = final_invoice.invoice_line_ids[0] + invoice_line = final_invoice.invoice_line_ids[1] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, 1) @@ -1199,11 +1199,11 @@ def test_partial_invoicing_multiple_so(self): self.assertEqual(len(delivery_note_line), 0) - invoice_line = final_invoice.invoice_line_ids[1] + invoice_line = final_invoice.invoice_line_ids[4] self.assertEqual(invoice_line.display_type, "line_section") self.assertEqual(invoice_line.name, "Down Payments") - invoice_line = final_invoice.invoice_line_ids[2] + invoice_line = final_invoice.invoice_line_ids[-1] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, -1) @@ -1225,7 +1225,7 @@ def test_partial_invoicing_multiple_so(self): self.assertEqual(delivery_note_line.sale_line_id, order_line) self.assertEqual(delivery_note_line.product_qty, 8) - invoice_line = final_invoice.invoice_line_ids[3] + invoice_line = final_invoice.invoice_line_ids[2] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, 8) @@ -1247,14 +1247,14 @@ def test_partial_invoicing_multiple_so(self): self.assertEqual(delivery_note_line.sale_line_id, order_line) self.assertEqual(delivery_note_line.product_qty, 2) - invoice_line = final_invoice.invoice_line_ids[4] + invoice_line = final_invoice.invoice_line_ids[3] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, 2) # # Fattura 3 - Linea 6 (DdT in fattura) # - invoice_line = final_invoice.invoice_line_ids[5] + invoice_line = final_invoice.invoice_line_ids[0] self.assertEqual(invoice_line.display_type, "line_note") self.assertEqual(invoice_line.quantity, 0) self.assertEqual(invoice_line.delivery_note_id, second_delivery_note) @@ -1286,3 +1286,140 @@ def test_delivery_note_to_draft_from_create(self): delivery_note.action_draft() self.assertEqual(delivery_note.invoice_status, "no") self.assertEqual(delivery_note.state, "draft") + + # ⇒ "DdT multipli: fatturazione completa" + def test_complete_invoicing_multiple_dn(self): + # + # - Picking -- DdT ┐ + # | | + # SO - ├ Fattura + # | | + # - Picking -- DdT ┘ + # + + # Activate advanced setting to allow more picking in one DN + self.env["ir.config_parameter"].sudo().set_param( + "l10n_it_delivery_note.group_use_advanced_delivery_notes", True + ) + + # SO + sales_order = self.create_sales_order( + [ + self.desk_combination_line, + self.customizable_desk_line, + ] + ) + self.assertEqual(len(sales_order.order_line), 2) + + sales_order.action_confirm() + + # 1° Picking + first_picking = sales_order.picking_ids + self.assertEqual(len(first_picking), 1) + self.assertEqual(len(first_picking.move_ids), 2) + + first_picking.move_ids[0].quantity_done = 1 + first_picking.move_ids[1].quantity_done = 1 + + result = first_picking.button_validate() + self.assertTrue(result) + + wizard = Form( + self.env[(result.get("res_model"))].with_context(**result["context"]) + ).save() + self.assertEqual(wizard._name, "stock.backorder.confirmation") + wizard.process() + + # 1° DdT + wizard = Form( + self.env["stock.delivery.note.create.wizard"].with_context( + active_ids=first_picking.ids, active_model="stock.picking" + ) + ).save() + result = wizard.confirm() + first_delivery_note = self.env["stock.delivery.note"].browse(result["res_id"]) + first_delivery_note.action_confirm() + self.assertEqual(len(first_delivery_note.line_ids), 2) + self.assertEqual(first_delivery_note.invoice_status, "to invoice") + + # 2° Picking + backorder = self.env["stock.picking"].search( + [("backorder_id", "=", first_picking.id)] + ) + self.assertEqual(len(backorder), 1) + self.assertEqual(len(backorder.move_ids), 1) + + backorder.move_ids[0].quantity_done = 2 + + result = backorder.button_validate() + self.assertTrue(result) + + # 2° DdT + second_delivery_note = self.create_delivery_note() + second_delivery_note.transport_datetime = datetime.now() + timedelta( + days=1, hours=3 + ) + second_delivery_note.picking_ids = backorder + second_delivery_note.action_confirm() + + self.assertEqual(len(second_delivery_note.line_ids), 1) + self.assertEqual(second_delivery_note.invoice_status, "to invoice") + + # Create invoice + delivery_notes = first_delivery_note | second_delivery_note + delivery_notes.action_invoice() + + self.assertEqual(first_delivery_note.state, "invoiced") + self.assertEqual(second_delivery_note.state, "invoiced") + + invoices = delivery_notes.mapped("invoice_ids") + self.assertEqual(len(invoices), 1) + + self.assertEqual(invoices.delivery_note_ids, delivery_notes) + self.assertEqual(len(invoices.invoice_line_ids), 5) + + # Check invoice lines + lines_note = invoices.invoice_line_ids.filtered( + lambda line: line.display_type == "line_note" + ) + lines_product = invoices.invoice_line_ids.filtered( + lambda line: line.display_type == "product" + ) + # Note 1 DdT 1: 'Delivery Note "DDT/C1/00001" of ...' + line_note_dn_1 = lines_note.filtered(lambda line: line.sequence == 1) + self.assertEqual( + line_note_dn_1.name, + f'Delivery Note "{first_delivery_note.name}" of ' + f'{first_delivery_note.date.strftime("%d/%m/%Y")}', + ) + # Product Line 1 DdT 1 + line_product_dn_1 = lines_product.filtered(lambda line: line.sequence == 2) + self.assertEqual( + line_product_dn_1.product_id, first_delivery_note.line_ids[0].product_id + ) + self.assertEqual( + line_product_dn_1.quantity, first_delivery_note.line_ids[0].product_qty + ) + # Product Line 2 DdT 1 + line_product_dn_2 = lines_product.filtered(lambda line: line.sequence == 3) + self.assertEqual( + line_product_dn_2.product_id, first_delivery_note.line_ids[1].product_id + ) + self.assertEqual( + line_product_dn_2.quantity, first_delivery_note.line_ids[1].product_qty + ) + # Note 1 DdT 2: 'Delivery Note "DDT/C1/00002" of ...' + line_note_dn_2 = lines_note.filtered(lambda line: line.sequence == 4) + self.assertEqual( + line_note_dn_2.name, + f'Delivery Note "{second_delivery_note.name}" of ' + f'{second_delivery_note.date.strftime("%d/%m/%Y")}', + ) + # Product Line 1 DdT 2 + line_product_dn_3 = lines_product.filtered(lambda line: line.sequence == 5) + self.assertEqual( + line_product_dn_3.product_id, second_delivery_note.line_ids[0].product_id + ) + self.assertEqual( + line_product_dn_3.quantity, second_delivery_note.line_ids[0].product_qty + ) diff --git a/l10n_it_delivery_note/wizard/delivery_note_invoice.py b/l10n_it_delivery_note/wizard/delivery_note_invoice.py index 21b530e89766..8143b6670535 100644 --- a/l10n_it_delivery_note/wizard/delivery_note_invoice.py +++ b/l10n_it_delivery_note/wizard/delivery_note_invoice.py @@ -1,9 +1,11 @@ # Copyright (C) 2018-Today: # Dinamiche Aziendali Srl () # @author: Giuseppe Borruso +# Copyright (c) 2024, Nextev Srl # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import api, fields, models +from odoo import _, api, fields, models +from odoo.exceptions import UserError INVOICE_STATUSES = [ ("no", "Nothing to invoice"), @@ -34,7 +36,28 @@ def create_invoices(self): delivery_note_ids = self.env["stock.delivery.note"].browse( self._context.get("active_ids", []) ) + if len(delivery_note_ids.mapped("partner_id")) > 1: + raise UserError(_("You must select only delivery notes from one partner.")) delivery_note_ids.action_invoice(self.invoice_method) - for invoice in delivery_note_ids.mapped("invoice_ids"): + invoices_ids = delivery_note_ids.mapped("invoice_ids") + for invoice in invoices_ids: invoice.invoice_date = self.invoice_date - return True + if len(invoices_ids) > 1: + return { + "name": _("Invoices"), + "type": "ir.actions.act_window", + "res_model": "account.move", + "view_type": "list", + "view_mode": "list", + "views": [[False, "list"], [False, "form"]], + "domain": [("id", "in", invoices_ids.ids)], + } + return { + "name": invoices_ids.display_name, + "type": "ir.actions.act_window", + "res_model": "account.move", + "view_type": "form", + "view_mode": "form", + "views": [[False, "form"]], + "res_id": invoices_ids.id, + }