From be9f1b8535c46b67497ee5dc6087e993af7818c2 Mon Sep 17 00:00:00 2001 From: ksoz Date: Thu, 31 Jul 2025 11:04:08 +0530 Subject: [PATCH] [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 (+). --- custom_sale_purchase_display/__init__.py | 1 + custom_sale_purchase_display/__manifest__.py | 14 ++++ .../models/__init__.py | 2 + .../models/product_product.py | 79 +++++++++++++++++++ .../models/product_template.py | 68 ++++++++++++++++ .../views/product_template_kanban_catalog.xml | 32 ++++++++ .../views/purchase_order_form.xml | 13 +++ .../views/sale_order_form_view.xml | 16 ++++ 8 files changed, 225 insertions(+) create mode 100644 custom_sale_purchase_display/__init__.py create mode 100644 custom_sale_purchase_display/__manifest__.py create mode 100644 custom_sale_purchase_display/models/__init__.py create mode 100644 custom_sale_purchase_display/models/product_product.py create mode 100644 custom_sale_purchase_display/models/product_template.py create mode 100644 custom_sale_purchase_display/views/product_template_kanban_catalog.xml create mode 100644 custom_sale_purchase_display/views/purchase_order_form.xml create mode 100644 custom_sale_purchase_display/views/sale_order_form_view.xml diff --git a/custom_sale_purchase_display/__init__.py b/custom_sale_purchase_display/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/custom_sale_purchase_display/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/custom_sale_purchase_display/__manifest__.py b/custom_sale_purchase_display/__manifest__.py new file mode 100644 index 00000000000..670fa6178c4 --- /dev/null +++ b/custom_sale_purchase_display/__manifest__.py @@ -0,0 +1,14 @@ +{ + 'name': 'Sale Order Product History', + 'version': '1.0', + 'category': 'Sales', + 'depends': ['sale_management', 'stock', 'purchase'], + 'data': [ + 'views/purchase_order_form.xml', + 'views/sale_order_form_view.xml', + 'views/product_template_kanban_catalog.xml', + ], + 'installable': True, + 'application': False, + 'license': 'LGPL-3' +} diff --git a/custom_sale_purchase_display/models/__init__.py b/custom_sale_purchase_display/models/__init__.py new file mode 100644 index 00000000000..18b37e85320 --- /dev/null +++ b/custom_sale_purchase_display/models/__init__.py @@ -0,0 +1,2 @@ +from . import product_product +from . import product_template diff --git a/custom_sale_purchase_display/models/product_product.py b/custom_sale_purchase_display/models/product_product.py new file mode 100644 index 00000000000..fbde660dcc3 --- /dev/null +++ b/custom_sale_purchase_display/models/product_product.py @@ -0,0 +1,79 @@ +from odoo import models, fields, api + + +class ProductProduct(models.Model): + _inherit = 'product.product' + + last_invoice_date = fields.Date( + string="Last Invoice Date", + compute="_compute_last_invoice_data", + store=False + ) + last_invoice_time_diff = fields.Char( + string="Last Invoice Time Diff", + compute="_compute_last_invoice_data", + store=False + ) + + def _compute_last_invoice_data(self): + for product in self: + partner_id = product.env.context.get('sale_order_partner_id') \ + or product.env.context.get('purchase_order_partner_id') + + is_sale = bool(product.env.context.get('sale_order_partner_id')) + move_type = 'out_invoice' if is_sale else 'in_invoice' + + domain = [ + ('state', '=', 'posted'), + ('invoice_date', '!=', False), + ('line_ids.product_id', '=', product.id), + ('move_type', '=', move_type), + ] + if partner_id: + domain.append(('partner_id', '=', partner_id)) + + move = product.env['account.move'].search( + domain, order='invoice_date desc', limit=1 + ) + + product.last_invoice_date = move.invoice_date if move else False + product.last_invoice_time_diff = ( + self._format_time_diff(move.invoice_date) if move else False + ) + + @api.model + def _get_recent_invoices(self, partner_id, is_sale=True): + if not partner_id: + return [] + + move_type = 'out_invoice' if is_sale else 'in_invoice' + moves = self.env['account.move'].search([ + ('partner_id', '=', partner_id), + ('move_type', '=', move_type), + ('state', '=', 'posted'), + ('invoice_date', '!=', False) + ], order='invoice_date desc') + + recent, seen = [], set() + for mv in moves: + for line in mv.line_ids.filtered('product_id'): + pid = line.product_id.id + if pid not in seen: + recent.append({'pid': pid, 'date': mv.invoice_date}) + seen.add(pid) + return recent + + @api.model + def _format_time_diff(self, invoice_date): + if not invoice_date: + return "" + days = (fields.Date.today() - invoice_date).days + if days > 365: + return f"{days // 365}y ago" + if days > 30: + return f"{days // 30}mo ago" + if days > 7: + return f"{days // 7}w ago" + if days > 0: + return f"{days}d ago" + return "today" diff --git a/custom_sale_purchase_display/models/product_template.py b/custom_sale_purchase_display/models/product_template.py new file mode 100644 index 00000000000..1e76394b4a1 --- /dev/null +++ b/custom_sale_purchase_display/models/product_template.py @@ -0,0 +1,68 @@ +from odoo import models, fields, api + + +class ProductTemplate(models.Model): + _inherit = 'product.template' + + last_invoice_date = fields.Date( + string="Last Invoice Date", + related='product_variant_id.last_invoice_date', + store=False + ) + last_invoice_time_diff = fields.Char( + string="Last Invoice Time Diff", + related='product_variant_id.last_invoice_time_diff', + store=False + ) + + product_variant_id = fields.Many2one( + 'product.product', + compute='_compute_product_variant_id', + store=True, + index=True + ) + + @api.model + def name_search(self, name="", args=None, operator="ilike", limit=100): + args = args or [] + + partner_id = self.env.context.get('sale_order_partner_id') \ + or self.env.context.get('purchase_order_partner_id') + if not partner_id: + return super().name_search(name, args, operator, limit) + + is_sale = bool(self.env.context.get('sale_order_partner_id')) + recent_lines = self.env['product.product']._get_recent_invoices( + partner_id=partner_id, + is_sale=is_sale + ) + + if not recent_lines: + return super().name_search(name, args, operator, limit) + + recent_template_ids = list(dict.fromkeys( + self.env['product.product'].browse(rl['pid']).product_tmpl_id.id + for rl in recent_lines + )) + + base_domain = [('name', operator, name)] + args + + recent_templates = self.search( + [('id', 'in', recent_template_ids)] + base_domain, + limit=limit + ) + other_templates = self.search( + [('id', 'not in', recent_template_ids)] + base_domain, + limit=max(0, limit - len(recent_templates)) + ) + + results = [] + for tmpl_id in recent_template_ids: + tmpl = recent_templates.filtered(lambda t: t.id == tmpl_id) + if tmpl: + td = tmpl.last_invoice_time_diff + label = f"{tmpl.display_name} ⏱ {td}" if td else tmpl.display_name + results.append((tmpl.id, label)) + + results.extend((t.id, t.display_name) for t in other_templates) + return results diff --git a/custom_sale_purchase_display/views/product_template_kanban_catalog.xml b/custom_sale_purchase_display/views/product_template_kanban_catalog.xml new file mode 100644 index 00000000000..7224063bbd6 --- /dev/null +++ b/custom_sale_purchase_display/views/product_template_kanban_catalog.xml @@ -0,0 +1,32 @@ + + + + product.product.kanban.inherit.catalog + product.product + + + + + + + + + +
(+ )
+
+ +
()
+
+ +
(0)
+
+
+ ⏱ +
+
+ 📅 +
+
+
+
+
diff --git a/custom_sale_purchase_display/views/purchase_order_form.xml b/custom_sale_purchase_display/views/purchase_order_form.xml new file mode 100644 index 00000000000..8ea02c56933 --- /dev/null +++ b/custom_sale_purchase_display/views/purchase_order_form.xml @@ -0,0 +1,13 @@ + + + + purchase.order.form.inherit.custom + purchase.order + + + + {'purchase_order_partner_id': parent.partner_id} + + + + diff --git a/custom_sale_purchase_display/views/sale_order_form_view.xml b/custom_sale_purchase_display/views/sale_order_form_view.xml new file mode 100644 index 00000000000..ca78d8637e4 --- /dev/null +++ b/custom_sale_purchase_display/views/sale_order_form_view.xml @@ -0,0 +1,16 @@ + + + + sale.order.form.inherit.custom + sale.order + + + + {'sale_order_partner_id': parent.partner_id} + + + {'sale_order_partner_id': parent.partner_id} + + + +