From db900fbd76ba011663e98682112a36388c38aa3d Mon Sep 17 00:00:00 2001 From: metu-odoo Date: Tue, 5 Aug 2025 16:20:41 +0530 Subject: [PATCH 1/2] [ADD] products_orderby_invoice: Imp dropdown in SO/RFQ Based on Invoice History Modified the product dropdown logic in Sales Order (SO) and Request for Quotation (RFQ) forms to prioritize products based on the customer's invoice history. - Dropdown now shows products ordered by the customer in previous invoices - Ordering is based on most recent to oldest invoices - Aims to make product selection faster and more relevant for recurring customers --- products_orderby_invoice/__init__.py | 3 + products_orderby_invoice/__manifest__.py | 11 ++++ products_orderby_invoice/models/__init__.py | 4 ++ .../models/product_product.py | 54 +++++++++++++++++ .../models/product_template.py | 58 +++++++++++++++++++ .../views/product_views.xml | 15 +++++ 6 files changed, 145 insertions(+) create mode 100644 products_orderby_invoice/__init__.py create mode 100644 products_orderby_invoice/__manifest__.py create mode 100644 products_orderby_invoice/models/__init__.py create mode 100644 products_orderby_invoice/models/product_product.py create mode 100644 products_orderby_invoice/models/product_template.py create mode 100644 products_orderby_invoice/views/product_views.xml diff --git a/products_orderby_invoice/__init__.py b/products_orderby_invoice/__init__.py new file mode 100644 index 00000000000..d6210b1285d --- /dev/null +++ b/products_orderby_invoice/__init__.py @@ -0,0 +1,3 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import models diff --git a/products_orderby_invoice/__manifest__.py b/products_orderby_invoice/__manifest__.py new file mode 100644 index 00000000000..c397ab494e0 --- /dev/null +++ b/products_orderby_invoice/__manifest__.py @@ -0,0 +1,11 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'Products Orderby Invoice', + 'version': '1.0', + 'depends': ['sale_management', 'account', 'stock', 'purchase'], + 'data': [ + 'views/product_views.xml', + ], + 'installable': True, +} diff --git a/products_orderby_invoice/models/__init__.py b/products_orderby_invoice/models/__init__.py new file mode 100644 index 00000000000..080a714ff17 --- /dev/null +++ b/products_orderby_invoice/models/__init__.py @@ -0,0 +1,4 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import product_template +from . import product_product diff --git a/products_orderby_invoice/models/product_product.py b/products_orderby_invoice/models/product_product.py new file mode 100644 index 00000000000..5a596bbf54c --- /dev/null +++ b/products_orderby_invoice/models/product_product.py @@ -0,0 +1,54 @@ +from odoo import api, fields, models + +class ProductProduct(models.Model): + _inherit = 'product.product' + + + surplus_qty = fields.Float(string='Surplus Quantity', compute='_compute_surplus_qty') + + @api.depends('virtual_available', 'qty_available') + def _compute_surplus_qty(self): + for rec in self: + rec.surplus_qty = rec.qty_available - rec.virtual_available + + @api.model + def name_search(self, name, args=None, operator='ilike', limit=100): + print("===============This is triggered====================") + args = list(args) if args else [] + partner_id = self.env.context.get('partner_id') + results = [] + matched_ids = set() + + if partner_id: + lines = self.env['account.move.line'].search([ + ('move_id.move_type', '=', 'out_invoice'), + ('move_id.partner_id', '=', partner_id), + ('move_id.state', '=', 'posted'), + ('product_id', '!=', False), + ]) + + lines = sorted(lines, key=lambda l: l.move_id.invoice_date or l.create_date, reverse=True) + + name_lower = name.lower() if name else '' + + for line in lines: + product = line.product_id + if product.id in matched_ids: + continue + if not name or (operator == 'ilike' and name_lower in product.name.lower()): + results.append((product.id, product.display_name)) + matched_ids.add(product.id) + if len(results) >= limit: + break + + remaining = limit - len(results) + if remaining > 0: + domain = args[:] + if name: + domain.append(('name', operator, name)) + if matched_ids: + domain.append(('id', 'not in', list(matched_ids))) # needs to be a list here + others = super().name_search(name, domain, operator=operator, limit=remaining) + results.extend(others) + + return results diff --git a/products_orderby_invoice/models/product_template.py b/products_orderby_invoice/models/product_template.py new file mode 100644 index 00000000000..a744e2cda83 --- /dev/null +++ b/products_orderby_invoice/models/product_template.py @@ -0,0 +1,58 @@ +from odoo import models, fields, api +from datetime import date + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + @api.model + def name_search(self, name, args=None, operator="ilike", limit=100): + args = list(args) if args else [] + partner_id = self.env.context.get("partner_id") + results = [] + matched_ids = [] + + lines = self.env["account.move.line"].search([ + ("move_id.move_type", "=", "out_invoice"), + ("move_id.partner_id", "=", partner_id), + ("move_id.state", "=", "posted"), + ("product_id.product_tmpl_id", "!=", False), + ]) + + lines = sorted( + lines, key=lambda l: l.move_id.invoice_date or fields.Date.today(), + reverse=True) + + tmpl_map = {} + for line in lines: + tmpl = line.product_id.product_tmpl_id + if tmpl.id not in tmpl_map: + tmpl_map[tmpl.id] = {"tmpl": tmpl, "date": line.move_id.invoice_date} + if len(tmpl_map) >= limit: + break + + name_lower = name.lower() if name else "" + today = date.today() + + for info in tmpl_map.values(): + tmpl = info["tmpl"] + invoice_date = info["date"] + if not name or (operator == "ilike" and name_lower in tmpl.name.lower()): + days = (today - invoice_date).days if invoice_date else "?" + display = f"{tmpl.display_name} (Last ordered {days} days ago)" + results.append((tmpl.id, display)) + matched_ids.append(tmpl.id) + if len(results) >= limit: + break + + remaining = limit - len(results) + if remaining > 0: + domain = args[:] + if name: + domain.append(("name", operator, name)) + if matched_ids: + domain.append(("id", "not in", matched_ids)) + others = super().name_search(name, args=domain, operator=operator, limit=remaining) + results.extend(others) + + return results diff --git a/products_orderby_invoice/views/product_views.xml b/products_orderby_invoice/views/product_views.xml new file mode 100644 index 00000000000..5475b5747f9 --- /dev/null +++ b/products_orderby_invoice/views/product_views.xml @@ -0,0 +1,15 @@ + + + + product.view.kanban.catalog.inherit.surplus.qty + product.product + + + + ( + + ) + + + + From d7feb269328fd6e117c92b02a37829200a52c23c Mon Sep 17 00:00:00 2001 From: metu-odoo Date: Thu, 7 Aug 2025 19:40:23 +0530 Subject: [PATCH 2/2] [IMP] products_orderby_invoice: Imp search and catalog based on invoice history Purpose: Make it easier to find and show products based on past invoices or bills. Approach: Changed name_search to list first the products already invoiced to the customer, sorted by latest date. Updated product catalog view to show last invoice date, moved info around, and added forecasted qty with color. Impact: Improves product search and helps users quickly spot stock levels and recent purchases. --- products_orderby_invoice/__manifest__.py | 1 + .../models/product_product.py | 25 ++++++---- .../models/product_template.py | 48 +++++++------------ .../views/product_views.xml | 36 +++++++++----- 4 files changed, 61 insertions(+), 49 deletions(-) diff --git a/products_orderby_invoice/__manifest__.py b/products_orderby_invoice/__manifest__.py index c397ab494e0..91ab0968878 100644 --- a/products_orderby_invoice/__manifest__.py +++ b/products_orderby_invoice/__manifest__.py @@ -8,4 +8,5 @@ 'views/product_views.xml', ], 'installable': True, + 'license': 'LGPL-3', } diff --git a/products_orderby_invoice/models/product_product.py b/products_orderby_invoice/models/product_product.py index 5a596bbf54c..6cf77900596 100644 --- a/products_orderby_invoice/models/product_product.py +++ b/products_orderby_invoice/models/product_product.py @@ -1,10 +1,23 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + from odoo import api, fields, models + class ProductProduct(models.Model): _inherit = 'product.product' - surplus_qty = fields.Float(string='Surplus Quantity', compute='_compute_surplus_qty') + invoice_date = fields.Datetime(string='Last Invoice Date', compute='_compute_invoice_date') + + @api.depends('invoice_date') + def _compute_invoice_date(self): + for rec in self: + lines = self.env['account.move.line'].search([ + ('product_id', '=', rec.id), + ('move_id.move_type', '=', 'out_invoice'), + ('move_id.state', '=', 'posted'), + ], limit=1, order='date desc') + rec.invoice_date = lines.move_id.date if lines else False @api.depends('virtual_available', 'qty_available') def _compute_surplus_qty(self): @@ -13,7 +26,6 @@ def _compute_surplus_qty(self): @api.model def name_search(self, name, args=None, operator='ilike', limit=100): - print("===============This is triggered====================") args = list(args) if args else [] partner_id = self.env.context.get('partner_id') results = [] @@ -29,15 +41,12 @@ def name_search(self, name, args=None, operator='ilike', limit=100): lines = sorted(lines, key=lambda l: l.move_id.invoice_date or l.create_date, reverse=True) - name_lower = name.lower() if name else '' - for line in lines: product = line.product_id if product.id in matched_ids: continue - if not name or (operator == 'ilike' and name_lower in product.name.lower()): - results.append((product.id, product.display_name)) - matched_ids.add(product.id) + results.append((product.id, product.display_name)) + matched_ids.add(product.id) if len(results) >= limit: break @@ -47,7 +56,7 @@ def name_search(self, name, args=None, operator='ilike', limit=100): if name: domain.append(('name', operator, name)) if matched_ids: - domain.append(('id', 'not in', list(matched_ids))) # needs to be a list here + domain.append(('id', 'not in', list(matched_ids))) others = super().name_search(name, domain, operator=operator, limit=remaining) results.extend(others) diff --git a/products_orderby_invoice/models/product_template.py b/products_orderby_invoice/models/product_template.py index a744e2cda83..9edb9ec3c75 100644 --- a/products_orderby_invoice/models/product_template.py +++ b/products_orderby_invoice/models/product_template.py @@ -1,5 +1,6 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + from odoo import models, fields, api -from datetime import date class ProductTemplate(models.Model): @@ -10,48 +11,35 @@ def name_search(self, name, args=None, operator="ilike", limit=100): args = list(args) if args else [] partner_id = self.env.context.get("partner_id") results = [] - matched_ids = [] + seen_ids = set() - lines = self.env["account.move.line"].search([ + if partner_id: + lines = self.env["account.move.line"].search([ ("move_id.move_type", "=", "out_invoice"), ("move_id.partner_id", "=", partner_id), ("move_id.state", "=", "posted"), ("product_id.product_tmpl_id", "!=", False), ]) - - lines = sorted( - lines, key=lambda l: l.move_id.invoice_date or fields.Date.today(), - reverse=True) - - tmpl_map = {} - for line in lines: - tmpl = line.product_id.product_tmpl_id - if tmpl.id not in tmpl_map: - tmpl_map[tmpl.id] = {"tmpl": tmpl, "date": line.move_id.invoice_date} - if len(tmpl_map) >= limit: - break - - name_lower = name.lower() if name else "" - today = date.today() - - for info in tmpl_map.values(): - tmpl = info["tmpl"] - invoice_date = info["date"] - if not name or (operator == "ilike" and name_lower in tmpl.name.lower()): - days = (today - invoice_date).days if invoice_date else "?" - display = f"{tmpl.display_name} (Last ordered {days} days ago)" + lines = sorted(lines, key=lambda l: l.move_id.invoice_date or fields.Date.today(), reverse=True) + + for line in lines: + tmpl = line.product_id.product_tmpl_id + if tmpl.id in seen_ids: + continue + days = line.product_id.product_tmpl_id.sale_delay + display = f"{tmpl.display_name} (Order Lead time {days} days)" results.append((tmpl.id, display)) - matched_ids.append(tmpl.id) - if len(results) >= limit: - break + seen_ids.add(tmpl.id) + if len(results) >= limit: + break remaining = limit - len(results) if remaining > 0: domain = args[:] if name: domain.append(("name", operator, name)) - if matched_ids: - domain.append(("id", "not in", matched_ids)) + if seen_ids: + domain.append(("id", "not in", list(seen_ids))) others = super().name_search(name, args=domain, operator=operator, limit=remaining) results.extend(others) diff --git a/products_orderby_invoice/views/product_views.xml b/products_orderby_invoice/views/product_views.xml index 5475b5747f9..89d858b17da 100644 --- a/products_orderby_invoice/views/product_views.xml +++ b/products_orderby_invoice/views/product_views.xml @@ -1,15 +1,29 @@ - product.view.kanban.catalog.inherit.surplus.qty - product.product - - - - ( - - ) - - - + product.view.kanban.catalog.inherit.surplus.qty + product.product + + + + ( + + + + + + + + + + + 0.00 + + ) +
+ Last invoice date : +
+
+
+