Skip to content

Commit 0147312

Browse files
committed
[IMP] custom_sale_purchase_display: enhance product listing
Improve product selection on Sale Orders, Purchase Orders Before: - Products in Sale Orders were displayed in default order, unrelated to customer history. - No indication of the last invoice date on product cards. - Forecasted and on-hand quantities were not visible at a glance. - No visual indicator for differences in stock levels. - Kanban product catalog in Purchase Orders didn't showed color tags After: - In Sale Orders, products already sold to the selected customer are prioritized and sorted from most recent to oldest invoice. - Remaining products appear afterward in standard order. - Product cards in catalog view now display the last invoice date - Forecasted quantity and on-hand quantity are shown directly on product cards. - Stock differences are highlighted: red for negative (-), green for positive (+).
1 parent fbf9ee9 commit 0147312

File tree

7 files changed

+209
-0
lines changed

7 files changed

+209
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import models
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
'name': 'Sale Order Product History',
3+
'version': '1.0',
4+
'category': 'Sales',
5+
'depends': ['sale_management', 'stock', 'purchase'],
6+
'data': [
7+
'views/purchase_order_form.xml',
8+
'views/product_template_kanban_catalog.xml',
9+
],
10+
'installable': True,
11+
'application': False,
12+
'license': 'LGPL-3'
13+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from . import product_product
2+
from . import product_template
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
from odoo import models, fields, api
2+
3+
4+
class ProductProduct(models.Model):
5+
_inherit = 'product.product'
6+
7+
last_invoice_date = fields.Date(
8+
string="Last Invoice Date",
9+
compute="_compute_last_invoice_data",
10+
store=False
11+
)
12+
last_invoice_time_diff = fields.Char(
13+
string="Last Invoice Time Diff",
14+
compute="_compute_last_invoice_data",
15+
store=False
16+
)
17+
18+
def _compute_last_invoice_data(self):
19+
for product in self:
20+
partner_id = product.env.context.get('sale_order_partner_id') \
21+
or product.env.context.get('purchase_order_partner_id')
22+
23+
is_sale = bool(product.env.context.get('sale_order_partner_id'))
24+
move_type = 'out_invoice' if is_sale else 'in_invoice'
25+
26+
domain = [
27+
('state', '=', 'posted'),
28+
('invoice_date', '!=', False),
29+
('line_ids.product_id', '=', product.id),
30+
('move_type', '=', move_type),
31+
]
32+
if partner_id:
33+
domain.append(('partner_id', '=', partner_id))
34+
35+
move = product.env['account.move'].search(
36+
domain, order='invoice_date desc', limit=1
37+
)
38+
39+
product.last_invoice_date = move.invoice_date if move else False
40+
product.last_invoice_time_diff = (
41+
self._format_time_diff(move.invoice_date) if move else False
42+
)
43+
44+
@api.model
45+
def _get_recent_invoices(self, partner_id, is_sale=True):
46+
if not partner_id:
47+
return []
48+
49+
move_type = 'out_invoice' if is_sale else 'in_invoice'
50+
moves = self.env['account.move'].search([
51+
('partner_id', '=', partner_id),
52+
('move_type', '=', move_type),
53+
('state', '=', 'posted'),
54+
('invoice_date', '!=', False)
55+
], order='invoice_date desc')
56+
57+
recent, seen = [], set()
58+
for mv in moves:
59+
for line in mv.line_ids.filtered('product_id'):
60+
pid = line.product_id.id
61+
if pid not in seen:
62+
recent.append({'pid': pid, 'date': mv.invoice_date})
63+
seen.add(pid)
64+
return recent
65+
66+
@api.model
67+
def _format_time_diff(self, invoice_date):
68+
if not invoice_date:
69+
return ""
70+
days = (fields.Date.today() - invoice_date).days
71+
if days > 365:
72+
return f"{days // 365}y ago"
73+
if days > 30:
74+
return f"{days // 30}mo ago"
75+
if days > 7:
76+
return f"{days // 7}w ago"
77+
if days > 0:
78+
return f"{days}d ago"
79+
return "today"
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
from odoo import models, fields, api
2+
3+
4+
class ProductTemplate(models.Model):
5+
_inherit = 'product.template'
6+
7+
last_invoice_date = fields.Date(
8+
string="Last Invoice Date",
9+
related='product_variant_id.last_invoice_date',
10+
store=False
11+
)
12+
last_invoice_time_diff = fields.Char(
13+
string="Last Invoice Time Diff",
14+
related='product_variant_id.last_invoice_time_diff',
15+
store=False
16+
)
17+
18+
product_variant_id = fields.Many2one(
19+
'product.product',
20+
compute='_compute_product_variant_id',
21+
store=True,
22+
index=True
23+
)
24+
25+
@api.model
26+
def name_search(self, name="", args=None, operator="ilike", limit=100):
27+
args = args or []
28+
29+
partner_id = self.env.context.get('sale_order_partner_id') \
30+
or self.env.context.get('purchase_order_partner_id')
31+
if not partner_id:
32+
return super().name_search(name, args, operator, limit)
33+
34+
is_sale = bool(self.env.context.get('sale_order_partner_id'))
35+
recent_lines = self.env['product.product']._get_recent_invoices(
36+
partner_id=partner_id,
37+
is_sale=is_sale
38+
)
39+
40+
if not recent_lines:
41+
return super().name_search(name, args, operator, limit)
42+
43+
recent_template_ids = list(dict.fromkeys(
44+
self.env['product.product'].browse(rl['pid']).product_tmpl_id.id
45+
for rl in recent_lines
46+
))
47+
48+
base_domain = [('name', operator, name)] + args
49+
50+
recent_templates = self.search(
51+
[('id', 'in', recent_template_ids)] + base_domain,
52+
limit=limit
53+
)
54+
other_templates = self.search(
55+
[('id', 'not in', recent_template_ids)] + base_domain,
56+
limit=max(0, limit - len(recent_templates))
57+
)
58+
59+
results = []
60+
for tmpl_id in recent_template_ids:
61+
tmpl = recent_templates.filtered(lambda t: t.id == tmpl_id)
62+
if tmpl:
63+
td = tmpl.last_invoice_time_diff
64+
label = f"{tmpl.display_name}{td}" if td else tmpl.display_name
65+
results.append((tmpl.id, label))
66+
67+
results.extend((t.id, t.display_name) for t in other_templates)
68+
return results
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<odoo>
3+
<record id="view_product_template_kanban_inherit" model="ir.ui.view">
4+
<field name="name">product.product.kanban.inherit.catalog</field>
5+
<field name="model">product.product</field>
6+
<field name="inherit_id" ref="product.product_view_kanban_catalog"/>
7+
<field name="arch" type="xml">
8+
<xpath expr="//div[contains(@name, 'o_kanban_qty_available')]" position="inside">
9+
<field name="virtual_available" invisible="1"/>
10+
<field name="qty_available" invisible="1"/> <!-- Ensure loaded -->
11+
<field name="last_invoice_date" invisible="1"/> <!-- Ensure loaded -->
12+
<field name="last_invoice_time_diff" invisible="1"/> <!-- Ensure loaded -->
13+
<t t-set="diff" t-value="record.virtual_available.raw_value - record.qty_available.raw_value"/>
14+
<t t-if="diff &gt; 0">
15+
<div style="color: green;">(+ <t t-esc="diff"/>)</div>
16+
</t>
17+
<t t-elif="diff &lt; 0">
18+
<div style="color: red;">(<t t-esc="diff"/>)</div>
19+
</t>
20+
<t t-else="">
21+
<div>(0)</div>
22+
</t>
23+
<!-- Display last invoice info -->
24+
<div t-if="record.last_invoice_time_diff.value" style="font-size: 80%; color: #888;">
25+
⏱ <t t-esc="record.last_invoice_time_diff.value"/>
26+
</div>
27+
<div t-if="record.last_invoice_date.value" style="font-size: 80%; color: #aaa;">
28+
📅 <t t-esc="record.last_invoice_date.value"/>
29+
</div>
30+
</xpath>
31+
</field>
32+
</record>
33+
</odoo>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<odoo>
3+
<record id="view_order_form_inherit_custom" model="ir.ui.view">
4+
<field name="name">purchase.order.form.inherit.custom</field>
5+
<field name="model">purchase.order</field>
6+
<field name="inherit_id" ref="purchase.purchase_order_form"/>
7+
<field name="arch" type="xml">
8+
<xpath expr="//field[@name='order_line']/list/field[@name='product_id']" position="attributes">
9+
<attribute name="context">{'purchase_order_partner_id': parent.partner_id}</attribute>
10+
</xpath>
11+
</field>
12+
</record>
13+
</odoo>

0 commit comments

Comments
 (0)