Skip to content

[IMP] custom_sale_purchase_display: enhance product listing #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
79 changes: 79 additions & 0 deletions custom_sale_purchase_display/models/product_product.py
Original file line number Diff line number Diff line change
@@ -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"
68 changes: 68 additions & 0 deletions custom_sale_purchase_display/models/product_template.py
Original file line number Diff line number Diff line change
@@ -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
173 changes: 173 additions & 0 deletions custom_sale_purchase_display/models/sale_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
from odoo import models, fields, api
from datetime import datetime


class SaleOrderLine(models.Model):
_inherit = 'sale.order.line'

# Computed stock-related fields to be shown per product line
forecasted_qty = fields.Float(string="Forecasted Qty", compute='_compute_forecasted_qty', store=False)
qty_on_hand = fields.Float(string="On Hand Qty", compute='_compute_qty_on_hand', store=False)
qty_difference = fields.Float(string="Qty Difference", compute='_compute_qty_difference', store=False)

@api.depends('product_id')
def _compute_forecasted_qty(self):
"""Fetches forecasted quantity (virtual stock) for selected product."""
for line in self:
if line.product_id:
line.forecasted_qty = line.product_id.virtual_available
else:
line.forecasted_qty = 0.0

@api.depends('product_id')
def _compute_qty_on_hand(self):
"""Fetches current on-hand quantity for selected product."""
for line in self:
if line.product_id:
line.qty_on_hand = line.product_id.qty_available
else:
line.qty_on_hand = 0.0

@api.depends('forecasted_qty', 'qty_on_hand')
def _compute_qty_difference(self):
"""Computes difference between forecasted and on-hand quantity."""
for line in self:
line.qty_difference = line.forecasted_qty - line.qty_on_hand


# Inherit SaleOrder to display last sold products for a customer
class SaleOrder(models.Model):
_inherit = 'sale.order'

# Computed list of recent products sold to the selected customer
last_sold_products = fields.Many2many(
'product.product',
compute='_compute_last_sold_products',
string="Last 5 Sold Products"
)

# Last invoice date for the selected customer
last_invoice_date = fields.Date(
compute='_compute_last_invoice_date',
string="Last Invoice Date"
)

# Textual info summary of last sold products and invoice timestamps
last_sold_products_info = fields.Text(
compute='_compute_last_sold_products_info',
string="Last Sold Products Info"
)

# Used to conditionally hide info block if there's no history
invisible_last_sold_info = fields.Boolean(
compute='_compute_invisible_last_sold_info',
string="Hide Last Sold Info"
)

@api.depends('partner_id')
def _compute_last_sold_products(self):
"""
Retrieves unique products from posted customer invoices,
ordered by invoice date for selected partner.
"""
for order in self:
if not order.partner_id:
order.last_sold_products = [(5, 0, 0)]
continue

# Search for invoice lines with products for the customer
lines = self.env['account.move.line'].search([
('move_id.partner_id', '=', order.partner_id.id),
('move_id.move_type', '=', 'out_invoice'),
('move_id.state', '=', 'posted'),
('display_type', '=', 'product'),
('product_id', '!=', False)
], order='date desc, id desc')

# Deduplicate products to keep the latest 5 sold ones
product_ids_ordered = []
seen_products = set()
for line in lines:
if line.product_id.id not in seen_products:
product_ids_ordered.append(line.product_id.id)
seen_products.add(line.product_id.id)

order.last_sold_products = [(6, 0, product_ids_ordered)]

@api.depends('partner_id')
def _compute_last_invoice_date(self):
"""
Finds the most recent invoice date for this customer.
"""
for order in self:
if not order.partner_id:
order.last_invoice_date = False
continue

last_invoice = self.env['account.move'].search([
('partner_id', '=', order.partner_id.id),
('move_type', '=', 'out_invoice'),
('state', '=', 'posted'),
('invoice_date', '!=', False)
], order='invoice_date desc', limit=1)

order.last_invoice_date = last_invoice.invoice_date if last_invoice else False

@api.depends('last_sold_products', 'partner_id')
def _compute_last_sold_products_info(self):
"""
Generates readable lines like:
• Product A (Invoiced 3 days ago on 2024-06-15)
"""
for order in self:
if not order.last_sold_products:
order.last_sold_products_info = "No recent products found for this customer."
continue

product_ids = order.last_sold_products.ids
last_invoice_dates = {}

# Find invoice dates per product
all_lines = self.env['account.move.line'].search([
('move_id.partner_id', '=', order.partner_id.id),
('product_id', 'in', product_ids),
('move_id.move_type', '=', 'out_invoice'),
('move_id.state', '=', 'posted'),
('move_id.invoice_date', '!=', False)
])

for line in all_lines:
product_id = line.product_id.id
invoice_date = line.move_id.invoice_date
if product_id not in last_invoice_dates or invoice_date > last_invoice_dates.get(product_id):
last_invoice_dates[product_id] = invoice_date

info_lines = []
current_dt = fields.Datetime.now()

for product in order.last_sold_products:
last_date = last_invoice_dates.get(product.id)
if last_date:
# Time difference from now to last invoice date
invoice_dt = datetime.combine(last_date, datetime.min.time())
time_diff = current_dt - invoice_dt
days = time_diff.days

if days > 1:
time_str = f"{days} days ago"
elif days == 1:
time_str = "1 day ago"
else:
time_str = f"{time_diff.seconds // 3600} hours ago"

info_lines.append(f"• {product.display_name} (Invoiced {time_str} on {last_date.strftime('%Y-%m-%d')})")
else:
info_lines.append(f"• {product.display_name} (No recent invoice found)")

order.last_sold_products_info = "\n".join(info_lines)

@api.depends('last_sold_products')
def _compute_invisible_last_sold_info(self):
"""Boolean toggle: hide info block if no products available."""
for order in self:
order.invisible_last_sold_info = not bool(order.last_sold_products)
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_product_template_kanban_inherit" model="ir.ui.view">
<field name="name">product.product.kanban.inherit.catalog</field>
<field name="model">product.product</field>
<field name="inherit_id" ref="product.product_view_kanban_catalog"/>
<field name="arch" type="xml">
<xpath expr="//div[contains(@name, 'o_kanban_qty_available')]" position="inside">
<field name="virtual_available" invisible="1"/>
<field name="qty_available" invisible="1"/> <!-- Ensure loaded -->
<field name="last_invoice_date" invisible="1"/> <!-- Ensure loaded -->
<field name="last_invoice_time_diff" invisible="1"/> <!-- Ensure loaded -->
<t t-set="diff" t-value="record.virtual_available.raw_value - record.qty_available.raw_value"/>
<t t-if="diff &gt; 0">
<div style="color: green;">(+ <t t-esc="diff"/>)</div>
</t>
<t t-elif="diff &lt; 0">
<div style="color: red;">(<t t-esc="diff"/>)</div>
</t>
<t t-else="">
<div>(0)</div>
</t>
<!-- Display last invoice info -->
<div t-if="record.last_invoice_time_diff.value" style="font-size: 80%; color: #888;">
⏱ <t t-esc="record.last_invoice_time_diff.value"/>
</div>
<div t-if="record.last_invoice_date.value" style="font-size: 80%; color: #aaa;">
📅 <t t-esc="record.last_invoice_date.value"/>
</div>
</xpath>
</field>
</record>
</odoo>
16 changes: 16 additions & 0 deletions custom_sale_purchase_display/views/purchase_order_form.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_order_form_inherit_custom" model="ir.ui.view">
<field name="name">purchase.order.form.inherit.custom</field>
<field name="model">purchase.order</field>
<field name="inherit_id" ref="purchase.purchase_order_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='order_line']/list/field[@name='product_id']" position="attributes">
<attribute name="context">{'purchase_order_partner_id': parent.partner_id}</attribute>
</xpath>
<!-- <xpath expr="//field[@name='order_line']/list/field[@name='product_template_id']" position="attributes">
<attribute name="context">{'purchase_order_partner_id': parent.partner_id}</attribute>
</xpath> -->
</field>
</record>
</odoo>
Loading