From e2745d4ff87885743c1df9d2df72127d217fc88f Mon Sep 17 00:00:00 2001 From: danal-odoo Date: Thu, 18 Dec 2025 18:14:39 +0530 Subject: [PATCH 1/3] [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` Datetime Field, `last_invoice_time` field. - Model `product.template`: Added `last_order` Datetime field, `last_invoice_date` Datetime 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 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`: Updated to show `uomDisplayName` next to `price`. --- last_purchased_products/__init__.py | 1 + last_purchased_products/__manifest__.py | 21 +++ last_purchased_products/models/__init__.py | 4 + last_purchased_products/models/product.py | 123 ++++++++++++++++++ .../models/product_template.py | 104 +++++++++++++++ .../product_catalog/order_line/order_line.xml | 16 +++ .../views/account_move_views.xml | 17 +++ .../views/product_views.xml | 34 +++++ .../views/purchase_order_views.xml | 13 ++ .../views/sale_order_views.xml | 13 ++ 10 files changed, 346 insertions(+) create mode 100644 last_purchased_products/__init__.py create mode 100644 last_purchased_products/__manifest__.py create mode 100644 last_purchased_products/models/__init__.py create mode 100644 last_purchased_products/models/product.py create mode 100644 last_purchased_products/models/product_template.py create mode 100644 last_purchased_products/static/src/product_catalog/order_line/order_line.xml create mode 100644 last_purchased_products/views/account_move_views.xml create mode 100644 last_purchased_products/views/product_views.xml create mode 100644 last_purchased_products/views/purchase_order_views.xml create mode 100644 last_purchased_products/views/sale_order_views.xml diff --git a/last_purchased_products/__init__.py b/last_purchased_products/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/last_purchased_products/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/last_purchased_products/__manifest__.py b/last_purchased_products/__manifest__.py new file mode 100644 index 00000000000..b407ec20875 --- /dev/null +++ b/last_purchased_products/__manifest__.py @@ -0,0 +1,21 @@ +{ + "name": "Last Purchased Products", + "version": "1.0", + "depends": ["sale_management", "purchase", "stock"], + "author": "danal", + "category": "Category", + "license": "LGPL-3", + "data": [ + "views/sale_order_views.xml", + "views/account_move_views.xml", + "views/purchase_order_views.xml", + "views/product_views.xml" + ], + "assets": { + "web.assets_backend": [ + "last_purchased_products/static/src/product_catalog/**/*.xml", + ] + }, + "installable": True, + "application": False, +} diff --git a/last_purchased_products/models/__init__.py b/last_purchased_products/models/__init__.py new file mode 100644 index 00000000000..28a989df5c8 --- /dev/null +++ b/last_purchased_products/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 diff --git a/last_purchased_products/models/product.py b/last_purchased_products/models/product.py new file mode 100644 index 00000000000..d73cd39c30d --- /dev/null +++ b/last_purchased_products/models/product.py @@ -0,0 +1,123 @@ +from dateutil.relativedelta import relativedelta +import datetime + +from odoo import models, api, fields + + +class ProductProduct(models.Model): + _inherit = "product.product" + + last_order = fields.Datetime(compute="_compute_last_order") + last_invoice_date = fields.Datetime(compute="_compute_last_invoice_date") + last_invoice_time = fields.Char(compute="_compute_invoice_time") + + @api.depends_context("customer", "formatted_display_name") + def _compute_display_name(self): + res = super()._compute_display_name() + if not self.env.context.get("customer") or not self.env.context.get( + "formatted_display_name" + ): + return res + compute_agotime_ref = self.compute_agotime + for product in self: + if not product.last_order: + continue + ago = compute_agotime_ref(product.last_order) + current_product_name = product.display_name or "" + if self.env.context.get("formatted_display_name"): + if ago: + time_postfix = f"\t--{ago}--" + else: + time_postfix = "" + product.display_name = f"{current_product_name}{time_postfix}" + else: + product.display_name = f"{current_product_name}" + + @api.depends_context("customer", "vendor") + def _compute_last_invoice_date(self): + customer_id = self.env.context.get("customer") + vendor_id = self.env.context.get("vendor") + domain = [ + ("product_id", "in", self.ids), + ("parent_state", "=", "posted"), + ] + if customer_id: + domain.append(("move_id.move_type", "=", "out_invoice")) + domain.append(("partner_id", "=", customer_id)) + elif vendor_id: + domain.append(("move_id.move_type", "=", "in_invoice")) + domain.append(("partner_id", "=", vendor_id)) + else: + pass + + last_invoice_dates = self.env["account.move.line"].search( + domain, order="create_date desc" + ) + invoice_dates = {} + for invoice in last_invoice_dates: + if invoice.product_id.id not in invoice_dates: + invoice_dates[invoice.product_id.id] = invoice.create_date + for product in self: + product.last_invoice_date = invoice_dates.get(product.id, False) + + @api.depends_context("customer") + def _compute_last_order(self): + last_orders = self.env["sale.order.line"].search( + [ + ("order_id.partner_id", "=", self.env.context.get("customer")), + ("product_id", "in", self.ids), + ("state", "=", "sale"), + ], + order="order_id desc", + ) + order_dates = {} + for order in last_orders: + if order.product_id.id not in order_dates: + order_dates[order.product_id.id] = order.order_id.date_order + for product in self: + product.last_order = order_dates.get(product.id, False) + + @api.depends('last_invoice_date') + def _compute_invoice_time(self): + compute_agotime_ref = self.compute_agotime + for product in self: + if product.last_invoice_date: + ago = compute_agotime_ref(product.last_invoice_date) + if not ago or ("s" in ago): + ago = "Just Now" + product.last_invoice_time = ago + else: + product.last_invoice_time = False + + @api.model + def name_search(self, name="", args=None, operator="ilike", limit=100): + if not self.env.context.get("customer") and not self.env.context.get("vendor"): + return super().name_search(name, args, operator, limit) + res = super().name_search(name, args, operator, limit=100) + ids = [r[0] for r in res] + records = self.browse(ids) + records.mapped("last_invoice_date") + sorted_records = records.sorted( + key=(lambda r: r.last_invoice_date or datetime.datetime.min), reverse=True + ) + return [(r.id, r.display_name) for r in sorted_records][:limit] + + def compute_agotime(self, datetime_field): + now = fields.Datetime.now() + rd = relativedelta(now, datetime_field) + if rd.years: + ago = f"{rd.years}y" + elif rd.months: + ago = f"{rd.months}mo" + elif rd.days: + ago = f"{rd.days}d" + elif rd.hours: + ago = f"{rd.hours}h" + elif rd.minutes: + ago = f"{rd.minutes}m" + elif rd.seconds: + ago = f"{rd.seconds}s" + else: + ago = "" + + return ago diff --git a/last_purchased_products/models/product_template.py b/last_purchased_products/models/product_template.py new file mode 100644 index 00000000000..ba921b510ae --- /dev/null +++ b/last_purchased_products/models/product_template.py @@ -0,0 +1,104 @@ +from dateutil.relativedelta import relativedelta +import datetime + +from odoo import models, api, fields + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + last_order = fields.Datetime(compute="_compute_last_order") + last_invoice_date = fields.Datetime(compute="_compute_last_invoice_date") + + @api.depends_context("customer", "formatted_display_name") + def _compute_display_name(self): + res = super()._compute_display_name() + if not self.env.context.get("customer") or not self.env.context.get( + "formatted_display_name" + ): + return res + + compute_agotime_ref = self.compute_agotime + for template in self: + if not template.last_order: + continue + ago = compute_agotime_ref(template.last_order) + current_product_template_name = template.display_name or "" + if self.env.context.get("formatted_display_name"): + if ago: + time_postfix = f"\t--{ago}--" + else: + time_postfix = "" + template.display_name = f"{current_product_template_name}{time_postfix}" + else: + template.display_name = f"{current_product_template_name}" + + @api.depends_context("customer") + def _compute_last_order(self): + last_orders = self.env["sale.order.line"].search( + [ + ("order_id.partner_id", "=", self.env.context.get("customer")), + ("product_id.product_tmpl_id", "in", self.ids), + ("state", "=", "sale"), + ], + order="order_id desc", + ) + order_dates = {} + for order in last_orders: + if order.product_id.id not in order_dates: + order_dates[order.product_id.product_tmpl_id.id] = ( + order.order_id.date_order + ) + for template in self: + template.last_order = order_dates.get(template.id, False) + + @api.depends_context("customer") + def _compute_last_invoice_date(self): + last_invoice_dates = self.env["account.move.line"].search( + [ + ("partner_id", "=", self.env.context.get("customer")), + ("product_id.product_tmpl_id", "in", self.ids), + ("parent_state", "=", "posted"), + ], + order="create_date desc", + ) + invoice_dates = {} + for invoice in last_invoice_dates: + if invoice.product_id.id not in invoice_dates: + invoice_dates[invoice.product_id.product_tmpl_id.id] = invoice.create_date + for template in self: + template.last_invoice_date = invoice_dates.get(template.id, False) + + @api.model + def name_search(self, name="", args=None, operator="ilike", limit=100): + customer_id = self.env.context.get("customer") + if not customer_id: + return super().name_search(name, args, operator, limit) + res = super().name_search(name, args, operator, limit=100) + ids = [r[0] for r in res] + records = self.browse(ids) + records.mapped("last_invoice_date") + sorted_records = records.sorted( + key=(lambda r: r.last_invoice_date or datetime.datetime.min), reverse=True + ) + return [(r.id, r.display_name) for r in sorted_records][:limit] + + def compute_agotime(self, datetime_field): + now = fields.Datetime.now() + rd = relativedelta(now, datetime_field) + if rd.years: + ago = f"{rd.years}y" + elif rd.months: + ago = f"{rd.months}mo" + elif rd.days: + ago = f"{rd.days}d" + elif rd.hours: + ago = f"{rd.hours}h" + elif rd.minutes: + ago = f"{rd.minutes}m" + elif rd.seconds: + ago = f"{rd.seconds}s" + else: + ago = "" + + return ago diff --git a/last_purchased_products/static/src/product_catalog/order_line/order_line.xml b/last_purchased_products/static/src/product_catalog/order_line/order_line.xml new file mode 100644 index 00000000000..4acb02f648a --- /dev/null +++ b/last_purchased_products/static/src/product_catalog/order_line/order_line.xml @@ -0,0 +1,16 @@ + + + + + o_product_catalog_price fw-bold + + + + / + + + + + + + diff --git a/last_purchased_products/views/account_move_views.xml b/last_purchased_products/views/account_move_views.xml new file mode 100644 index 00000000000..4a2949f9229 --- /dev/null +++ b/last_purchased_products/views/account_move_views.xml @@ -0,0 +1,17 @@ + + + + account.move.form.custom.display + account.move + + + + { + 'vendor': parent.partner_id if parent.move_type in ('in_invoice', 'in_refund') else False, + 'customer': parent.partner_id if parent.move_type not in ('in_invoice','in_refund') else False + } + + + + diff --git a/last_purchased_products/views/product_views.xml b/last_purchased_products/views/product_views.xml new file mode 100644 index 00000000000..203d316b46b --- /dev/null +++ b/last_purchased_products/views/product_views.xml @@ -0,0 +1,34 @@ + + + + product.view.kanban.catalog.inherit + product.product + + + + + + + + + ( + + + + + + + + ) + + + + + + + ago + + + + + + diff --git a/last_purchased_products/views/purchase_order_views.xml b/last_purchased_products/views/purchase_order_views.xml new file mode 100644 index 00000000000..ab013b2c323 --- /dev/null +++ b/last_purchased_products/views/purchase_order_views.xml @@ -0,0 +1,13 @@ + + + + purchase.order.form.custom.display + purchase.order + + + + {'vendor': parent.partner_id} + + + + diff --git a/last_purchased_products/views/sale_order_views.xml b/last_purchased_products/views/sale_order_views.xml new file mode 100644 index 00000000000..131f5537b93 --- /dev/null +++ b/last_purchased_products/views/sale_order_views.xml @@ -0,0 +1,13 @@ + + + + sale.order.form.custom.display + sale.order + + + + {'customer': parent.partner_id} + + + + From e160ea393379571c6c459368585bad30566a0def Mon Sep 17 00:00:00 2001 From: danal-odoo Date: Tue, 23 Dec 2025 18:53:34 +0530 Subject: [PATCH 2/3] [IMP] last_purchased_products: add test suite for computation and sorting logic This commit introduces the `TestLastOrderProduct` test suite to ensure the stability of product sorting and time computation based on sales and purchase history. Key scenarios covered: Last Order Computation: - Verifies that confirming a Sale Order updates the `last_order` field. - Checks that the 'Ago' time suffix (e.g., '--1h--') is correctly appended to `display_name` when the context is active. Last Invoice Date: - Verifies that posting a Customer Invoice updates `last_invoice_date`. - Ensures valid date comparisons between fields. Vendor Bill Sorting: - Verifies that posting a Vendor Bill updates `last_invoice_date` when in a vendor context. - Ensures products from recent bills appear at the top of search results. Name Search Sorting: - Verifies that recently invoiced products appear at the top of the list in the `name_search` results. - Confirms that sorting falls back to the default (alphabetical) when no customer/vendor context is provided. Time Display Logic: - Validates `last_invoice_time` computation. - Specifically verifies that invoices created less than a minute ago return 'Just Now'. Context Handling: - Ensures logic only triggers when `customer`, `vendor`, or `formatted_display_name` keys are present in the context, preventing unwanted side effects in standard views. --- last_purchased_products/tests/__init__.py | 1 + .../tests/test_last_order_product.py | 184 ++++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 last_purchased_products/tests/__init__.py create mode 100644 last_purchased_products/tests/test_last_order_product.py diff --git a/last_purchased_products/tests/__init__.py b/last_purchased_products/tests/__init__.py new file mode 100644 index 00000000000..738ed1dfce0 --- /dev/null +++ b/last_purchased_products/tests/__init__.py @@ -0,0 +1 @@ +from . import test_last_order_product diff --git a/last_purchased_products/tests/test_last_order_product.py b/last_purchased_products/tests/test_last_order_product.py new file mode 100644 index 00000000000..fba4ebcc0ae --- /dev/null +++ b/last_purchased_products/tests/test_last_order_product.py @@ -0,0 +1,184 @@ +from odoo.tests.common import TransactionCase +from odoo import fields, Command +from datetime import timedelta + + +class TestLastOrderProduct(TransactionCase): + + @classmethod + def setUpClass(self): + super().setUpClass() + + self.partner = self.env["res.users"].create( + { + "name": "Salesman Test User", + "login": "sales_test_user", + "email": "sales_test@example.com", + } + ) + + # Product 1: Will have an Order + self.product = self.env["product.product"].create( + { + "name": "A Ordered Product", + "type": "consu", + "list_price": 100.0, + } + ) + + # Product 2: Will have an Invoice + self.product_second = self.env["product.product"].create( + { + "name": "B Invoiced Product", + "type": "consu", + "list_price": 200.0, + } + ) + + # Create Order for Product 1 + self.order = self.env["sale.order"].create( + { + "partner_id": self.partner.partner_id.id, + "order_line": [ + Command.create( + { + "product_id": self.product.id, + "product_uom_qty": 1, + "price_unit": 100.0, + } + ) + ], + } + ) + + # Create Invoice for Product 2 + self.invoice = self.env["account.move"].create( + { + "move_type": "out_invoice", + "partner_id": self.partner.partner_id.id, + "invoice_date": fields.Date.today(), + "invoice_line_ids": [ + Command.create( + { + "product_id": self.product_second.id, + "quantity": 1, + "price_unit": 200.0, + } + ) + ], + } + ) + + # Create Bill for Product + self.bill = self.env["account.move"].create( + { + "move_type": "in_invoice", + "partner_id": self.partner.partner_id.id, + "invoice_date": fields.Date.today(), + "invoice_line_ids": [ + Command.create( + { + "product_id": self.product.id, + "quantity": 1, + "price_unit": 200.0, + } + ) + ], + } + ) + + self.past_time = fields.Datetime.now() - timedelta(hours=1) + + def test_last_order_computation_name_search(self): + """Test that confirming a sale order updates the last_order and display_name has order time""" + self.order.action_confirm() + self.assertEqual(self.order.state, "sale") + self.order.date_order = self.past_time + ctx = {"customer": self.partner.partner_id.id, "formatted_display_name": True} + product_with_ctx = self.product.with_context(ctx) + product_with_ctx._compute_last_order() + self.assertEqual( + product_with_ctx.last_order, + self.order.date_order, + "Last order date should match the sale order date", + ) + results = ( + self.env["product.product"] + .with_context(ctx) + .name_search(name=self.product.name) + ) + ago = self.product.with_context(ctx).compute_agotime(self.order.date_order) + self.assertEqual(ago, "1h", "Time computed should be 1h") + result_name = results[0][1] + self.assertIn("--1h--", result_name, "Name should contain the time suffix") + + def test_last_invoice_date_computation_name_search(self): + """Test that confirming a create date updates the last_invoice_date and product appear on top""" + self.invoice.action_post() + self.assertEqual(self.invoice.state, "posted") + ctx = {"customer": self.partner.partner_id.id, "formatted_display_name": True} + product_with_ctx = self.product_second.with_context(ctx) + product_with_ctx._compute_last_invoice_date() + self.assertEqual( + product_with_ctx.last_invoice_date, + self.invoice.create_date, + "Last invoice date should match the invoice creation date", + ) + results = self.env["product.product"].with_context(ctx).name_search(name="") + self.assertEqual( + results[0][1], + self.product_second.name, + "Recently invoiced product should be on top.", + ) + + def test_no_customer_name_search(self): + """Test that if no customer is selected then should ive default display_name and sorting""" + self.invoice.action_post() + self.order.action_confirm() + ctx = {"customer": None, "formatted_display_name": True} + results = self.env["product.product"].with_context(ctx).name_search(name="") + result_ids = [r[0] for r in results] + index_product_a = result_ids.index(self.product.id) + index_product_b = result_ids.index(self.product_second.id) + self.assertLess( + index_product_a, + index_product_b, + "Without customer context, sorting should default.", + ) + product_a_result_name = next(r[1] for r in results if r[0] == self.product.id) + self.assertNotIn( + "--", + product_a_result_name, + "Suffix should not be present without customer context", + ) + + def test_last_invoice_time_compute(self): + """Test to compute last_invoice_time which depends on last_invoice_date""" + self.invoice.action_post() + ctx = {"customer": self.partner.partner_id.id, "order_id": self.order.id} + product_with_ctx = self.product_second.with_context(ctx) + product_with_ctx.with_context(ctx)._compute_invoice_time() + self.assertEqual( + product_with_ctx.last_invoice_time, + "Just Now", + "Last invoice time should be 'Just Now' for less then 1 minute older invoices", + ) + + def test_purchase_order_product_sorting(self): + """Test to confirming recently purchsed product is on top""" + self.bill.action_post() + self.assertEqual(self.bill.state, "posted") + ctx = {"vendor": self.partner.partner_id.id, "formatted_display_name": True} + product_with_ctx = self.product.with_context(ctx) + product_with_ctx._compute_last_invoice_date() + self.assertEqual( + product_with_ctx.last_invoice_date, + self.bill.create_date, + "Last invoice date should match the bill creation date", + ) + results = self.env["product.product"].with_context(ctx).name_search(name="") + self.assertEqual( + results[0][1], + self.product.name, + "Billed product should be at the top for Vendor context", + ) From c91f7fba1279484ddc02bf9dd72081eeba99963e Mon Sep 17 00:00:00 2001 From: danal-odoo Date: Wed, 24 Dec 2025 11:54:38 +0530 Subject: [PATCH 3/3] [IMP] last_purchased_products: added customer name next to invoice time Before, there was no customer name next to last invoice time, it was hard to identify that to which customer this product was recently invoiced. this commit add that functionality which will add customer name to whom then recent invoice created and that name is clickable will take to that invoice form. Technical Details: - Model `product.product`: Created `invoice_partner_name` Char field to store customer name. - Model `product.product`: Created action `action_open_last_invoice` to open last invoice of product. - View `product_views`: Updated to show customer name next to `last_invoice_time`. --- last_purchased_products/__manifest__.py | 10 +- last_purchased_products/models/product.py | 43 +++++-- .../models/product_template.py | 111 +++--------------- .../tests/test_last_order_product.py | 6 +- .../views/account_move_views.xml | 12 +- .../views/product_views.xml | 8 +- .../views/purchase_order_views.xml | 3 + .../views/sale_order_views.xml | 24 ++-- 8 files changed, 96 insertions(+), 121 deletions(-) diff --git a/last_purchased_products/__manifest__.py b/last_purchased_products/__manifest__.py index b407ec20875..6a1fe379533 100644 --- a/last_purchased_products/__manifest__.py +++ b/last_purchased_products/__manifest__.py @@ -1,5 +1,13 @@ { "name": "Last Purchased Products", + "description": """ + add last order time next to product display name, + order product list as recently to old invoice creation time, + add same functionality to customer invoice, + add Unit of Measure next to price in catalog kanban view, + add recent invoice time after price in catalog kanban view, + add customer name next to invoice time. + """, "version": "1.0", "depends": ["sale_management", "purchase", "stock"], "author": "danal", @@ -9,7 +17,7 @@ "views/sale_order_views.xml", "views/account_move_views.xml", "views/purchase_order_views.xml", - "views/product_views.xml" + "views/product_views.xml", ], "assets": { "web.assets_backend": [ diff --git a/last_purchased_products/models/product.py b/last_purchased_products/models/product.py index d73cd39c30d..34e4cbf10ba 100644 --- a/last_purchased_products/models/product.py +++ b/last_purchased_products/models/product.py @@ -1,7 +1,7 @@ -from dateutil.relativedelta import relativedelta import datetime +from dateutil.relativedelta import relativedelta -from odoo import models, api, fields +from odoo import api, fields, models class ProductProduct(models.Model): @@ -10,6 +10,7 @@ class ProductProduct(models.Model): last_order = fields.Datetime(compute="_compute_last_order") last_invoice_date = fields.Datetime(compute="_compute_last_invoice_date") last_invoice_time = fields.Char(compute="_compute_invoice_time") + invoice_partner_name = fields.Char(compute="_compute_last_invoice_date") @api.depends_context("customer", "formatted_display_name") def _compute_display_name(self): @@ -48,37 +49,45 @@ def _compute_last_invoice_date(self): domain.append(("move_id.move_type", "=", "in_invoice")) domain.append(("partner_id", "=", vendor_id)) else: - pass + domain.append(("move_id.move_type", "=", "out_invoice")) last_invoice_dates = self.env["account.move.line"].search( domain, order="create_date desc" ) invoice_dates = {} + invoice_partners = {} for invoice in last_invoice_dates: if invoice.product_id.id not in invoice_dates: invoice_dates[invoice.product_id.id] = invoice.create_date + if invoice.product_id.id not in invoice_partners: + invoice_partners[invoice.product_id.id] = invoice.partner_id.name for product in self: product.last_invoice_date = invoice_dates.get(product.id, False) + product.invoice_partner_name = invoice_partners.get(product.id, False) @api.depends_context("customer") def _compute_last_order(self): last_orders = self.env["sale.order.line"].search( [ ("order_id.partner_id", "=", self.env.context.get("customer")), - ("product_id", "in", self.ids), ("state", "=", "sale"), ], - order="order_id desc", + order="order_id", ) order_dates = {} for order in last_orders: - if order.product_id.id not in order_dates: - order_dates[order.product_id.id] = order.order_id.date_order + p_id = order.product_id.id + if p_id not in order_dates: + order_dates[p_id] = order.order_id.date_order for product in self: product.last_order = order_dates.get(product.id, False) - @api.depends('last_invoice_date') + @api.depends("last_invoice_date") + @api.depends_context("isPurchase") def _compute_invoice_time(self): + if self.env.context.get("isPurchase"): + self.write({"last_invoice_time": False}) + return compute_agotime_ref = self.compute_agotime for product in self: if product.last_invoice_date: @@ -89,6 +98,24 @@ def _compute_invoice_time(self): else: product.last_invoice_time = False + def action_open_last_invoice(self): + self.ensure_one() + domain = [ + ("move_id.move_type", "=", "out_invoice"), + ("product_id", "=", self.id), + ("parent_state", "=", "posted"), + ] + last_line = self.env["account.move.line"].search( + domain, order="create_date desc", limit=1 + ) + return { + "type": "ir.actions.act_window", + "res_model": "account.move", + "res_id": last_line.move_id.id, + "view_mode": "form", + "target": "current", + } + @api.model def name_search(self, name="", args=None, operator="ilike", limit=100): if not self.env.context.get("customer") and not self.env.context.get("vendor"): diff --git a/last_purchased_products/models/product_template.py b/last_purchased_products/models/product_template.py index ba921b510ae..5417d5d2d3c 100644 --- a/last_purchased_products/models/product_template.py +++ b/last_purchased_products/models/product_template.py @@ -1,104 +1,27 @@ -from dateutil.relativedelta import relativedelta -import datetime - -from odoo import models, api, fields +from odoo import api, fields, models class ProductTemplate(models.Model): _inherit = "product.template" - last_order = fields.Datetime(compute="_compute_last_order") - last_invoice_date = fields.Datetime(compute="_compute_last_invoice_date") - - @api.depends_context("customer", "formatted_display_name") - def _compute_display_name(self): - res = super()._compute_display_name() - if not self.env.context.get("customer") or not self.env.context.get( - "formatted_display_name" - ): - return res - - compute_agotime_ref = self.compute_agotime - for template in self: - if not template.last_order: - continue - ago = compute_agotime_ref(template.last_order) - current_product_template_name = template.display_name or "" - if self.env.context.get("formatted_display_name"): - if ago: - time_postfix = f"\t--{ago}--" - else: - time_postfix = "" - template.display_name = f"{current_product_template_name}{time_postfix}" - else: - template.display_name = f"{current_product_template_name}" - - @api.depends_context("customer") - def _compute_last_order(self): - last_orders = self.env["sale.order.line"].search( - [ - ("order_id.partner_id", "=", self.env.context.get("customer")), - ("product_id.product_tmpl_id", "in", self.ids), - ("state", "=", "sale"), - ], - order="order_id desc", - ) - order_dates = {} - for order in last_orders: - if order.product_id.id not in order_dates: - order_dates[order.product_id.product_tmpl_id.id] = ( - order.order_id.date_order - ) - for template in self: - template.last_order = order_dates.get(template.id, False) + product_variant_id = fields.Many2one( + "product.product", "Product", compute="_compute_product_variant_id", store=True + ) + display_name = fields.Char(related="product_variant_id.display_name") - @api.depends_context("customer") - def _compute_last_invoice_date(self): - last_invoice_dates = self.env["account.move.line"].search( - [ - ("partner_id", "=", self.env.context.get("customer")), - ("product_id.product_tmpl_id", "in", self.ids), - ("parent_state", "=", "posted"), - ], - order="create_date desc", - ) - invoice_dates = {} - for invoice in last_invoice_dates: - if invoice.product_id.id not in invoice_dates: - invoice_dates[invoice.product_id.product_tmpl_id.id] = invoice.create_date - for template in self: - template.last_invoice_date = invoice_dates.get(template.id, False) + @api.depends("product_variant_ids") + def _compute_product_variant_id(self): + for p in self: + p.product_variant_id = p.product_variant_ids[:1].id @api.model def name_search(self, name="", args=None, operator="ilike", limit=100): - customer_id = self.env.context.get("customer") - if not customer_id: - return super().name_search(name, args, operator, limit) - res = super().name_search(name, args, operator, limit=100) - ids = [r[0] for r in res] - records = self.browse(ids) - records.mapped("last_invoice_date") - sorted_records = records.sorted( - key=(lambda r: r.last_invoice_date or datetime.datetime.min), reverse=True + variant_results = self.env["product.product"].name_search( + name, args, operator, limit=limit ) - return [(r.id, r.display_name) for r in sorted_records][:limit] - - def compute_agotime(self, datetime_field): - now = fields.Datetime.now() - rd = relativedelta(now, datetime_field) - if rd.years: - ago = f"{rd.years}y" - elif rd.months: - ago = f"{rd.months}mo" - elif rd.days: - ago = f"{rd.days}d" - elif rd.hours: - ago = f"{rd.hours}h" - elif rd.minutes: - ago = f"{rd.minutes}m" - elif rd.seconds: - ago = f"{rd.seconds}s" - else: - ago = "" - - return ago + if not variant_results: + return [] + variant_ids = [res[0] for res in variant_results] + variants = self.env["product.product"].browse(variant_ids) + templates = variants.mapped("product_tmpl_id") + return [(t.id, t.display_name) for t in templates][:limit] diff --git a/last_purchased_products/tests/test_last_order_product.py b/last_purchased_products/tests/test_last_order_product.py index fba4ebcc0ae..a8a745cc3b7 100644 --- a/last_purchased_products/tests/test_last_order_product.py +++ b/last_purchased_products/tests/test_last_order_product.py @@ -1,8 +1,10 @@ -from odoo.tests.common import TransactionCase -from odoo import fields, Command from datetime import timedelta +from odoo import fields, Command +from odoo.tests.common import TransactionCase, tagged + +@tagged('post_install', '-at_install') class TestLastOrderProduct(TransactionCase): @classmethod diff --git a/last_purchased_products/views/account_move_views.xml b/last_purchased_products/views/account_move_views.xml index 4a2949f9229..f4412f31d93 100644 --- a/last_purchased_products/views/account_move_views.xml +++ b/last_purchased_products/views/account_move_views.xml @@ -3,14 +3,20 @@ account.move.form.custom.display account.move - + { 'vendor': parent.partner_id if parent.move_type in ('in_invoice', 'in_refund') else False, - 'customer': parent.partner_id if parent.move_type not in ('in_invoice','in_refund') else False - } + 'customer': parent.partner_id if parent.move_type in ('out_invoice','out_refund') else False + } + + + + + {'order_id': parent.id, 'isPurchase': parent.move_type in ('in_invoice', 'in_refund')} + diff --git a/last_purchased_products/views/product_views.xml b/last_purchased_products/views/product_views.xml index 203d316b46b..94bd30e027a 100644 --- a/last_purchased_products/views/product_views.xml +++ b/last_purchased_products/views/product_views.xml @@ -8,6 +8,7 @@ + @@ -26,7 +27,12 @@ ago - + + ( + + ) + + diff --git a/last_purchased_products/views/purchase_order_views.xml b/last_purchased_products/views/purchase_order_views.xml index ab013b2c323..e42220b13fc 100644 --- a/last_purchased_products/views/purchase_order_views.xml +++ b/last_purchased_products/views/purchase_order_views.xml @@ -8,6 +8,9 @@ {'vendor': parent.partner_id} + + {'order_id': parent.id, 'isPurchase': True} + diff --git a/last_purchased_products/views/sale_order_views.xml b/last_purchased_products/views/sale_order_views.xml index 131f5537b93..17c28fa047b 100644 --- a/last_purchased_products/views/sale_order_views.xml +++ b/last_purchased_products/views/sale_order_views.xml @@ -1,13 +1,13 @@ - - - sale.order.form.custom.display - sale.order - - - - {'customer': parent.partner_id} - - - - + + + sale.order.form.custom.display + sale.order + + + + {'customer': parent.partner_id} + + + +