Skip to content

Commit 3a32fc9

Browse files
committed
[ADD] last_purchased_products: sort selection by recent customer history
Before, the product selection dropdown did not display the time of the previous order and products were not sorted based on how recently they were invoiced for the selected customer. Additionally, the last invoice time and specific quantity calculations were missing from the catalog view. This module enhances the product selection mechanism to assist users in identifying frequently purchased items. Specific improvements include: - Displaying the `last_order` time next to the product name in the dropdown. - Sorting products by `last_invoice_date` specific to the selected customer (partner). - Showing `final_qty` and `last_invoice_time` in the product catalog kanban view. If no customer is selected in the context, the standard selection behavior and sorting apply. Technical Details: - Model `product.product`: Added `last_order` Datetime field, `last_invoice_date` Date Field, `last_invoice_time` field. - Model `product.template`: Added `last_order` Datetime field, `last_invoice_date` Date Field. - Method `name_search`: Overridden to order products as `last_invoice_date`. - Method `_compute_display_name`: Overridden to show `last_order` as to string time at right side of product name in selection dropdown. - Method `_compute_last_order`: to compute `last_order` of shown products in selection dropdown in context of selected customer(`partner_id`). - Method `_compute_last_invoice_date`: to compute `last_invoice_date` to order recently invoiced first shown products in selection dropdown in context of selected customer(`partner_id`). - Method `compute_agotime`: to compute given Datetime or Date to string format. - View `sale_order_views`: Updated `product_template_id` and catalog button to pass the selected `partner_id` in the context as `customer`. - View `account_move_views`: Updated `product_id` and catalog button to pass the selected `partner_id` in the context as `customer`. - View `purchase_order_views`: Updated `product_id` to pass the selected `partner_id` in the context as `vendor`. - View `product_views`: Updated `product_view_kanban_catalog` to display `final_qty` (`virtual_available` - `qty_available`) next to `On Hand` and `last_invoice_time` after `qty_available` in `product_view_kanban_catalog`. - Template `order_line`: added `uomDisplayName` after `price`.
1 parent b68a192 commit 3a32fc9

File tree

10 files changed

+344
-0
lines changed

10 files changed

+344
-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: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "Last Purchased Products",
3+
"version": "1.0",
4+
"depends": ["sale_management", "purchase", "stock"],
5+
"author": "danal",
6+
"category": "Category",
7+
"license": "LGPL-3",
8+
"data": [
9+
"views/sale_order_views.xml",
10+
"views/account_move_views.xml",
11+
"views/purchase_order_views.xml",
12+
"views/product_views.xml"
13+
],
14+
"assets": {
15+
'web.assets_backend': [
16+
'last_purchased_products/static/src/product_catalog/**/*.xml',
17+
]
18+
}
19+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Part of Odoo. See LICENSE file for full copyright and licensing details.
2+
3+
from . import product_template
4+
from . import product
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
from dateutil.relativedelta import relativedelta
2+
import datetime
3+
4+
from odoo import models, api, fields
5+
6+
7+
class ProductProduct(models.Model):
8+
_inherit = "product.product"
9+
10+
last_order = fields.Datetime(compute="_compute_last_order")
11+
last_invoice_date = fields.Date(compute="_compute_last_invoice_date")
12+
last_invoice_time = fields.Char(compute="_compute_invoice_time")
13+
14+
@api.depends_context("customer", "formatted_display_name")
15+
def _compute_display_name(self):
16+
res = super()._compute_display_name()
17+
if not self.env.context.get("customer") or not self.env.context.get(
18+
"formatted_display_name"
19+
):
20+
return res
21+
compute_agotime_ref = self.compute_agotime
22+
for product in self:
23+
if not product.last_order:
24+
continue
25+
ago = compute_agotime_ref(fields.Datetime.now(), product.last_order)
26+
current_product_name = product.display_name or ""
27+
if self.env.context.get("formatted_display_name"):
28+
if ago:
29+
time_postfix = f"\t--{ago}--"
30+
else:
31+
time_postfix = ""
32+
product.display_name = f"{current_product_name}{time_postfix}"
33+
else:
34+
product.display_name = f"{current_product_name}"
35+
36+
@api.depends_context("customer", "vendor")
37+
def _compute_last_invoice_date(self):
38+
customer_id = self.env.context.get("customer")
39+
vendor_id = self.env.context.get("vendor")
40+
domain = [
41+
("product_id", "in", self.ids),
42+
("parent_state", "=", "posted"),
43+
]
44+
if customer_id:
45+
domain.append(("partner_id", "=", customer_id))
46+
else:
47+
domain.append(("move_id.move_type", "=", "in_invoice"))
48+
domain.append(("partner_id", "=", vendor_id))
49+
50+
last_invoice_dates = self.env["account.move.line"].search(
51+
domain, order="invoice_date desc"
52+
)
53+
invoice_dates = {}
54+
for invoice in last_invoice_dates:
55+
if invoice.product_id.id not in invoice_dates:
56+
invoice_dates[invoice.product_id.id] = invoice.invoice_date
57+
for product in self:
58+
product.last_invoice_date = invoice_dates.get(product.id, False)
59+
60+
@api.depends_context("customer")
61+
def _compute_last_order(self):
62+
last_orders = self.env["sale.order.line"].search(
63+
[
64+
("order_id.partner_id", "=", self.env.context.get("customer")),
65+
("product_id", "in", self.ids),
66+
("state", "=", "sale"),
67+
],
68+
order="order_id desc",
69+
)
70+
order_dates = {}
71+
for order in last_orders:
72+
if order.product_id.id not in order_dates:
73+
order_dates[order.product_id.id] = order.order_id.date_order
74+
for product in self:
75+
product.last_order = order_dates.get(product.id, False)
76+
77+
@api.depends('last_invoice_date')
78+
def _compute_invoice_time(self):
79+
compute_agotime_ref = self.compute_agotime
80+
for product in self:
81+
if product.last_invoice_date:
82+
ago = compute_agotime_ref(fields.Date.today(), product.last_invoice_date)
83+
if not ago:
84+
ago = "Today"
85+
product.last_invoice_time = ago
86+
else:
87+
product.last_invoice_time = False
88+
89+
@api.model
90+
def name_search(self, name="", args=None, operator="ilike", limit=100):
91+
if not self.env.context.get("customer") and not self.env.context.get("vendor"):
92+
return super().name_search(name, args, operator, limit)
93+
res = super().name_search(name, args, operator, limit=100)
94+
ids = [r[0] for r in res]
95+
records = self.browse(ids)
96+
records.mapped("last_invoice_date")
97+
sorted_records = records.sorted(
98+
key=(lambda r: r.last_invoice_date or datetime.date.min), reverse=True
99+
)
100+
return [(r.id, r.display_name) for r in sorted_records][:limit]
101+
102+
def compute_agotime(self, current, datetime_field):
103+
rd = relativedelta(current, datetime_field)
104+
if rd.years:
105+
ago = f"{rd.years}y"
106+
elif rd.months:
107+
ago = f"{rd.months}mo"
108+
elif rd.days:
109+
ago = f"{rd.days}d"
110+
elif rd.hours:
111+
ago = f"{rd.hours}h"
112+
elif rd.minutes:
113+
ago = f"{rd.minutes}m"
114+
elif rd.seconds:
115+
ago = f"{rd.seconds}s"
116+
else:
117+
ago = ""
118+
119+
return ago
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
from dateutil.relativedelta import relativedelta
2+
import datetime
3+
4+
from odoo import models, api, fields
5+
6+
7+
class ProductTemplate(models.Model):
8+
_inherit = "product.template"
9+
10+
last_order = fields.Datetime(compute="_compute_last_order")
11+
last_invoice_date = fields.Date(compute="_compute_last_invoice_date")
12+
13+
@api.depends_context("customer", "formatted_display_name")
14+
def _compute_display_name(self):
15+
res = super()._compute_display_name()
16+
if not self.env.context.get("customer") or not self.env.context.get(
17+
"formatted_display_name"
18+
):
19+
return res
20+
21+
compute_agotime_ref = self.compute_agotime
22+
for template in self:
23+
if not template.last_order:
24+
continue
25+
ago = compute_agotime_ref(template.last_order)
26+
current_product_template_name = template.display_name or ""
27+
if self.env.context.get("formatted_display_name"):
28+
if ago:
29+
time_postfix = f"\t--{ago}--"
30+
else:
31+
time_postfix = ""
32+
template.display_name = f"{current_product_template_name}{time_postfix}"
33+
else:
34+
template.display_name = f"{current_product_template_name}"
35+
36+
@api.depends_context("customer")
37+
def _compute_last_order(self):
38+
last_orders = self.env["sale.order.line"].search(
39+
[
40+
("order_id.partner_id", "=", self.env.context.get("customer")),
41+
("product_id.product_tmpl_id", "in", self.ids),
42+
("state", "=", "sale"),
43+
],
44+
order="order_id desc",
45+
)
46+
order_dates = {}
47+
for order in last_orders:
48+
if order.product_id.id not in order_dates:
49+
order_dates[order.product_id.product_tmpl_id.id] = (
50+
order.order_id.date_order
51+
)
52+
for template in self:
53+
template.last_order = order_dates.get(template.id, False)
54+
55+
@api.depends_context("customer")
56+
def _compute_last_invoice_date(self):
57+
last_invoice_dates = self.env["account.move.line"].search(
58+
[
59+
("partner_id", "=", self.env.context.get("customer")),
60+
("product_id.product_tmpl_id", "in", self.ids),
61+
("parent_state", "=", "posted"),
62+
],
63+
order="invoice_date desc",
64+
)
65+
invoice_dates = {}
66+
for invoice in last_invoice_dates:
67+
if invoice.product_id.id not in invoice_dates:
68+
invoice_dates[invoice.product_id.product_tmpl_id.id] = (
69+
invoice.invoice_date
70+
)
71+
for template in self:
72+
template.last_invoice_date = invoice_dates.get(template.id, False)
73+
74+
@api.model
75+
def name_search(self, name="", args=None, operator="ilike", limit=100):
76+
customer_id = self.env.context.get("customer")
77+
if not customer_id:
78+
return super().name_search(name, args, operator, limit)
79+
res = super().name_search(name, args, operator, limit=100)
80+
ids = [r[0] for r in res]
81+
records = self.browse(ids)
82+
records.mapped("last_invoice_date")
83+
sorted_records = records.sorted(
84+
key=(lambda r: r.last_invoice_date or datetime.date.min), reverse=True
85+
)
86+
return [(r.id, r.display_name) for r in sorted_records][:limit]
87+
88+
def compute_agotime(self, datetime_field):
89+
now = fields.Datetime.now()
90+
rd = relativedelta(now, datetime_field)
91+
if rd.years:
92+
ago = f"{rd.years}y"
93+
elif rd.months:
94+
ago = f"{rd.months}mo"
95+
elif rd.days:
96+
ago = f"{rd.days}d"
97+
elif rd.hours:
98+
ago = f"{rd.hours}h"
99+
elif rd.minutes:
100+
ago = f"{rd.minutes}m"
101+
elif rd.seconds:
102+
ago = f"{rd.seconds}s"
103+
else:
104+
ago = ""
105+
106+
return ago
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?xml version="1.0" encoding="UTF-8" ?>
2+
<templates xml:space="preserve">
3+
<t t-inherit="product.ProductCatalogOrderLine" t-inherit-mode="extension">
4+
<xpath expr="//span[@t-out=&quot;price&quot;]" position="attributes">
5+
<attribute name="class">o_product_catalog_price fw-bold</attribute>
6+
</xpath>
7+
<xpath expr="//span[@t-out=&quot;price&quot;]" position="after">
8+
<t t-if="this.env.displayUoM">
9+
/ <span class="text-muted text-truncate w-50 me-4" t-out="props.uomDisplayName"/>
10+
</t>
11+
<t t-else="">
12+
<span class="me-4"/>
13+
</t>
14+
</xpath>
15+
</t>
16+
</templates>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<odoo>
3+
<record id="view_move_form_custom_display" model="ir.ui.view">
4+
<field name="name">account.move.form.custom.display</field>
5+
<field name="model">account.move</field>
6+
<field name="inherit_id" ref="account.view_move_form"/>
7+
<field name="arch" type="xml">
8+
<xpath expr="//field[@name='invoice_line_ids']/list/field[@name='product_id']" position="attributes">
9+
<attribute name="context">{'customer': parent.partner_id}</attribute>
10+
</xpath>
11+
<xpath expr="//button[@name='action_add_from_catalog']" position="attributes">
12+
<attribute name="context">{'order_id': parent.id, 'customer': parent.partner_id}</attribute>
13+
</xpath>
14+
</field>
15+
</record>
16+
</odoo>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<odoo>
3+
<record id="product_view_kanban_catalog_inherit" model="ir.ui.view">
4+
<field name="name">product.view.kanban.catalog.inherit</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="//field[@name='id']" position="after">
9+
<field name="virtual_available" invisible="1"/>
10+
<field name="last_invoice_time" invisible="1"/>
11+
</xpath>
12+
<xpath expr="//t[@name='qty_available']" position="inside">
13+
<t t-set="final_qty" t-value="record.virtual_available.raw_value - record.qty_available.raw_value"/>
14+
<t t-if="final_qty"> (
15+
<span t-if="final_qty >= 0" class="fw-bold text-success">
16+
+<t t-out="final_qty"/>
17+
</span>
18+
<span t-else="" class="fw-bold text-danger">
19+
<t t-out="final_qty"/>
20+
</span>
21+
)
22+
</t>
23+
</xpath>
24+
<xpath expr="//div[@name='o_kanban_qty_available_and_on_hand']" position="after">
25+
<t t-if="record.last_invoice_time.raw_value" name="last_invoice_time">
26+
<span class="text-muted">
27+
<t t-out="record.last_invoice_time.raw_value"/>
28+
<t t-if="record.last_invoice_time.raw_value != 'Today'"> ago</t>
29+
</span>
30+
</t>
31+
</xpath>
32+
</field>
33+
</record>
34+
</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_purchase_order_form_custom_display" model="ir.ui.view">
4+
<field name="name">purchase.order.form.custom.display</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">{'vendor': parent.partner_id}</attribute>
10+
</xpath>
11+
</field>
12+
</record>
13+
</odoo>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<odoo>
3+
<record id="view_order_form_custom_display" model="ir.ui.view">
4+
<field name="name">sale.order.form.custom.display</field>
5+
<field name="model">sale.order</field>
6+
<field name="inherit_id" ref="sale.view_order_form"/>
7+
<field name="arch" type="xml">
8+
<xpath expr="//field[@name='order_line']/list/field[@name='product_template_id']" position="attributes">
9+
<attribute name="context">{'customer': parent.partner_id}</attribute>
10+
</xpath>
11+
<xpath expr="//button[@name='action_add_from_catalog']" position="attributes">
12+
<attribute name="context">{'order_id': parent.id,'customer': parent.partner_id}</attribute>
13+
</xpath>
14+
</field>
15+
</record>
16+
</odoo>

0 commit comments

Comments
 (0)