From c97d8b5cf72c3560f2b229576fb9b5f38c71c4ac Mon Sep 17 00:00:00 2001 From: Alessandro Uffreduzzi Date: Mon, 19 Aug 2024 11:43:23 +0200 Subject: [PATCH] [IMP]l10n_it_delivery_note: split move lines based on dn --- .../models/account_invoice.py | 74 ++++++------ l10n_it_delivery_note/models/sale_order.py | 113 +++++++++++++----- .../test_stock_delivery_note_invoicing.py | 24 +++- 3 files changed, 138 insertions(+), 73 deletions(-) diff --git a/l10n_it_delivery_note/models/account_invoice.py b/l10n_it_delivery_note/models/account_invoice.py index 93937e3a4c72..256dcf364b1f 100644 --- a/l10n_it_delivery_note/models/account_invoice.py +++ b/l10n_it_delivery_note/models/account_invoice.py @@ -6,9 +6,11 @@ # @author: Matteo Bilotta # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from collections import defaultdict + from odoo import _, fields, models -from .stock_delivery_note import DATE_FORMAT, DOMAIN_INVOICE_STATUSES +from .stock_delivery_note import DATE_FORMAT class AccountInvoice(models.Model): @@ -87,13 +89,27 @@ def _prepare_note_dn_value(self, sequence, delivery_note_id): "quantity": 0, } + def _has_dn_line_note(self, delivery_note): + self.ensure_one() + return bool( + self.invoice_line_ids.filtered( + lambda line, dn=delivery_note: line.display_type == "line_note" + and line.delivery_note_id == dn + ) + ) + def update_delivery_note_lines(self): context = {} for invoice in self.filtered(lambda i: i.delivery_note_ids): + sequence = 1 new_lines = [] - old_lines = invoice.invoice_line_ids.filtered(lambda l: l.note_dn) - old_lines.unlink() + + # Build a dictionary {delivery.note(1, 2): account.move.line(3, 5)} + inv_line_by_dn = defaultdict(self.env["account.move.line"].browse) + for inv_line in invoice.invoice_line_ids: + dn = inv_line.delivery_note_line_id.delivery_note_id + inv_line_by_dn[dn] |= inv_line # # TODO: Come bisogna comportarsi nel caso in @@ -111,7 +127,9 @@ def update_delivery_note_lines(self): # context["lang"] = invoice.partner_id.lang - if len(invoice.delivery_note_ids) == 1: + if len(invoice.delivery_note_ids) == 1 and not invoice._has_dn_line_note( + invoice.delivery_note_ids[0] + ): sequence = invoice.invoice_line_ids[0].sequence - 1 new_lines.append( ( @@ -123,40 +141,21 @@ def update_delivery_note_lines(self): ) ) else: - sequence = 1 - done_invoice_lines = self.env["account.move.line"] - for dn in invoice.mapped("delivery_note_ids").sorted(key="name"): - dn_invoice_lines = invoice.invoice_line_ids.filtered( - lambda x: x not in done_invoice_lines - and dn - in x.mapped( - "sale_line_ids.delivery_note_line_ids.delivery_note_id" + for dn in inv_line_by_dn: + if not dn or invoice._has_dn_line_note(dn): + continue + # import wdb; wdb.set_trace() + new_lines_vals = self._prepare_note_dn_value(sequence, dn) + new_lines.append( + ( + 0, + False, + new_lines_vals, ) - # fixme test invoice from 2 sale lines ) - done_invoice_lines |= dn_invoice_lines - for note_line in dn.line_ids.filtered( - lambda l: l.invoice_status == DOMAIN_INVOICE_STATUSES[2] - ): - for invoice_line in dn_invoice_lines: - if ( - note_line - in invoice_line.sale_line_ids.delivery_note_line_ids - ): - invoice_line.delivery_note_id = ( - note_line.delivery_note_id.id - ) - if dn_invoice_lines: - new_lines.append( - ( - 0, - False, - self._prepare_note_dn_value(sequence, dn), - ) - ) - sequence += 1 - for invoice_line in dn_invoice_lines: - invoice_line.sequence = sequence + sequence += 1 + for line in inv_line_by_dn[dn]: + line.sequence = sequence sequence += 1 invoice.write({"line_ids": new_lines}) @@ -192,4 +191,7 @@ 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", readonly=True, copy=False + ) note_dn = fields.Boolean(string="Note DN") diff --git a/l10n_it_delivery_note/models/sale_order.py b/l10n_it_delivery_note/models/sale_order.py index c401f7885909..1b352846e1f9 100644 --- a/l10n_it_delivery_note/models/sale_order.py +++ b/l10n_it_delivery_note/models/sale_order.py @@ -1,7 +1,8 @@ # Copyright (c) 2019, Link IT Europe Srl # @author: Matteo Bilotta -from odoo import api, fields, models +from odoo import _, api, fields, models +from odoo.exceptions import UserError from .stock_delivery_note import DOMAIN_DELIVERY_NOTE_STATES, DOMAIN_INVOICE_STATUSES @@ -52,63 +53,109 @@ def onchange_partner_id_shipping_info(self): self.update(values) - def _assign_delivery_notes_invoices(self, invoice_ids): + def _cancel_delivery_note_lines(self): order_lines = self.mapped("order_line").filtered( lambda l: l.is_invoiced and l.delivery_note_line_ids ) - delivery_note_lines = order_lines.mapped("delivery_note_line_ids").filtered( lambda l: l.is_invoiceable ) delivery_notes = delivery_note_lines.mapped("delivery_note_id") - ready_delivery_notes = delivery_notes.filtered( lambda n: n.state != DOMAIN_DELIVERY_NOTE_STATES[0] ) - draft_delivery_notes = delivery_notes - ready_delivery_notes draft_delivery_note_lines = ( draft_delivery_notes.mapped("line_ids") & delivery_note_lines ) - - ready_delivery_note_lines = delivery_note_lines - draft_delivery_note_lines - - # - # TODO: È necessario gestire il caso di fatturazione splittata - # di una stessa riga d'ordine associata ad una sola - # picking (e di conseguenza, ad un solo DdT)? - # Può essere, invece, un caso "borderline" - # da lasciar gestire all'operatore? - # Personalmente, non lo gestirei e delegherei - # all'operatore questa responsabilità... - # - draft_delivery_note_lines.write( {"invoice_status": DOMAIN_INVOICE_STATUSES[0], "sale_line_id": None} ) - ready_delivery_note_lines.write({"invoice_status": DOMAIN_INVOICE_STATUSES[2]}) - for ready_delivery_note in ready_delivery_notes: - ready_invoice_ids = [ - invoice_id - for invoice_id in ready_delivery_note.sale_ids.mapped("invoice_ids").ids - if invoice_id in invoice_ids - ] - ready_delivery_note.write( - {"invoice_ids": [(4, invoice_id) for invoice_id in ready_invoice_ids]} + def _assign_delivery_notes_invoices(self, invoice_ids): + if not invoice_ids: + return + + self._cancel_delivery_note_lines() + + all_invoice_lines = invoice_ids.invoice_line_ids + for sol in self.order_line: + if not (sol.is_invoiced and sol.delivery_note_line_ids): + continue + dn_lines = sol.delivery_note_line_ids.filtered( + lambda l: l.is_invoiceable + and l.delivery_note_id.state + not in ( + DOMAIN_DELIVERY_NOTE_STATES[0], # draft + DOMAIN_DELIVERY_NOTE_STATES[-1], # cancel + ) ) - - ready_delivery_notes._compute_invoice_status() + if not dn_lines: + continue + inv_lines = all_invoice_lines.filtered( + lambda line, s=sol: s in line.sale_line_ids + ).with_context(check_move_validity=False) + inv_line = inv_lines[0] # safety guard + inv_line.write( + { + "delivery_note_line_id": dn_lines[0], + "delivery_note_id": dn_lines[0].delivery_note_id, + } + ) + if len(dn_lines) > 1: + inv_line.quantity = dn_lines[0].product_qty + move_id = inv_line.move_id + remaining_dn_lines = dn_lines[1:] + product = sol.product_id + for dn_line in remaining_dn_lines: + move_line = dn_line.move_id.move_line_ids.filtered( + lambda l, p=product: l.product_id == p + ) + if len(move_line) != 1: + raise UserError( + _( + "No unique matching move line was found for %(sol)s in" + " Stock Move %(move)s" + ) + % { + "sol": sol.name, + "move": dn_line.move_id.name, + } + ) + new_data = inv_line.copy_data( + { + "quantity": move_line.qty_done, + "price_unit": inv_line.price_unit, + "delivery_note_line_id": dn_line.id, + "delivery_note_id": dn_line.delivery_note_id.id, + "sale_line_ids": [(6, 0, inv_line.sale_line_ids.ids)], + } + )[0] + move_id.write({"invoice_line_ids": [(0, 0, new_data)]}) + # We are setting `inv_line.quantity` again because + # `_move_autocomplete_invoice_lines_write()` applies the new changes on + # a temporary copy of the original invoice that fetches outdated data + # thus requiring a second write + inv_line.quantity = dn_lines[0].product_qty + move_id._onchange_invoice_line_ids() + dn_lines.write( + { + "invoice_status": DOMAIN_INVOICE_STATUSES[2], + } + ) + for dn in dn_lines.mapped("delivery_note_id"): + dn.invoice_ids += inv_lines.mapped("move_id") + dn._compute_invoice_status() + invoice_ids._check_balanced() def _generate_delivery_note_lines(self, invoice_ids): - invoices = self.env["account.move"].browse(invoice_ids) - invoices.update_delivery_note_lines() + invoice_ids.update_delivery_note_lines() def _create_invoices(self, grouped=False, final=False, date=None): invoice_ids = super()._create_invoices(grouped=grouped, final=final, date=date) - self._assign_delivery_notes_invoices(invoice_ids.ids) - self._generate_delivery_note_lines(invoice_ids.ids) + self._assign_delivery_notes_invoices(invoice_ids) + self._generate_delivery_note_lines(invoice_ids) return invoice_ids 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 1a9a0b638c42..ffa0e714c2f9 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 @@ -1356,10 +1356,12 @@ def test_invoicing_multiple_dn(self): invoice.action_post() self.assertEqual(invoice.state, "posted") self.assertEqual( - invoice.invoice_line_ids.filtered( - lambda inv_line: inv_line.product_id.id - == self.right_corner_desk_line[2]["product_id"] - ).quantity, + sum( + invoice.invoice_line_ids.filtered( + lambda inv_line: inv_line.product_id.id + == self.right_corner_desk_line[2]["product_id"] + ).mapped("quantity") + ), 2, ) self.assertEqual( @@ -1379,6 +1381,20 @@ def test_invoicing_multiple_dn(self): f'Delivery Note "{back_dn.name}" of {back_dn.date.strftime(DATE_FORMAT)}', invoice.invoice_line_ids.mapped("name"), ) + invoice_lines = invoice.invoice_line_ids.sorted("sequence") + for delivery_note in invoice_lines.mapped("delivery_note_id"): + inv_dn_lines = invoice_lines.filtered( + lambda l, dn=delivery_note: l.delivery_note_id == dn + ) + note_line = inv_dn_lines.filtered(lambda l: l.note_dn) + self.assertEqual(len(note_line), 1) + # Check that the first line of a particular dn is a note + self.assertEqual(inv_dn_lines[0], note_line) + # Check that all lines of a particular dn are neighbouring each other + self.assertEqual( + len(inv_dn_lines), + int(inv_dn_lines[-1].sequence) - int(inv_dn_lines[0].sequence) + 1, + ) def test_invoicing_multi_dn_multi_so_same_product(self): self.env["ir.config_parameter"].sudo().set_param(