Skip to content

[IMP] sale: enhance product listing and display on sales/purchase forms #905

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: 18.0
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions custom_sale_purchase_display/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
14 changes: 14 additions & 0 deletions custom_sale_purchase_display/__manifest__.py
Original file line number Diff line number Diff line change
@@ -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'
}
3 changes: 3 additions & 0 deletions custom_sale_purchase_display/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import sale_order
from . import product_product
from . import product_template
170 changes: 170 additions & 0 deletions custom_sale_purchase_display/models/product_product.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
from odoo import models, fields, api


class ProductProduct(models.Model):
_inherit = 'product.product'

# Computed field showing the latest invoice date for this product (context-sensitive)
last_invoice_date = fields.Date(
string="Last Invoice Date",
compute="_compute_last_invoice_date",
store=False
)

# Computed field showing time difference from today to last invoice date
last_invoice_time_diff = fields.Char(
string="Last Invoice Time Diff",
compute="_compute_last_invoice_time_diff",
store=False
)

def _compute_last_invoice_date(self):
for product in self:
partner_id = self.env.context.get('sale_order_partner_id') or self.env.context.get('purchase_order_partner_id')
if partner_id:
domain = [
('partner_id', '=', partner_id),
('state', '=', 'posted'),
('invoice_date', '!=', False),
('line_ids.product_id', '=', product.id)
]
if self.env.context.get('sale_order_partner_id'):
domain += [('move_type', '=', 'out_invoice')]
else:
domain += [('move_type', '=', 'in_invoice')]
move = self.env['account.move'].search(domain, order='invoice_date desc', limit=1)
product.last_invoice_date = move.invoice_date if move else False
else:
# Fallback: search globally for latest sales invoice
domain = [
('move_type', '=', 'out_invoice'),
('state', '=', 'posted'),
('invoice_date', '!=', False),
('line_ids.product_id', '=', product.id)
]
move = self.env['account.move'].search(domain, order='invoice_date desc', limit=1)
product.last_invoice_date = move.invoice_date if move else False

def _compute_last_invoice_time_diff(self):
for product in self:
if product.last_invoice_date:
diff = fields.Date.today() - product.last_invoice_date
days = diff.days
if days > 365:
product.last_invoice_time_diff = f"{days // 365}y ago"
elif days > 30:
product.last_invoice_time_diff = f"{days // 30}mo ago"
elif days > 7:
product.last_invoice_time_diff = f"{days // 7}w ago"
elif days > 0:
product.last_invoice_time_diff = f"{days}d ago"
else:
product.last_invoice_time_diff = "today"
else:
product.last_invoice_time_diff = False

@api.model
def _get_recent_sales(self, partner_id):
if not partner_id:
return []
domain = [
('partner_id', '=', partner_id),
('move_type', '=', 'out_invoice'),
('state', '=', 'posted'),
('invoice_date', '!=', False)
]
moves = self.env['account.move'].search(domain)
recent_sales = []
seen_products = set()
for move in moves.sorted(key=lambda m: m.invoice_date, reverse=True):
for line in move.line_ids:
pid = line.product_id.id
if pid and pid not in seen_products:
recent_sales.append({'pid': pid, 'date': move.invoice_date})
seen_products.add(pid)
return recent_sales

@api.model
def _get_recent_purchases(self, partner_id):
if not partner_id:
return []
domain = [
('partner_id', '=', partner_id),
('move_type', '=', 'in_invoice'),
('state', '=', 'posted'),
('invoice_date', '!=', False)
]
moves = self.env['account.move'].search(domain)
recent_purchases = []
seen_products = set()
for move in moves.sorted(key=lambda m: m.invoice_date, reverse=True):
for line in move.line_ids:
pid = line.product_id.id
if pid and pid not in seen_products:
recent_purchases.append({'pid': pid, 'date': move.invoice_date})
seen_products.add(pid)
return recent_purchases

@api.model
def _format_time_diff(self, date_input):
date = date_input.get('date') if isinstance(date_input, dict) else date_input
if not date:
return ""
diff = fields.Date.today() - date
days = diff.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"

@api.model
def name_search(self, name="", args=None, operator="ilike", limit=100):
args = args or []
sale_partner_id = self.env.context.get('sale_order_partner_id')
purchase_partner_id = self.env.context.get('purchase_order_partner_id')
partner_id = sale_partner_id or purchase_partner_id
if not partner_id:
return super().name_search(name, args, operator, limit)

# Get product history data
if sale_partner_id:
history_data = self._get_recent_sales(partner_id)
else:
history_data = self._get_recent_purchases(partner_id)

if not history_data:
return super().name_search(name, args, operator, limit)

# Sort product IDs based on most recent usage
sorted_history = sorted(history_data, key=lambda x: x['date'], reverse=True)
product_ids_ordered = [item['pid'] for item in sorted_history]
history_map = {item['pid']: item for item in sorted_history}

# Prioritize search based on historical relevance
domain = [('product_variant_ids.id', 'in', product_ids_ordered), ('name', operator, name)] + args
prioritized_products = self.search(domain).sorted(
key=lambda p: product_ids_ordered.index(p.product_variant_id.id if p.product_variant_id else p.id)
)

results = []
prioritized_ids = []
for product in prioritized_products:
item = history_map.get(product.id)
time_str = self._format_time_diff(item) if item else ""
display_name = f"{product.display_name} ⏱ {time_str}" if time_str else product.display_name
results.append((product.id, display_name))
prioritized_ids.append(product.id)

# Include additional matching products if under limit
other_limit = limit - len(results)
if other_limit > 0:
domain = [('id', 'not in', prioritized_ids), ('name', operator, name)] + args
other_products = self.search(domain, limit=other_limit)
results.extend([(p.id, p.display_name) for p in other_products])

return results
76 changes: 76 additions & 0 deletions custom_sale_purchase_display/models/product_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
from odoo import models, fields, api


class ProductTemplate(models.Model):
_inherit = 'product.template'

# Related fields to show recent invoice data for a product variant (read-only)
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
)

# Helper field to get the main variant of the template (used in searching/mapping)
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 []
# Get customer/vendor ID from context
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)

# Fetch recent product interaction history based on context
if self.env.context.get('sale_order_partner_id'):
history_data = self.env['product.product']._get_recent_sales(partner_id)
else:
history_data = self.env['product.product']._get_recent_purchases(partner_id)

if not history_data:
return super().name_search(name, args, operator, limit)

# Build a lookup of product ID to history entry
history_map = {item['pid']: item for item in history_data}
variant_ids = list(history_map.keys())
# Find related product templates from those variants
templates = self.env['product.product'].browse(variant_ids).mapped('product_tmpl_id')

# Prioritize templates based on history order
domain = [('id', 'in', templates.ids), ('name', operator, name)] + args
prioritized_templates = templates.search(domain).sorted(
key=lambda t: min(
[history_data.index(d) for d in history_data if d['pid'] in t.product_variant_ids.ids]
or [len(history_data)]
)
)

results = []
for tmpl in prioritized_templates:
variant_id = tmpl.product_variant_id.id if tmpl.product_variant_ids else False
item = history_map.get(variant_id)
# Add time difference if available to display name
time_str = self.env['product.product']._format_time_diff(item) if item else ""
display_name = f"{tmpl.display_name} ⏱ {time_str}" if time_str else tmpl.display_name
results.append((tmpl.id, display_name))

# If results are fewer than limit, append others that matched but not in history
other_limit = limit - len(results)
if other_limit > 0:
domain = [('id', 'not in', prioritized_templates.ids), ('name', operator, name)] + args
other_templates = self.search(domain, limit=other_limit)
results.extend([(t.id, t.display_name) for t in other_templates])

return results
Loading