Skip to content

Commit 9eafc83

Browse files
BorrusoBabbato
andcommitted
[REF] l10n_it_delivery_note: refactoring invoice generation and preserving sales lines in invoice lines including notes and sections
Co-authored-by: Babbato <abarbato@dinamicheaziendali.it>
1 parent f36d8aa commit 9eafc83

File tree

3 files changed

+355
-75
lines changed

3 files changed

+355
-75
lines changed

l10n_it_delivery_note/models/sale_order.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,9 @@ def _create_invoices(self, grouped=False, final=False, date=None):
129129
# - "Create Invoice" button should appear only when new delivery notes exist
130130
# This would avoid bypassing standard invoicing when delivery notes exist
131131
# but user wants to invoice directly (e.g., for multi-delivery orders).
132-
if self.delivery_note_ids:
132+
if any(dn.invoice_status == "to invoice" for dn in self.delivery_note_ids):
133133
self.delivery_note_ids.action_invoice(
134-
invoice_method="service", final=final, sale_orders=self
134+
invoice_method="service", final=final, sale_orders=self, grouped=grouped
135135
)
136136
# Assign delivery notes to invoices and update statuses
137137
self._assign_delivery_notes_invoices(self.invoice_ids.ids)

l10n_it_delivery_note/models/stock_delivery_note.py

Lines changed: 203 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
66

77
import datetime
8+
from itertools import groupby
89

910
from odoo import Command, api, fields, models
1011
from odoo.exceptions import AccessError, UserError
@@ -701,6 +702,80 @@ def _prepare_invoice(self, sale_orders):
701702
)
702703
return invoice_vals
703704

705+
def _build_dn_map(self, sale_orders):
706+
"""Map delivery note lines by sale_line_id."""
707+
dn_map = {}
708+
for dn in self.sorted(key=lambda d: (d.date, d.name)):
709+
for line in dn.line_ids:
710+
if line.sale_line_id and (
711+
not sale_orders or line.sale_line_id.order_id in sale_orders
712+
):
713+
dn_map.setdefault(line.sale_line_id.id, []).append((dn, line))
714+
return dn_map
715+
716+
def _append_dn_linked_lines(self, vals_list, dn_map, sequence, current_dn):
717+
"""Append product lines coming from delivery notes, grouped by DN."""
718+
account_move = self.env["account.move"]
719+
720+
for dn, dn_line in dn_map:
721+
if current_dn != dn:
722+
vals_list.append(
723+
Command.create(account_move._prepare_note_dn_value(sequence, dn))
724+
)
725+
sequence += 1
726+
current_dn = dn
727+
728+
if returned_moves := dn_line.mapped("move_id.returned_move_ids"):
729+
return_qty = sum(returned_moves.mapped("quantity"))
730+
product_qty = dn_line.product_qty - return_qty
731+
else:
732+
product_qty = dn_line.product_qty
733+
734+
vals = dn_line._prepare_invoice_line(
735+
sequence=sequence,
736+
quantity=product_qty,
737+
)
738+
vals_list.append(Command.create(vals))
739+
sequence += 1
740+
741+
return sequence, current_dn
742+
743+
def _append_sale_order_lines(self, vals_list, sale_orders, dn_map, sequence):
744+
"""Append invoice lines from sale orders preserving SO line order."""
745+
current_dn = None
746+
for order in sale_orders:
747+
so_lines = order.order_line.sorted(key=lambda ln: ln.sequence)
748+
749+
for line in so_lines:
750+
# Add SO lines notes/sections
751+
if line.display_type and not line.is_downpayment:
752+
vals = line._prepare_invoice_line(sequence=sequence)
753+
vals_list.append(Command.create(vals))
754+
sequence += 1
755+
continue
756+
757+
if line.id in dn_map and line.order_id == order:
758+
# Add delivery note lines
759+
sequence, current_dn = self._append_dn_linked_lines(
760+
vals_list=vals_list,
761+
dn_map=dn_map[line.id],
762+
sequence=sequence,
763+
current_dn=current_dn,
764+
)
765+
else:
766+
# Add remaining SO lines not in delivery notes
767+
if (
768+
line.product_id.type != "service"
769+
and line.qty_to_invoice > 0
770+
and not line.is_downpayment
771+
):
772+
vals = line._prepare_invoice_line(sequence=sequence)
773+
vals["sale_line_ids"] = [Command.link(line.id)]
774+
vals_list.append(Command.create(vals))
775+
sequence += 1
776+
777+
return sequence
778+
704779
def _prepare_invoice_lines(self, sale_orders, invoice_method, final):
705780
"""Creates invoice lines from delivery note lines,
706781
sorting delivery notes by date and name.
@@ -717,48 +792,16 @@ def _prepare_invoice_lines(self, sale_orders, invoice_method, final):
717792
Sets the created lines in the invoice values dictionary"""
718793
vals_list = []
719794
sequence = 1
720-
account_move = self.env["account.move"]
721-
722-
# Add delivery note lines as sections
723-
for dn in self.sorted(key=lambda d: (d.date, d.name)):
724-
vals_list.append(
725-
Command.create(account_move._prepare_note_dn_value(sequence, dn))
726-
)
727-
sequence += 1
728795

729-
# Get delivery note lines, filtered by sale_orders if provided
730-
dn_line_ids = dn.line_ids
731-
if sale_orders:
732-
dn_line_ids = dn_line_ids.filtered(
733-
lambda line: line.sale_line_id
734-
and line.sale_line_id.order_id in sale_orders
735-
)
796+
dn_map = self._build_dn_map(sale_orders)
736797

737-
# Add delivery note lines
738-
for line in dn_line_ids:
739-
if line.mapped("move_id.returned_move_ids"):
740-
return_qty = sum(line.mapped("move_id.returned_move_ids.quantity"))
741-
product_qty = line.product_qty - return_qty
742-
else:
743-
product_qty = line.product_qty
744-
vals = line._prepare_invoice_line(
745-
sequence=sequence, quantity=product_qty
746-
)
747-
vals_list.append(Command.create(vals))
748-
sequence += 1
749-
750-
# Add remaining SO lines not in delivery notes
751798
if sale_orders:
752-
sale_lines = sale_orders.mapped("order_line").filtered(
753-
lambda ol: not ol.delivery_note_line_ids
754-
and ol.product_id.type != "service"
755-
and ol.qty_to_invoice > 0
799+
sequence = self._append_sale_order_lines(
800+
vals_list=vals_list,
801+
sale_orders=sale_orders,
802+
dn_map=dn_map,
803+
sequence=sequence,
756804
)
757-
for line in sale_lines:
758-
vals = line._prepare_invoice_line(sequence=sequence)
759-
vals["sale_line_ids"] = [(4, line.id)]
760-
vals_list.append(Command.create(vals))
761-
sequence += 1
762805

763806
# Add service lines if requested
764807
if invoice_method == "service":
@@ -769,7 +812,7 @@ def _prepare_invoice_lines(self, sale_orders, invoice_method, final):
769812
)
770813
for line in service_lines:
771814
vals = line._prepare_invoice_line(sequence=sequence)
772-
vals["sale_line_ids"] = [(4, line.id)]
815+
vals["sale_line_ids"] = [Command.link(line.id)]
773816
vals_list.append(Command.create(vals))
774817
sequence += 1
775818

@@ -799,7 +842,7 @@ def _prepare_invoice_lines(self, sale_orders, invoice_method, final):
799842

800843
return vals_list
801844

802-
def _update_invoice_statuses(self, invoice, sale_orders):
845+
def _update_invoice_statuses(self, invoices, sale_orders):
803846
"""Update invoice statuses after invoice creation"""
804847
# Get all delivery note lines with sale orders
805848
delivery_note_lines = self.mapped("line_ids").filtered(
@@ -817,20 +860,92 @@ def _update_invoice_statuses(self, invoice, sale_orders):
817860

818861
# Link invoice to delivery notes
819862
for delivery_note in self:
820-
delivery_note.write({"invoice_ids": [(4, invoice.id)]})
863+
delivery_note.write(
864+
{"invoice_ids": [Command.link(invoice.id) for invoice in invoices]}
865+
)
821866

822867
# Recompute overall invoice status
823868
self._compute_invoice_status()
824869

825-
def action_invoice(self, invoice_method=False, final=False, sale_orders=None):
870+
def _build_invoice_vals_list(
871+
self, sale_orders, delivery_notes, invoice_method=False, final=False
872+
):
873+
invoice_vals_list = []
874+
875+
orders = delivery_notes.sale_ids & sale_orders
876+
invoice_vals = delivery_notes._prepare_invoice(orders)
877+
invoice_vals["invoice_line_ids"] = delivery_notes._prepare_invoice_lines(
878+
orders, invoice_method, final
879+
)
880+
invoice_vals_list.append(invoice_vals)
881+
882+
return invoice_vals_list
883+
884+
def _group_invoice_vals(self, invoice_vals_list, sale_orders):
885+
new_invoice_vals_list = []
886+
invoice_grouping_keys = sale_orders._get_invoice_grouping_keys()
887+
888+
invoice_vals_list = sorted(
889+
invoice_vals_list,
890+
key=lambda vals: [vals.get(k) for k in invoice_grouping_keys],
891+
)
892+
893+
for _keys, invoices in groupby(
894+
invoice_vals_list,
895+
key=lambda vals: [vals.get(k) for k in invoice_grouping_keys],
896+
):
897+
grouped_vals = None
898+
origins = set()
899+
payment_refs = set()
900+
refs = set()
901+
902+
for invoice_vals in invoices:
903+
if not grouped_vals:
904+
grouped_vals = invoice_vals
905+
else:
906+
grouped_vals["invoice_line_ids"] += invoice_vals["invoice_line_ids"]
907+
908+
if invoice_vals.get("invoice_origin"):
909+
origins.add(invoice_vals["invoice_origin"])
910+
if invoice_vals.get("payment_reference"):
911+
payment_refs.add(invoice_vals["payment_reference"])
912+
if invoice_vals.get("ref"):
913+
refs.add(invoice_vals["ref"])
914+
915+
grouped_vals.update(
916+
{
917+
"ref": ", ".join(refs)[:2000],
918+
"invoice_origin": ", ".join(origins),
919+
"payment_reference": payment_refs.pop()
920+
if len(payment_refs) == 1
921+
else False,
922+
}
923+
)
924+
new_invoice_vals_list.append(grouped_vals)
925+
926+
return new_invoice_vals_list
927+
928+
def action_invoice(
929+
self, invoice_method=False, final=False, sale_orders=None, grouped=False
930+
):
826931
delivery_note_ids = self.filtered(
827932
lambda dn: dn.state == "confirm" and dn.invoice_status == "to invoice"
828933
)
829934
delivery_note_ids._check_delivery_notes_before_invoicing()
830935

831936
# If not passed explicitly, get all sale orders from delivery notes
832-
if sale_orders is None:
833-
sale_orders = delivery_note_ids.sale_ids
937+
sale_orders = sale_orders or delivery_note_ids.sale_ids
938+
if not sale_orders:
939+
return self.env["account.move"]
940+
941+
# Check if the user has access rights to create invoices
942+
if not self.env["account.move"].check_access("create"):
943+
try:
944+
self.check_access("write")
945+
except AccessError:
946+
return self.env["account.move"]
947+
948+
moves = self.env["account.move"]
834949

835950
# Group by payment term (include empty payment term)
836951
payment_terms = sale_orders.payment_term_id or [False]
@@ -850,13 +965,6 @@ def action_invoice(self, invoice_method=False, final=False, sale_orders=None):
850965
if not filtered_sales:
851966
continue
852967

853-
# Check if the user has access rights to create invoices
854-
if not self.env["account.move"].check_access("create"):
855-
try:
856-
self.check_access("write")
857-
except AccessError:
858-
return self.env["account.move"]
859-
860968
# Filter delivery notes related to the sale orders
861969
filter_delivery_notes = delivery_note_ids.filtered(
862970
lambda dn, so=filtered_sales: all(
@@ -865,36 +973,58 @@ def action_invoice(self, invoice_method=False, final=False, sale_orders=None):
865973
or all(so_id in dn.sale_ids.ids for so_id in so.ids)
866974
)
867975

868-
# Prepare invoice
869-
invoice_vals = filter_delivery_notes._prepare_invoice(filtered_sales)
870-
871-
# Prepare invoice lines
872-
vals_list = filter_delivery_notes._prepare_invoice_lines(
873-
filtered_sales, invoice_method, final
976+
# prepare invoice lines for every SO
977+
invoice_vals_list = self._build_invoice_vals_list(
978+
filtered_sales, filter_delivery_notes, invoice_method, final
874979
)
980+
if not invoice_vals_list:
981+
continue
875982

876-
# invoice creation
877-
invoice_vals["invoice_line_ids"] = vals_list
878-
invoice_id = (
879-
self.env["account.move"]
880-
.sudo()
881-
.with_context(default_move_type="out_invoice")
882-
.create(invoice_vals)
883-
)
983+
# GROUPING
984+
if not grouped:
985+
invoice_vals_list = self._group_invoice_vals(
986+
invoice_vals_list, sale_orders
987+
)
988+
989+
for invoice_vals in invoice_vals_list:
990+
sequence = 1
991+
for line in invoice_vals.get("invoice_line_ids", []):
992+
if len(line) >= 3 and isinstance(line[2], dict):
993+
line[2]["sequence"] = self.env[
994+
"sale.order.line"
995+
]._get_invoice_line_sequence(
996+
new=sequence,
997+
old=line[2].get("sequence"),
998+
)
999+
sequence += 1
8841000

885-
# Update statuses
886-
filter_delivery_notes._update_invoice_statuses(invoice_id, filtered_sales)
1001+
# CREATE invoices
1002+
moves = self.env["account.move"]
1003+
1004+
for invoice_vals in invoice_vals_list:
1005+
move = (
1006+
self.env["account.move"]
1007+
.sudo()
1008+
.with_context(default_move_type="out_invoice")
1009+
.create(invoice_vals)
1010+
)
1011+
moves |= move
1012+
1013+
# update delivery notes status
1014+
filter_delivery_notes._update_invoice_statuses(moves, sale_orders)
8871015

8881016
# Some moves might actually be refunds: convert them if the total amount is
8891017
# negative.
8901018
# We do this after the moves have been created since we need taxes, etc.
8911019
# to know if the total is actually negative or not
8921020
if final and (
893-
move_to_switch := invoice_id.sudo().filtered(lambda m: m.amount_total < 0)
1021+
moves_to_switch := moves.sudo().filtered(lambda m: m.amount_total < 0)
8941022
):
895-
with self.env.protecting([invoice_id._fields["team_id"]], move_to_switch):
896-
move_to_switch.action_switch_move_type()
897-
invoice_id._set_reversed_entry(move_to_switch)
1023+
with self.env.protecting([moves._fields["team_id"]], moves_to_switch):
1024+
moves_to_switch.action_switch_move_type()
1025+
sale_orders.invoice_ids._set_reversed_entry(moves_to_switch)
1026+
1027+
return moves
8981028

8991029
def action_done(self):
9001030
self.write({"state": DOMAIN_DELIVERY_NOTE_STATES[3]})
@@ -972,7 +1102,7 @@ def _create_detail_lines(self, move_ids):
9721102
moves = self.env["stock.move"].browse(move_ids)
9731103
lines_vals = self.env["stock.delivery.note.line"]._prepare_detail_lines(moves)
9741104

975-
self.write({"line_ids": [(0, False, vals) for vals in lines_vals]})
1105+
self.write({"line_ids": [Command.create(vals) for vals in lines_vals]})
9761106

9771107
def _delete_detail_lines(self, move_ids):
9781108
if not move_ids:
@@ -982,7 +1112,7 @@ def _delete_detail_lines(self, move_ids):
9821112
[("move_id", "in", move_ids)]
9831113
)
9841114

985-
self.write({"line_ids": [(2, line.id, False) for line in lines]})
1115+
self.write({"line_ids": [Command.delete(line.id) for line in lines]})
9861116

9871117
def update_detail_lines(self):
9881118
for note in self:

0 commit comments

Comments
 (0)