1
+ from odoo import models , fields , api
2
+ from datetime import datetime
3
+
4
+
5
+ class SaleOrderLine (models .Model ):
6
+ _inherit = 'sale.order.line'
7
+
8
+ # Computed stock-related fields to be shown per product line
9
+ forecasted_qty = fields .Float (string = "Forecasted Qty" , compute = '_compute_forecasted_qty' , store = False )
10
+ qty_on_hand = fields .Float (string = "On Hand Qty" , compute = '_compute_qty_on_hand' , store = False )
11
+ qty_difference = fields .Float (string = "Qty Difference" , compute = '_compute_qty_difference' , store = False )
12
+
13
+ @api .depends ('product_id' )
14
+ def _compute_forecasted_qty (self ):
15
+ """Fetches forecasted quantity (virtual stock) for selected product."""
16
+ for line in self :
17
+ if line .product_id :
18
+ line .forecasted_qty = line .product_id .virtual_available
19
+ else :
20
+ line .forecasted_qty = 0.0
21
+
22
+ @api .depends ('product_id' )
23
+ def _compute_qty_on_hand (self ):
24
+ """Fetches current on-hand quantity for selected product."""
25
+ for line in self :
26
+ if line .product_id :
27
+ line .qty_on_hand = line .product_id .qty_available
28
+ else :
29
+ line .qty_on_hand = 0.0
30
+
31
+ @api .depends ('forecasted_qty' , 'qty_on_hand' )
32
+ def _compute_qty_difference (self ):
33
+ """Computes difference between forecasted and on-hand quantity."""
34
+ for line in self :
35
+ line .qty_difference = line .forecasted_qty - line .qty_on_hand
36
+
37
+
38
+ # Inherit SaleOrder to display last sold products for a customer
39
+ class SaleOrder (models .Model ):
40
+ _inherit = 'sale.order'
41
+
42
+ # Computed list of recent products sold to the selected customer
43
+ last_sold_products = fields .Many2many (
44
+ 'product.product' ,
45
+ compute = '_compute_last_sold_products' ,
46
+ string = "Last 5 Sold Products"
47
+ )
48
+
49
+ # Last invoice date for the selected customer
50
+ last_invoice_date = fields .Date (
51
+ compute = '_compute_last_invoice_date' ,
52
+ string = "Last Invoice Date"
53
+ )
54
+
55
+ # Textual info summary of last sold products and invoice timestamps
56
+ last_sold_products_info = fields .Text (
57
+ compute = '_compute_last_sold_products_info' ,
58
+ string = "Last Sold Products Info"
59
+ )
60
+
61
+ # Used to conditionally hide info block if there's no history
62
+ invisible_last_sold_info = fields .Boolean (
63
+ compute = '_compute_invisible_last_sold_info' ,
64
+ string = "Hide Last Sold Info"
65
+ )
66
+
67
+ @api .depends ('partner_id' )
68
+ def _compute_last_sold_products (self ):
69
+ """
70
+ Retrieves unique products from posted customer invoices,
71
+ ordered by invoice date for selected partner.
72
+ """
73
+ for order in self :
74
+ if not order .partner_id :
75
+ order .last_sold_products = [(5 , 0 , 0 )]
76
+ continue
77
+
78
+ # Search for invoice lines with products for the customer
79
+ lines = self .env ['account.move.line' ].search ([
80
+ ('move_id.partner_id' , '=' , order .partner_id .id ),
81
+ ('move_id.move_type' , '=' , 'out_invoice' ),
82
+ ('move_id.state' , '=' , 'posted' ),
83
+ ('display_type' , '=' , 'product' ),
84
+ ('product_id' , '!=' , False )
85
+ ], order = 'date desc, id desc' )
86
+
87
+ # Deduplicate products to keep the latest 5 sold ones
88
+ product_ids_ordered = []
89
+ seen_products = set ()
90
+ for line in lines :
91
+ if line .product_id .id not in seen_products :
92
+ product_ids_ordered .append (line .product_id .id )
93
+ seen_products .add (line .product_id .id )
94
+
95
+ order .last_sold_products = [(6 , 0 , product_ids_ordered )]
96
+
97
+ @api .depends ('partner_id' )
98
+ def _compute_last_invoice_date (self ):
99
+ """
100
+ Finds the most recent invoice date for this customer.
101
+ """
102
+ for order in self :
103
+ if not order .partner_id :
104
+ order .last_invoice_date = False
105
+ continue
106
+
107
+ last_invoice = self .env ['account.move' ].search ([
108
+ ('partner_id' , '=' , order .partner_id .id ),
109
+ ('move_type' , '=' , 'out_invoice' ),
110
+ ('state' , '=' , 'posted' ),
111
+ ('invoice_date' , '!=' , False )
112
+ ], order = 'invoice_date desc' , limit = 1 )
113
+
114
+ order .last_invoice_date = last_invoice .invoice_date if last_invoice else False
115
+
116
+ @api .depends ('last_sold_products' , 'partner_id' )
117
+ def _compute_last_sold_products_info (self ):
118
+ """
119
+ Generates readable lines like:
120
+ • Product A (Invoiced 3 days ago on 2024-06-15)
121
+ """
122
+ for order in self :
123
+ if not order .last_sold_products :
124
+ order .last_sold_products_info = "No recent products found for this customer."
125
+ continue
126
+
127
+ product_ids = order .last_sold_products .ids
128
+ last_invoice_dates = {}
129
+
130
+ # Find invoice dates per product
131
+ all_lines = self .env ['account.move.line' ].search ([
132
+ ('move_id.partner_id' , '=' , order .partner_id .id ),
133
+ ('product_id' , 'in' , product_ids ),
134
+ ('move_id.move_type' , '=' , 'out_invoice' ),
135
+ ('move_id.state' , '=' , 'posted' ),
136
+ ('move_id.invoice_date' , '!=' , False )
137
+ ])
138
+
139
+ for line in all_lines :
140
+ product_id = line .product_id .id
141
+ invoice_date = line .move_id .invoice_date
142
+ if product_id not in last_invoice_dates or invoice_date > last_invoice_dates .get (product_id ):
143
+ last_invoice_dates [product_id ] = invoice_date
144
+
145
+ info_lines = []
146
+ current_dt = fields .Datetime .now ()
147
+
148
+ for product in order .last_sold_products :
149
+ last_date = last_invoice_dates .get (product .id )
150
+ if last_date :
151
+ # Time difference from now to last invoice date
152
+ invoice_dt = datetime .combine (last_date , datetime .min .time ())
153
+ time_diff = current_dt - invoice_dt
154
+ days = time_diff .days
155
+
156
+ if days > 1 :
157
+ time_str = f"{ days } days ago"
158
+ elif days == 1 :
159
+ time_str = "1 day ago"
160
+ else :
161
+ time_str = f"{ time_diff .seconds // 3600 } hours ago"
162
+
163
+ info_lines .append (f"• { product .display_name } (Invoiced { time_str } on { last_date .strftime ('%Y-%m-%d' )} )" )
164
+ else :
165
+ info_lines .append (f"• { product .display_name } (No recent invoice found)" )
166
+
167
+ order .last_sold_products_info = "\n " .join (info_lines )
168
+
169
+ @api .depends ('last_sold_products' )
170
+ def _compute_invisible_last_sold_info (self ):
171
+ """Boolean toggle: hide info block if no products available."""
172
+ for order in self :
173
+ order .invisible_last_sold_info = not bool (order .last_sold_products )
0 commit comments