From fe709daba8be56c2cd0cdc88048649b3214bf248 Mon Sep 17 00:00:00 2001 From: ppch-odoo Date: Mon, 17 Mar 2025 12:10:02 +0530 Subject: [PATCH 1/7] [IMP] last_ordered_products: improved sale and purchase by adding this module - In product_product and product_template override method name_search to get last ordered time for selected customer. - Implemented functions to get last_sold_products and last_purchased_products based on customer's invoice and vendor's bill. - Added compute method to get last_order_time and time difference string. - Updated context in sale_order, purchase_order and account_move to get order_type and partner_id in context - In catalog, to re-organize displayed information inherited sale_order and sale_order_line, override some methods to get sale_uom of order line - In catalog, to re-organize displayed information inherited product_view_kanban_catalog view - Extend ProductCatalogOrderLine to get sale_uom in catalog - Patch ProductCatalogKanbanRecord to get information as per the requirement - Extend Autocomplete to get time difference string in product dropdown - Patch ProductLabelSectionAndNoteFieldAutocomplete to override function which will be used to get time difference string --- last_ordered_products/__init__.py | 1 + last_ordered_products/__manifest__.py | 23 +++ last_ordered_products/models/__init__.py | 4 + .../models/product_product.py | 153 ++++++++++++++++++ .../models/product_template.py | 153 ++++++++++++++++++ last_ordered_products/models/sale_order.py | 20 +++ .../models/sale_order_line.py | 23 +++ .../src/core/autocomplete/autocomplete.js | 5 + .../src/core/autocomplete/autocomplete.xml | 16 ++ .../src/product_catalog/kanban_record.js | 16 ++ .../product_catalog/order_line/order_line.js | 12 ++ .../product_catalog/order_line/order_line.xml | 19 +++ .../order_line/sale_order_line.js | 12 ++ .../product_label_section_and_note_field.js | 11 ++ .../views/account_move_form.xml | 15 ++ last_ordered_products/views/product_views.xml | 33 ++++ .../views/purchase_order_form.xml | 18 +++ .../views/sale_order_form.xml | 32 ++++ 18 files changed, 566 insertions(+) create mode 100644 last_ordered_products/__init__.py create mode 100644 last_ordered_products/__manifest__.py create mode 100644 last_ordered_products/models/__init__.py create mode 100644 last_ordered_products/models/product_product.py create mode 100644 last_ordered_products/models/product_template.py create mode 100644 last_ordered_products/models/sale_order.py create mode 100644 last_ordered_products/models/sale_order_line.py create mode 100644 last_ordered_products/static/src/core/autocomplete/autocomplete.js create mode 100644 last_ordered_products/static/src/core/autocomplete/autocomplete.xml create mode 100644 last_ordered_products/static/src/product_catalog/kanban_record.js create mode 100644 last_ordered_products/static/src/product_catalog/order_line/order_line.js create mode 100644 last_ordered_products/static/src/product_catalog/order_line/order_line.xml create mode 100644 last_ordered_products/static/src/product_catalog/order_line/sale_order_line.js create mode 100644 last_ordered_products/static/src/views/fields/product_label_section_and_note_field.js create mode 100644 last_ordered_products/views/account_move_form.xml create mode 100644 last_ordered_products/views/product_views.xml create mode 100644 last_ordered_products/views/purchase_order_form.xml create mode 100644 last_ordered_products/views/sale_order_form.xml diff --git a/last_ordered_products/__init__.py b/last_ordered_products/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/last_ordered_products/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/last_ordered_products/__manifest__.py b/last_ordered_products/__manifest__.py new file mode 100644 index 00000000000..cf134ae3c32 --- /dev/null +++ b/last_ordered_products/__manifest__.py @@ -0,0 +1,23 @@ +{ + 'name': "Last Ordered Products", + 'version': '1.0', + 'depends': ['sale_management', 'purchase', 'stock'], + 'author': "Parthav Chodvadiya (PPCH)", + 'category': '', + 'description': """ + Show last ordered products for customers in sale order and for vendors in purchase order + """, + 'data': [ + 'views/account_move_form.xml', + 'views/sale_order_form.xml', + 'views/purchase_order_form.xml', + 'views/product_views.xml', + ], + 'assets': { + 'web.assets_backend': [ + 'last_ordered_products/static/src/**/*.js', + 'last_ordered_products/static/src/**/*.xml', + ], + }, + 'license': 'LGPL-3', +} diff --git a/last_ordered_products/models/__init__.py b/last_ordered_products/models/__init__.py new file mode 100644 index 00000000000..f2f1aeb80ce --- /dev/null +++ b/last_ordered_products/models/__init__.py @@ -0,0 +1,4 @@ +from . import product_product +from . import product_template +from . import sale_order +from . import sale_order_line diff --git a/last_ordered_products/models/product_product.py b/last_ordered_products/models/product_product.py new file mode 100644 index 00000000000..c0e11bbd027 --- /dev/null +++ b/last_ordered_products/models/product_product.py @@ -0,0 +1,153 @@ +from datetime import datetime +from odoo import api, fields, models +from odoo.osv import expression + + +class ProductProduct(models.Model): + _inherit = 'product.product' + + last_order_time = fields.Datetime(compute='_compute_last_order_time') + last_date_str = fields.Char(compute='_compute_last_order_time') + + @api.depends_context('order_id') + def _compute_last_order_time(self): + """Compute the last order time for each product based on the latest sale or purchase.""" + + order_type = False + if self.env.context.get('active_model') == 'sale.order.line': + partner_id = self.env['sale.order'].browse(self.env.context.get('order_id')).partner_id.id + order_type = 'sale' + elif self.env.context.get('active_model') == 'purchase.order.line': + partner_id = self.env['purchase.order'].browse(self.env.context.get('order_id')).partner_id.id + order_type = 'purchase' + elif self.env.context.get('active_model') == 'account.journal': + active_id = self.env.context.get('active_id') + if active_id: + order_type = self.env['account.journal'].browse(active_id).type + partner_id = self.env.context.get('partner_id') or self.env.context.get('default_partner_id') + else: + partner_id = self.env.context.get('partner_id') + order_type = self.env.context.get('order_type') + + if not partner_id: + self.last_date_str = False + + last_ordered_products = {} + + if order_type == 'sale': + last_ordered_products = self._get_last_sold_products(partner_id) + elif order_type == 'purchase': + last_ordered_products = self._get_last_purchased_products(partner_id) + + for record in self: + last_date = last_ordered_products.get(record.id) + + record.last_order_time = last_date if last_date else False + record.last_date_str = self._get_time_ago_string(record.last_order_time) + + def _get_last_sold_products(self, partner_id): + '''Fetch products last sold to the given customer''' + + sale_orders = self.env['sale.order'].search([('partner_id', '=', partner_id)]) + + if not sale_orders: + return {} + + sale_order_lines = self.env['sale.order.line'].search([ + ('order_id', 'in', sale_orders.ids) + ]) + + invoices = self.env['account.move'].search([ + ('partner_id', '=', partner_id), + ('invoice_origin', 'in', sale_orders.mapped('name')) + ]) + + last_sale_ordered_products = {} + for inv in invoices: + for sol in sale_order_lines.filtered(lambda line: line.order_id.name == inv.invoice_origin): + product_id = sol.product_id.id + last_date = inv.create_date + if product_id not in last_sale_ordered_products or last_date > last_sale_ordered_products[product_id]: + last_sale_ordered_products[product_id] = last_date + + return last_sale_ordered_products + + def _get_last_purchased_products(self, partner_id): + '''Fetch products last purchased to the given vendor''' + + purchase_orders = self.env['purchase.order'].search([('partner_id', '=', partner_id)]) + + if not purchase_orders: + return {} + + purchase_order_line = self.env['purchase.order.line'].search([ + ('order_id', 'in', purchase_orders.ids) + ]) + + invoices = self.env['account.move'].search([ + ('partner_id', '=', partner_id), + ('invoice_origin', 'in', purchase_orders.mapped('name')) + ]) + + last_purchased_order_products = {} + for inv in invoices: + for sol in purchase_order_line.filtered(lambda line: line.order_id.name == inv.invoice_origin): + product_id = sol.product_id.id + last_date = inv.create_date + if product_id not in last_purchased_order_products or last_date > last_purchased_order_products[product_id]: + last_purchased_order_products[product_id] = last_date + + return last_purchased_order_products + + def _get_time_ago_string(self, last_date): + '''Convert datetime to human-readable time difference (e.g., "1d", "4h", "4mo")''' + + if not last_date: + return "" + + now = fields.Datetime.now() + diff = now - last_date + + if diff.days > 365: + return f"{diff.days // 365}y" + elif diff.days > 30: + return f"{diff.days // 30}mo" + elif diff.days > 0: + return f"{diff.days}d" + elif diff.seconds >= 3600: + return f"{diff.seconds // 3600}h" + elif diff.seconds >= 60: + return f"{diff.seconds // 60}m" + else: + return f"{diff.seconds}s" + + @api.model + def name_search(self, name='', args=None, operator='ilike', limit=100): + '''Modify product dropdown in sale order line to show last sold date''' + + domain = args or [] + partner_id = self.env.context.get('partner_id') + order_type = self.env.context.get('order_type') + active_id = self.env.context.get('active_id') + if active_id: + order_type = self.env['account.journal'].browse(active_id).type + + if partner_id: + last_ordered_products = {} + if order_type == 'sale': + last_ordered_products = self._get_last_sold_products(partner_id) + elif order_type == 'purchase': + last_ordered_products = self._get_last_purchased_products(partner_id) + + product_ids = list(last_ordered_products.keys()) + + products = self.search_fetch(expression.AND([domain, [('id', 'in', product_ids)], [("name", operator, name)]]), ['display_name'], limit=limit) + limit_rest = limit and limit - len(products) + if limit_rest is None or limit_rest > 0: + products |= self.search_fetch(expression.AND([domain, [('id', 'not in', product_ids)], [("name", operator, name)]]), ['display_name'], limit=limit_rest) + + products = sorted(products, key=lambda p: p.last_order_time if p.last_order_time else datetime.min, reverse=True) + + return [(product.id, product.display_name, product.last_date_str if product.last_date_str else False) for product in products] + + return super().name_search(name, args, operator, limit) diff --git a/last_ordered_products/models/product_template.py b/last_ordered_products/models/product_template.py new file mode 100644 index 00000000000..94f1df09b78 --- /dev/null +++ b/last_ordered_products/models/product_template.py @@ -0,0 +1,153 @@ +from datetime import datetime +from odoo import api, fields, models +from odoo.osv import expression + + +class ProductTemplate(models.Model): + _inherit = 'product.template' + + last_order_time = fields.Datetime(compute='_compute_last_order_time') + last_date_str = fields.Char(compute='_compute_last_order_time') + + @api.depends_context('order_id') + def _compute_last_order_time(self): + """Compute the last order time for each product based on the latest sale or purchase.""" + + order_type = False + if self.env.context.get('active_model') == 'sale.order.line': + partner_id = self.env['sale.order'].browse(self.env.context.get('order_id')).partner_id.id + order_type = 'sale' + elif self.env.context.get('active_model') == 'purchase.order.line': + partner_id = self.env['purchase.order'].browse(self.env.context.get('order_id')).partner_id.id + order_type = 'purchase' + elif self.env.context.get('active_model') == 'account.journal': + active_id = self.env.context.get('active_id') + if active_id: + order_type = self.env['account.journal'].browse(active_id).type + partner_id = self.env.context.get('partner_id') or self.env.context.get('default_partner_id') + else: + partner_id = self.env.context.get('partner_id') + order_type = self.env.context.get('order_type') + + if not partner_id: + self.last_date_str = False + + last_ordered_products = {} + + if order_type == 'sale': + last_ordered_products = self._get_last_sold_products(partner_id) + elif order_type == 'purchase': + last_ordered_products = self._get_last_purchased_products(partner_id) + + for record in self: + last_date = last_ordered_products.get(record.id) + + record.last_order_time = last_date if last_date else False + record.last_date_str = self._get_time_ago_string(record.last_order_time) + + def _get_last_sold_products(self, partner_id): + '''Fetch products last sold to the given customer''' + + sale_orders = self.env['sale.order'].search([('partner_id', '=', partner_id)]) + + if not sale_orders: + return {} + + sale_order_lines = self.env['sale.order.line'].search([ + ('order_id', 'in', sale_orders.ids) + ]) + + invoices = self.env['account.move'].search([ + ('partner_id', '=', partner_id), + ('invoice_origin', 'in', sale_orders.mapped('name')) + ]) + + last_sale_ordered_products = {} + for inv in invoices: + for sol in sale_order_lines.filtered(lambda line: line.order_id.name == inv.invoice_origin): + product_id = sol.product_id.product_tmpl_id.id + last_date = inv.create_date + if product_id not in last_sale_ordered_products or last_date > last_sale_ordered_products[product_id]: + last_sale_ordered_products[product_id] = last_date + + return last_sale_ordered_products + + def _get_last_purchased_products(self, partner_id): + '''Fetch products last purchased to the given vendor''' + + purchase_orders = self.env['purchase.order'].search([('partner_id', '=', partner_id)]) + + if not purchase_orders: + return {} + + purchase_order_line = self.env['purchase.order.line'].search([ + ('order_id', 'in', purchase_orders.ids) + ]) + + invoices = self.env['account.move'].search([ + ('partner_id', '=', partner_id), + ('invoice_origin', 'in', purchase_orders.mapped('name')) + ]) + + last_purchased_order_products = {} + for inv in invoices: + for sol in purchase_order_line.filtered(lambda line: line.order_id.name == inv.invoice_origin): + product_id = sol.product_id.product_tmpl_id.id + last_date = inv.create_date + if product_id not in last_purchased_order_products or last_date > last_purchased_order_products[product_id]: + last_purchased_order_products[product_id] = last_date + + return last_purchased_order_products + + def _get_time_ago_string(self, last_date): + '''Convert datetime to human-readable time difference (e.g., "1d", "4h", "4mo")''' + + if not last_date: + return "" + + now = fields.Datetime.now() + diff = now - last_date + + if diff.days > 365: + return f"{diff.days // 365}y" + elif diff.days > 30: + return f"{diff.days // 30}mo" + elif diff.days > 0: + return f"{diff.days}d" + elif diff.seconds >= 3600: + return f"{diff.seconds // 3600}h" + elif diff.seconds >= 60: + return f"{diff.seconds // 60}m" + else: + return f"{diff.seconds}s" + + @api.model + def name_search(self, name='', args=None, operator='ilike', limit=100): + '''Modify product dropdown in sale order line to show last sold date''' + + domain = args or [] + partner_id = self.env.context.get('partner_id') + order_type = self.env.context.get('order_type') + active_id = self.env.context.get('active_id') + if active_id: + order_type = self.env['account.journal'].browse(active_id).type + + if partner_id: + last_ordered_products = {} + if order_type == 'sale': + last_ordered_products = self._get_last_sold_products(partner_id) + elif order_type == 'purchase': + last_ordered_products = self._get_last_purchased_products(partner_id) + + product_ids = list(last_ordered_products.keys()) + + products = self.search_fetch(expression.AND([domain, [('id', 'in', product_ids)], [("name", operator, name)]]), ['display_name'], limit=limit) + limit_rest = limit and limit - len(products) + if limit_rest is None or limit_rest > 0: + products |= self.search_fetch(expression.AND([domain, [('id', 'not in', product_ids)], [("name", operator, name)]]), ['display_name'], limit=limit_rest) + + products = sorted(products, key=lambda p: p.last_order_time if p.last_order_time else datetime.min, reverse=True) + + return [(product.id, product.display_name, product.last_date_str if product.last_date_str else False) for product in products] + + return super().name_search(name, args, operator, limit) diff --git a/last_ordered_products/models/sale_order.py b/last_ordered_products/models/sale_order.py new file mode 100644 index 00000000000..450676866d0 --- /dev/null +++ b/last_ordered_products/models/sale_order.py @@ -0,0 +1,20 @@ +from odoo import models + + +class SaleOrder(models.Model): + _inherit = 'sale.order' + + def _get_action_add_from_catalog_extra_context(self): + return { + **super()._get_action_add_from_catalog_extra_context(), + 'display_uom': self.env.user.has_group('uom.group_uom'), + } + + def _get_product_catalog_order_data(self, products, **kwargs): + res = super()._get_product_catalog_order_data(products, **kwargs) + for product in products: + res[product.id]['uom'] = { + 'display_name': product.uom_id.display_name, + 'id': product.uom_id.id, + } + return res diff --git a/last_ordered_products/models/sale_order_line.py b/last_ordered_products/models/sale_order_line.py new file mode 100644 index 00000000000..80022a64065 --- /dev/null +++ b/last_ordered_products/models/sale_order_line.py @@ -0,0 +1,23 @@ +from odoo import models + + +class SaleOrderLine(models.Model): + _inherit = 'sale.order.line' + + def _get_product_catalog_lines_data(self, **kwargs): + res = super()._get_product_catalog_lines_data(**kwargs) + if len(self) == 1: + res.update({ + 'uom': { + 'display_name': self.product_id.uom_id.display_name, + 'id': self.product_id.uom_id.id, + }, + }) + if self.product_id.uom_id != self.product_uom: + res['sale_uom'] = { + 'display_name': self.product_uom.display_name, + 'id': self.product_uom.id, + } + return res + else: + return res diff --git a/last_ordered_products/static/src/core/autocomplete/autocomplete.js b/last_ordered_products/static/src/core/autocomplete/autocomplete.js new file mode 100644 index 00000000000..1b2eae5d97d --- /dev/null +++ b/last_ordered_products/static/src/core/autocomplete/autocomplete.js @@ -0,0 +1,5 @@ +import { AutoComplete } from "@web/core/autocomplete/autocomplete"; + +export class OrderedProductsAutoComplete extends AutoComplete { + static template = "last_ordered_products.OrderedProductsAutoComplete"; +} diff --git a/last_ordered_products/static/src/core/autocomplete/autocomplete.xml b/last_ordered_products/static/src/core/autocomplete/autocomplete.xml new file mode 100644 index 00000000000..406e46fe501 --- /dev/null +++ b/last_ordered_products/static/src/core/autocomplete/autocomplete.xml @@ -0,0 +1,16 @@ + + + + + +
+ + +
+
+ + + +
+
+
diff --git a/last_ordered_products/static/src/product_catalog/kanban_record.js b/last_ordered_products/static/src/product_catalog/kanban_record.js new file mode 100644 index 00000000000..2cbc2266ca3 --- /dev/null +++ b/last_ordered_products/static/src/product_catalog/kanban_record.js @@ -0,0 +1,16 @@ +import { ProductCatalogKanbanRecord } from "@product/product_catalog/kanban_record"; +import { ProductCatalogLastOrderOrderLine } from "./order_line/order_line"; +import { patch } from "@web/core/utils/patch"; + +patch(ProductCatalogKanbanRecord.prototype, { + setup() { + super.setup(); + }, + + get orderLineComponent() { + if (this.env.orderResModel === "sale.order") { + return ProductCatalogLastOrderOrderLine; + } + return super.orderLineComponent; + }, +}); diff --git a/last_ordered_products/static/src/product_catalog/order_line/order_line.js b/last_ordered_products/static/src/product_catalog/order_line/order_line.js new file mode 100644 index 00000000000..4c0569b7eb4 --- /dev/null +++ b/last_ordered_products/static/src/product_catalog/order_line/order_line.js @@ -0,0 +1,12 @@ +import { ProductCatalogOrderLine } from "@product/product_catalog/order_line/order_line"; + +export class ProductCatalogLastOrderOrderLine extends ProductCatalogOrderLine { + static template = "ProductCatalogLastOrderOrderLine"; + static props = { + ...ProductCatalogLastOrderOrderLine.props, + sale_uom: { type: Object, optional: true }, + uom: Object, + }; +} + + diff --git a/last_ordered_products/static/src/product_catalog/order_line/order_line.xml b/last_ordered_products/static/src/product_catalog/order_line/order_line.xml new file mode 100644 index 00000000000..80e652f266a --- /dev/null +++ b/last_ordered_products/static/src/product_catalog/order_line/order_line.xml @@ -0,0 +1,19 @@ + + + + + !this.env.displayUoM + + + + + / + + + + + + + diff --git a/last_ordered_products/static/src/product_catalog/order_line/sale_order_line.js b/last_ordered_products/static/src/product_catalog/order_line/sale_order_line.js new file mode 100644 index 00000000000..bfd35bcaa7f --- /dev/null +++ b/last_ordered_products/static/src/product_catalog/order_line/sale_order_line.js @@ -0,0 +1,12 @@ +import { ProductCatalogSaleOrderLine } from "@sale_stock/product_catalog/sale_order_line/sale_order_line"; +import { patch } from "@web/core/utils/patch"; + +patch(ProductCatalogSaleOrderLine, { + template: "ProductCatalogLastOrderOrderLine", + props: { + ...ProductCatalogSaleOrderLine.props, + deliveredQty: Number, + sale_uom: { type: Object, optional: true }, + uom: Object, + } +}); diff --git a/last_ordered_products/static/src/views/fields/product_label_section_and_note_field.js b/last_ordered_products/static/src/views/fields/product_label_section_and_note_field.js new file mode 100644 index 00000000000..9effd3d0dc5 --- /dev/null +++ b/last_ordered_products/static/src/views/fields/product_label_section_and_note_field.js @@ -0,0 +1,11 @@ +import { ProductLabelSectionAndNoteFieldAutocomplete } from "@account/components/product_label_section_and_note_field/product_label_section_and_note_field"; +import { patch } from "@web/core/utils/patch"; + +patch(ProductLabelSectionAndNoteFieldAutocomplete.prototype, { + mapRecordToOption(result) { + let res = super.mapRecordToOption(result) + let time_str = result[2] ? result[2] : "" + res['time_str'] = time_str + return res + }, +}); diff --git a/last_ordered_products/views/account_move_form.xml b/last_ordered_products/views/account_move_form.xml new file mode 100644 index 00000000000..1a2e0221af2 --- /dev/null +++ b/last_ordered_products/views/account_move_form.xml @@ -0,0 +1,15 @@ + + + + account.move.view.last.product.sold + account.move + + + + { + 'partner_id': parent.partner_id, + } + + + + diff --git a/last_ordered_products/views/product_views.xml b/last_ordered_products/views/product_views.xml new file mode 100644 index 00000000000..db483bd6924 --- /dev/null +++ b/last_ordered_products/views/product_views.xml @@ -0,0 +1,33 @@ + + + + product.view.kanban.catalog.last.order.products + product.product + + + +
+ +
+
+ + +
+ On Hand + + + (+) + + + () + +
+
+ + + ago + +
+
+
diff --git a/last_ordered_products/views/purchase_order_form.xml b/last_ordered_products/views/purchase_order_form.xml new file mode 100644 index 00000000000..d3d3900cf33 --- /dev/null +++ b/last_ordered_products/views/purchase_order_form.xml @@ -0,0 +1,18 @@ + + + + purchase.order.view.last.product.sold + purchase.order + + + + { + 'partner_id': parent.partner_id, + 'quantity': product_qty, + 'company_id': parent.company_id, + 'order_type': 'purchase' + } + + + + diff --git a/last_ordered_products/views/sale_order_form.xml b/last_ordered_products/views/sale_order_form.xml new file mode 100644 index 00000000000..cd3fdf1be14 --- /dev/null +++ b/last_ordered_products/views/sale_order_form.xml @@ -0,0 +1,32 @@ + + + + sale.order.view.last.product.sold + sale.order + + + + { + 'partner_id': parent.partner_id, + 'quantity': product_uom_qty, + 'pricelist': parent.pricelist_id, + 'uom': product_uom, + 'company_id': parent.company_id, + 'default_lst_price': price_unit, + 'order_type': 'sale' + } + + + { + 'partner_id': parent.partner_id, + 'quantity': product_uom_qty, + 'pricelist': parent.pricelist_id, + 'uom': product_uom, + 'company_id': parent.company_id, + 'default_lst_price': price_unit, + 'order_type': 'sale' + } + + + + From 484372a3c0cf985789015ceb2e524cd2039d2964 Mon Sep 17 00:00:00 2001 From: ppch-odoo Date: Mon, 17 Mar 2025 19:04:55 +0530 Subject: [PATCH 2/7] [IMP] last_ordered_products: improved sale and purchase by adding this module - Optimizing code - Try to remove unnecessary code --- .../models/product_product.py | 47 ++++++++++--------- .../models/product_template.py | 47 ++++++++++--------- .../models/sale_order_line.py | 6 +-- 3 files changed, 51 insertions(+), 49 deletions(-) diff --git a/last_ordered_products/models/product_product.py b/last_ordered_products/models/product_product.py index c0e11bbd027..60093535fbc 100644 --- a/last_ordered_products/models/product_product.py +++ b/last_ordered_products/models/product_product.py @@ -20,7 +20,7 @@ def _compute_last_order_time(self): elif self.env.context.get('active_model') == 'purchase.order.line': partner_id = self.env['purchase.order'].browse(self.env.context.get('order_id')).partner_id.id order_type = 'purchase' - elif self.env.context.get('active_model') == 'account.journal': + elif self.env.context.get('active_model') == 'account.journal': active_id = self.env.context.get('active_id') if active_id: order_type = self.env['account.journal'].browse(active_id).type @@ -30,7 +30,10 @@ def _compute_last_order_time(self): order_type = self.env.context.get('order_type') if not partner_id: - self.last_date_str = False + for record in self: + record.last_order_time = False + record.last_date_str = False + return last_ordered_products = {} @@ -43,30 +46,29 @@ def _compute_last_order_time(self): last_date = last_ordered_products.get(record.id) record.last_order_time = last_date if last_date else False - record.last_date_str = self._get_time_ago_string(record.last_order_time) + record.last_date_str = self._get_time_ago_string(last_date) if last_date else False def _get_last_sold_products(self, partner_id): '''Fetch products last sold to the given customer''' - sale_orders = self.env['sale.order'].search([('partner_id', '=', partner_id)]) - - if not sale_orders: - return {} - sale_order_lines = self.env['sale.order.line'].search([ - ('order_id', 'in', sale_orders.ids) + ('order_id.partner_id', '=', partner_id) ]) + if not sale_order_lines: + return {} + invoices = self.env['account.move'].search([ ('partner_id', '=', partner_id), - ('invoice_origin', 'in', sale_orders.mapped('name')) + ('invoice_origin', 'in', sale_order_lines.order_id.mapped('name')) ]) last_sale_ordered_products = {} - for inv in invoices: - for sol in sale_order_lines.filtered(lambda line: line.order_id.name == inv.invoice_origin): + invoice_dates = {inv.invoice_origin: inv.create_date for inv in invoices} + for sol in sale_order_lines: + last_date = invoice_dates.get(sol.order_id.name) + if last_date: product_id = sol.product_id.id - last_date = inv.create_date if product_id not in last_sale_ordered_products or last_date > last_sale_ordered_products[product_id]: last_sale_ordered_products[product_id] = last_date @@ -75,25 +77,24 @@ def _get_last_sold_products(self, partner_id): def _get_last_purchased_products(self, partner_id): '''Fetch products last purchased to the given vendor''' - purchase_orders = self.env['purchase.order'].search([('partner_id', '=', partner_id)]) - - if not purchase_orders: - return {} - purchase_order_line = self.env['purchase.order.line'].search([ - ('order_id', 'in', purchase_orders.ids) + ('order_id.partner_id', '=', partner_id) ]) + if not purchase_order_line: + return {} + invoices = self.env['account.move'].search([ ('partner_id', '=', partner_id), - ('invoice_origin', 'in', purchase_orders.mapped('name')) + ('invoice_origin', 'in', purchase_order_line.order_id.mapped('name')) ]) last_purchased_order_products = {} - for inv in invoices: - for sol in purchase_order_line.filtered(lambda line: line.order_id.name == inv.invoice_origin): + invoice_dates = {inv.invoice_origin: inv.create_date for inv in invoices} + for sol in purchase_order_line: + last_date = invoice_dates.get(sol.order_id.name) + if last_date: product_id = sol.product_id.id - last_date = inv.create_date if product_id not in last_purchased_order_products or last_date > last_purchased_order_products[product_id]: last_purchased_order_products[product_id] = last_date diff --git a/last_ordered_products/models/product_template.py b/last_ordered_products/models/product_template.py index 94f1df09b78..874af833b33 100644 --- a/last_ordered_products/models/product_template.py +++ b/last_ordered_products/models/product_template.py @@ -20,7 +20,7 @@ def _compute_last_order_time(self): elif self.env.context.get('active_model') == 'purchase.order.line': partner_id = self.env['purchase.order'].browse(self.env.context.get('order_id')).partner_id.id order_type = 'purchase' - elif self.env.context.get('active_model') == 'account.journal': + elif self.env.context.get('active_model') == 'account.journal': active_id = self.env.context.get('active_id') if active_id: order_type = self.env['account.journal'].browse(active_id).type @@ -30,7 +30,10 @@ def _compute_last_order_time(self): order_type = self.env.context.get('order_type') if not partner_id: - self.last_date_str = False + for record in self: + record.last_order_time = False + record.last_date_str = False + return last_ordered_products = {} @@ -43,30 +46,29 @@ def _compute_last_order_time(self): last_date = last_ordered_products.get(record.id) record.last_order_time = last_date if last_date else False - record.last_date_str = self._get_time_ago_string(record.last_order_time) + record.last_date_str = self._get_time_ago_string(last_date) if last_date else False def _get_last_sold_products(self, partner_id): '''Fetch products last sold to the given customer''' - sale_orders = self.env['sale.order'].search([('partner_id', '=', partner_id)]) - - if not sale_orders: - return {} - sale_order_lines = self.env['sale.order.line'].search([ - ('order_id', 'in', sale_orders.ids) + ('order_id.partner_id', '=', partner_id) ]) + if not sale_order_lines: + return {} + invoices = self.env['account.move'].search([ ('partner_id', '=', partner_id), - ('invoice_origin', 'in', sale_orders.mapped('name')) + ('invoice_origin', 'in', sale_order_lines.order_id.mapped('name')) ]) last_sale_ordered_products = {} - for inv in invoices: - for sol in sale_order_lines.filtered(lambda line: line.order_id.name == inv.invoice_origin): + invoice_dates = {inv.invoice_origin: inv.create_date for inv in invoices} + for sol in sale_order_lines: + last_date = invoice_dates.get(sol.order_id.name) + if last_date: product_id = sol.product_id.product_tmpl_id.id - last_date = inv.create_date if product_id not in last_sale_ordered_products or last_date > last_sale_ordered_products[product_id]: last_sale_ordered_products[product_id] = last_date @@ -75,25 +77,24 @@ def _get_last_sold_products(self, partner_id): def _get_last_purchased_products(self, partner_id): '''Fetch products last purchased to the given vendor''' - purchase_orders = self.env['purchase.order'].search([('partner_id', '=', partner_id)]) - - if not purchase_orders: - return {} - purchase_order_line = self.env['purchase.order.line'].search([ - ('order_id', 'in', purchase_orders.ids) + ('order_id.partner_id', '=', partner_id) ]) + if not purchase_order_line: + return {} + invoices = self.env['account.move'].search([ ('partner_id', '=', partner_id), - ('invoice_origin', 'in', purchase_orders.mapped('name')) + ('invoice_origin', 'in', purchase_order_line.order_id.mapped('name')) ]) last_purchased_order_products = {} - for inv in invoices: - for sol in purchase_order_line.filtered(lambda line: line.order_id.name == inv.invoice_origin): + invoice_dates = {inv.invoice_origin: inv.create_date for inv in invoices} + for sol in purchase_order_line: + last_date = invoice_dates.get(sol.order_id.name) + if last_date: product_id = sol.product_id.product_tmpl_id.id - last_date = inv.create_date if product_id not in last_purchased_order_products or last_date > last_purchased_order_products[product_id]: last_purchased_order_products[product_id] = last_date diff --git a/last_ordered_products/models/sale_order_line.py b/last_ordered_products/models/sale_order_line.py index 80022a64065..311941f98e0 100644 --- a/last_ordered_products/models/sale_order_line.py +++ b/last_ordered_products/models/sale_order_line.py @@ -15,9 +15,9 @@ def _get_product_catalog_lines_data(self, **kwargs): }) if self.product_id.uom_id != self.product_uom: res['sale_uom'] = { - 'display_name': self.product_uom.display_name, - 'id': self.product_uom.id, - } + 'display_name': self.product_uom.display_name, + 'id': self.product_uom.id, + } return res else: return res From 1932940709fbb1e87f843e58adefad3e15c718b2 Mon Sep 17 00:00:00 2001 From: ppch-odoo Date: Tue, 18 Mar 2025 14:33:18 +0530 Subject: [PATCH 3/7] [IMP] last_ordered_products: improved sale and purchase by adding this module - Removed unnecessary code - In product catalog also products will be sorted based on invoice creation date --- .../models/product_template.py | 47 +------------------ .../models/sale_order_line.py | 14 ++---- .../src/core/autocomplete/autocomplete.xml | 2 +- .../src/product_catalog/kanban_model.js | 10 ++++ .../product_catalog/order_line/order_line.js | 2 - last_ordered_products/views/product_views.xml | 1 + 6 files changed, 19 insertions(+), 57 deletions(-) create mode 100644 last_ordered_products/static/src/product_catalog/kanban_model.js diff --git a/last_ordered_products/models/product_template.py b/last_ordered_products/models/product_template.py index 874af833b33..bf19c4f1e1c 100644 --- a/last_ordered_products/models/product_template.py +++ b/last_ordered_products/models/product_template.py @@ -13,21 +13,8 @@ class ProductTemplate(models.Model): def _compute_last_order_time(self): """Compute the last order time for each product based on the latest sale or purchase.""" - order_type = False - if self.env.context.get('active_model') == 'sale.order.line': - partner_id = self.env['sale.order'].browse(self.env.context.get('order_id')).partner_id.id - order_type = 'sale' - elif self.env.context.get('active_model') == 'purchase.order.line': - partner_id = self.env['purchase.order'].browse(self.env.context.get('order_id')).partner_id.id - order_type = 'purchase' - elif self.env.context.get('active_model') == 'account.journal': - active_id = self.env.context.get('active_id') - if active_id: - order_type = self.env['account.journal'].browse(active_id).type - partner_id = self.env.context.get('partner_id') or self.env.context.get('default_partner_id') - else: - partner_id = self.env.context.get('partner_id') - order_type = self.env.context.get('order_type') + partner_id = self.env.context.get('partner_id') + order_type = self.env.context.get('order_type') if not partner_id: for record in self: @@ -39,8 +26,6 @@ def _compute_last_order_time(self): if order_type == 'sale': last_ordered_products = self._get_last_sold_products(partner_id) - elif order_type == 'purchase': - last_ordered_products = self._get_last_purchased_products(partner_id) for record in self: last_date = last_ordered_products.get(record.id) @@ -74,32 +59,6 @@ def _get_last_sold_products(self, partner_id): return last_sale_ordered_products - def _get_last_purchased_products(self, partner_id): - '''Fetch products last purchased to the given vendor''' - - purchase_order_line = self.env['purchase.order.line'].search([ - ('order_id.partner_id', '=', partner_id) - ]) - - if not purchase_order_line: - return {} - - invoices = self.env['account.move'].search([ - ('partner_id', '=', partner_id), - ('invoice_origin', 'in', purchase_order_line.order_id.mapped('name')) - ]) - - last_purchased_order_products = {} - invoice_dates = {inv.invoice_origin: inv.create_date for inv in invoices} - for sol in purchase_order_line: - last_date = invoice_dates.get(sol.order_id.name) - if last_date: - product_id = sol.product_id.product_tmpl_id.id - if product_id not in last_purchased_order_products or last_date > last_purchased_order_products[product_id]: - last_purchased_order_products[product_id] = last_date - - return last_purchased_order_products - def _get_time_ago_string(self, last_date): '''Convert datetime to human-readable time difference (e.g., "1d", "4h", "4mo")''' @@ -137,8 +96,6 @@ def name_search(self, name='', args=None, operator='ilike', limit=100): last_ordered_products = {} if order_type == 'sale': last_ordered_products = self._get_last_sold_products(partner_id) - elif order_type == 'purchase': - last_ordered_products = self._get_last_purchased_products(partner_id) product_ids = list(last_ordered_products.keys()) diff --git a/last_ordered_products/models/sale_order_line.py b/last_ordered_products/models/sale_order_line.py index 311941f98e0..dec3985a981 100644 --- a/last_ordered_products/models/sale_order_line.py +++ b/last_ordered_products/models/sale_order_line.py @@ -7,17 +7,13 @@ class SaleOrderLine(models.Model): def _get_product_catalog_lines_data(self, **kwargs): res = super()._get_product_catalog_lines_data(**kwargs) if len(self) == 1: - res.update({ - 'uom': { - 'display_name': self.product_id.uom_id.display_name, - 'id': self.product_id.uom_id.id, - }, - }) + res['uom'] = { + 'display_name': self.product_id.uom_id.display_name, + 'id': self.product_id.uom_id.id, + } if self.product_id.uom_id != self.product_uom: res['sale_uom'] = { 'display_name': self.product_uom.display_name, 'id': self.product_uom.id, } - return res - else: - return res + return res diff --git a/last_ordered_products/static/src/core/autocomplete/autocomplete.xml b/last_ordered_products/static/src/core/autocomplete/autocomplete.xml index 406e46fe501..2cc104497fc 100644 --- a/last_ordered_products/static/src/core/autocomplete/autocomplete.xml +++ b/last_ordered_products/static/src/core/autocomplete/autocomplete.xml @@ -3,7 +3,7 @@ -
+
diff --git a/last_ordered_products/static/src/product_catalog/kanban_model.js b/last_ordered_products/static/src/product_catalog/kanban_model.js new file mode 100644 index 00000000000..d8a2bac35d0 --- /dev/null +++ b/last_ordered_products/static/src/product_catalog/kanban_model.js @@ -0,0 +1,10 @@ +import { ProductCatalogKanbanModel } from "@product/product_catalog/kanban_model"; +import { patch } from "@web/core/utils/patch"; + +patch(ProductCatalogKanbanModel.prototype, { + async _loadData(params){ + const result = await super._loadData(...arguments); + result.records.sort((a, b) => new Date(b.last_order_time) - new Date(a.last_order_time)); + return result; + } +}); diff --git a/last_ordered_products/static/src/product_catalog/order_line/order_line.js b/last_ordered_products/static/src/product_catalog/order_line/order_line.js index 4c0569b7eb4..2647e0847eb 100644 --- a/last_ordered_products/static/src/product_catalog/order_line/order_line.js +++ b/last_ordered_products/static/src/product_catalog/order_line/order_line.js @@ -8,5 +8,3 @@ export class ProductCatalogLastOrderOrderLine extends ProductCatalogOrderLine { uom: Object, }; } - - diff --git a/last_ordered_products/views/product_views.xml b/last_ordered_products/views/product_views.xml index db483bd6924..2003a0351a4 100644 --- a/last_ordered_products/views/product_views.xml +++ b/last_ordered_products/views/product_views.xml @@ -27,6 +27,7 @@ ago + From 7922c9039e7875806570ed854757807ea872605d Mon Sep 17 00:00:00 2001 From: ppch-odoo Date: Wed, 19 Mar 2025 18:55:05 +0530 Subject: [PATCH 4/7] [IMP] last_ordered_products: testcases added - testcases added --- last_ordered_products/tests/__init__.py | 1 + .../tests/test_last_ordered_products.py | 158 ++++++++++++++++++ 2 files changed, 159 insertions(+) create mode 100644 last_ordered_products/tests/__init__.py create mode 100644 last_ordered_products/tests/test_last_ordered_products.py diff --git a/last_ordered_products/tests/__init__.py b/last_ordered_products/tests/__init__.py new file mode 100644 index 00000000000..0b0dffef5ed --- /dev/null +++ b/last_ordered_products/tests/__init__.py @@ -0,0 +1 @@ +from . import test_last_ordered_products diff --git a/last_ordered_products/tests/test_last_ordered_products.py b/last_ordered_products/tests/test_last_ordered_products.py new file mode 100644 index 00000000000..435eb547b8f --- /dev/null +++ b/last_ordered_products/tests/test_last_ordered_products.py @@ -0,0 +1,158 @@ +from odoo.tests.common import TransactionCase +from odoo.tests import tagged + + +@tagged('post_install', '-at_install') +class LastOrderedProductsTestCase(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.test_product_1 = cls.env['product.product'].create({ + 'name': 'New_Product_1', + 'purchase_method': 'purchase' + }) + cls.test_product_2 = cls.env['product.product'].create({ + 'name': 'New_Product_2', + 'purchase_method': 'purchase' + }) + cls.test_product_3 = cls.env['product.product'].create({ + 'name': 'New_Product_3', + 'purchase_method': 'purchase' + }) + cls.test_product_4 = cls.env['product.product'].create({ + 'name': 'New_Product_4', + 'purchase_method': 'purchase' + }) + + cls.test_partner_1 = cls.env['res.partner'].create({ + 'name': 'new_partner' + }) + + cls.test_journal_type_sale = cls.env['account.journal'].search([ + ('type', '=', 'sale') + ], limit=1) + + cls.test_journal_type_purchase = cls.env['account.journal'].search([ + ('type', '=', 'purchase') + ], limit=1) + + cls.test_sale_order_1 = cls.env['sale.order'].create({ + 'partner_id': cls.test_partner_1.id, + 'order_line': [ + (0, 0, { + 'name': cls.test_product_1.name, + 'product_id': cls.test_product_1.id, + 'product_uom_qty': 1, + 'product_uom': cls.test_product_1.uom_id.id, + 'price_unit': cls.test_product_1.list_price, + }) + ], + }) + cls.test_sale_order_1.action_confirm() + so_context = { + 'active_model': 'sale.order', + 'active_ids': [cls.test_sale_order_1.id], + 'active_id': cls.test_sale_order_1.id, + 'default_journal_id': cls.test_journal_type_sale.id, + } + payment_params = { + 'advance_payment_method': 'percentage', + 'amount': 50, + } + cls.test_downpayment = cls.env['sale.advance.payment.inv'].with_context(so_context).create(payment_params) + cls.test_downpayment.create_invoices() + cls.test_invoice_1 = cls.env['account.move'].search([ + ('invoice_origin', '=', cls.test_sale_order_1.name) + ]) + cls.env.cr.execute(""" UPDATE account_move set create_date = '%s' WHERE id = '%s'""" % ('2024-02-10', cls.test_invoice_1.id)) + + cls.test_sale_order_2 = cls.env['sale.order'].create({ + 'partner_id': cls.test_partner_1.id, + 'order_line': [ + (0, 0, { + 'name': cls.test_product_2.name, + 'product_id': cls.test_product_2.id, + 'product_uom_qty': 1, + 'product_uom': cls.test_product_2.uom_id.id, + 'price_unit': cls.test_product_2.list_price, + }) + ], + }) + cls.test_sale_order_2.action_confirm() + so_context = { + 'active_model': 'sale.order', + 'active_ids': [cls.test_sale_order_2.id], + 'active_id': cls.test_sale_order_2.id, + 'default_journal_id': cls.test_journal_type_sale.id, + } + payment_params = { + 'advance_payment_method': 'percentage', + 'amount': 50, + } + cls.test_downpayment = cls.env['sale.advance.payment.inv'].with_context(so_context).create(payment_params) + cls.test_downpayment.create_invoices() + cls.test_invoice_2 = cls.env['account.move'].search([ + ('invoice_origin', '=', cls.test_sale_order_2.name) + ]) + cls.env.cr.execute(""" UPDATE account_move set create_date = '%s' WHERE id = '%s'""" % ('2023-02-10', cls.test_invoice_2.id)) + + cls.test_purchase_order_1 = cls.env['purchase.order'].create({ + 'partner_id': cls.test_partner_1.id, + 'order_line': [ + (0, 0, { + 'name': cls.test_product_3.name, + 'product_id': cls.test_product_3.id, + 'product_uom_qty': 1, + 'product_uom': cls.test_product_3.uom_id.id, + 'price_unit': cls.test_product_3.list_price, + }) + ], + }) + cls.test_purchase_order_1.button_confirm() + cls.test_purchase_order_1.action_view_picking() + cls.test_purchase_order_1.action_create_invoice() + cls.test_bill_1 = cls.env['account.move'].search([ + ('invoice_origin', '=', cls.test_purchase_order_1.name) + ]) + cls.env.cr.execute(""" UPDATE account_move set create_date = '%s' WHERE id = '%s'""" % ('2024-02-10', cls.test_bill_1.id)) + + cls.test_purchase_order_2 = cls.env['purchase.order'].create({ + 'partner_id': cls.test_partner_1.id, + 'order_line': [ + (0, 0, { + 'name': cls.test_product_4.name, + 'product_id': cls.test_product_4.id, + 'product_uom_qty': 1, + 'product_uom': cls.test_product_4.uom_id.id, + 'price_unit': cls.test_product_4.list_price, + }) + ], + }) + cls.test_purchase_order_2.button_confirm() + cls.test_purchase_order_2.action_view_picking() + cls.test_purchase_order_2.action_create_invoice() + cls.test_bill_2 = cls.env['account.move'].search([ + ('invoice_origin', '=', cls.test_purchase_order_2.name) + ]) + cls.env.cr.execute(""" UPDATE account_move set create_date = '%s' WHERE id = '%s'""" % ('2023-02-10', cls.test_bill_2.id)) + + def test_product_variant_in_sale_order(self): + so_context = { + 'partner_id': self.test_partner_1.id, + 'order_type': 'sale' + } + res = self.env['product.product'].with_context(so_context).name_search() + res_ids = [r[0] for r in res] + self.assertEqual(self.test_product_1.id, res_ids[0]) + self.assertEqual(self.test_product_2.id, res_ids[1]) + + def test_product_variant_in_purchase_order(self): + po_context = { + 'partner_id': self.test_partner_1.id, + 'order_type': 'purchase' + } + res = self.env['product.product'].with_context(po_context).name_search() + res_ids = [r[0] for r in res] + self.assertEqual(self.test_product_3.id, res_ids[0]) + self.assertEqual(self.test_product_4.id, res_ids[1]) From 8f87feb2bde77d20827ca56aeeb87bfc2a81c03d Mon Sep 17 00:00:00 2001 From: ppch-odoo Date: Fri, 21 Mar 2025 18:57:38 +0530 Subject: [PATCH 5/7] [IMP] last_ordered_products: web_tour added - web_tour added to show flow of module --- last_ordered_products/__manifest__.py | 1 + .../data/last_ordered_products_tour.xml | 8 ++ .../src/js/tours/last_ordered_products.js | 129 ++++++++++++++++++ .../src/product_catalog/kanban_record.js | 4 - 4 files changed, 138 insertions(+), 4 deletions(-) create mode 100644 last_ordered_products/data/last_ordered_products_tour.xml create mode 100644 last_ordered_products/static/src/js/tours/last_ordered_products.js diff --git a/last_ordered_products/__manifest__.py b/last_ordered_products/__manifest__.py index cf134ae3c32..feefe3153eb 100644 --- a/last_ordered_products/__manifest__.py +++ b/last_ordered_products/__manifest__.py @@ -8,6 +8,7 @@ Show last ordered products for customers in sale order and for vendors in purchase order """, 'data': [ + 'data/last_ordered_products_tour.xml', 'views/account_move_form.xml', 'views/sale_order_form.xml', 'views/purchase_order_form.xml', diff --git a/last_ordered_products/data/last_ordered_products_tour.xml b/last_ordered_products/data/last_ordered_products_tour.xml new file mode 100644 index 00000000000..5fbebf389fb --- /dev/null +++ b/last_ordered_products/data/last_ordered_products_tour.xml @@ -0,0 +1,8 @@ + + + + last_ordered_products_tour + 1 + Congrats, best of luck catching such big fish! :) + + diff --git a/last_ordered_products/static/src/js/tours/last_ordered_products.js b/last_ordered_products/static/src/js/tours/last_ordered_products.js new file mode 100644 index 00000000000..bed9127e84d --- /dev/null +++ b/last_ordered_products/static/src/js/tours/last_ordered_products.js @@ -0,0 +1,129 @@ +/** @odoo-module **/ + +import { _t } from "@web/core/l10n/translation"; +import { registry } from "@web/core/registry"; +import { stepUtils } from "@web_tour/tour_service/tour_utils"; + +registry.category("web_tour.tours").add('last_ordered_products_tour', { + url: "/odoo", + steps: () => [ + stepUtils.showAppsMenuItem(), + { + isActive: ["community"], + trigger: ".o_app[data-menu-xmlid='sale.sale_menu_root']", + content: _t("Lets create a beautiful quotation in a few clicks ."), + tooltipPosition: "right", + run: "click", + }, + { + isActive: ["enterprise"], + trigger: ".o_app[data-menu-xmlid='sale.sale_menu_root']", + content: _t("Let’s create a beautiful quotation in a few clicks ."), + tooltipPosition: "bottom", + run: "click", + }, + { + trigger: "button.o_list_button_add", + content: _t("Build your first quotation right here!"), + tooltipPosition: "bottom", + run: "click", + }, + { + trigger: ".o_field_res_partner_many2one[name='partner_id'] input", + content: _t("Search a customer name ('Azure Interior'"), + tooltipPosition: "right", + run: "edit Azure", + }, + { + trigger: ".o-autocomplete--dropdown-item > a:contains('Azure')", + content: "Select azure interior", + run: "click", + }, + { + trigger: ".o_field_x2many_list_row_add a", + content: _t("Add a product"), + tooltipPosition: "bottom", + run: "click" + }, + { + trigger: ".o_field_widget[name='product_id'], .o_field_widget[name='product_template_id']", + content: _t("Select a product"), + tooltipPosition: "bottom", + run: "click" + }, + { + trigger: ".o_field_sol_product_many2one[name='product_id'] input, .o_field_sol_product_many2one[name='product_id'] input", + content: _t("Search a product (Large Cabinet)'"), + tooltipPosition: "top", + run: "edit Large Cabinet", + }, + { + trigger: ".o-autocomplete--dropdown-item > a:contains('Cabinet')", + content: _t("Select Large Cabinet"), + run: "click", + }, + { + trigger: ".o_form_button_save", + content: _t("Save Manually"), + run: "click", + }, + { + trigger: "button[name=action_confirm]", + content: _t("Confirm Sale Order"), + tooltipPosition: "bottom", + run: "click" + }, + { + trigger: "#create_invoice_percentage", + content: _t("Create Invoice"), + tooltipPosition: "bottom", + run: "click" + }, + { + trigger: "#create_invoice_open", + content: _t("Create Draft"), + tooltipPosition: "bottom", + run: "click" + }, + { + content: "Breadcrumb back to Quotations", + trigger: ".breadcrumb-item:contains('Quotations')", + run: "click", + }, + { + trigger: ".o_list_button_add", + content: _t("Create New Sale Order"), + tooltipPosition: "bottom", + run: "click" + }, + { + trigger: ".o_field_res_partner_many2one[name='partner_id'] input", + content: _t("Search a customer name ('Azure Interior'"), + tooltipPosition: "right", + run: "edit Azure", + }, + { + trigger: ".o-autocomplete--dropdown-item > a:contains('Azure')", + content: "Select azure interior", + run: "click", + }, + { + trigger: ".o_field_x2many_list_row_add a", + content: _t("Add a product"), + tooltipPosition: "bottom", + run: "click" + }, + { + trigger: ".o_field_widget[name='product_id'], .o_field_widget[name='product_template_id']", + content: _t("Select a product"), + tooltipPosition: "bottom", + run: "click" + }, + { + trigger: "div[name='product_id'] .o-autocomplete--dropdown-item > a:contains('[E-COM07]')", + content: _t("You can see here Product Large Cabinet which is invoiced few time ago to this customer"), + tooltipPosition: "right", + run: "click", + }, + ] +}); diff --git a/last_ordered_products/static/src/product_catalog/kanban_record.js b/last_ordered_products/static/src/product_catalog/kanban_record.js index 2cbc2266ca3..f1aa9de256c 100644 --- a/last_ordered_products/static/src/product_catalog/kanban_record.js +++ b/last_ordered_products/static/src/product_catalog/kanban_record.js @@ -3,10 +3,6 @@ import { ProductCatalogLastOrderOrderLine } from "./order_line/order_line"; import { patch } from "@web/core/utils/patch"; patch(ProductCatalogKanbanRecord.prototype, { - setup() { - super.setup(); - }, - get orderLineComponent() { if (this.env.orderResModel === "sale.order") { return ProductCatalogLastOrderOrderLine; From 9e01f9e8ccbec5167f5ab650afe9debb2bc3f1e2 Mon Sep 17 00:00:00 2001 From: ppch-odoo Date: Tue, 25 Mar 2025 19:01:13 +0530 Subject: [PATCH 6/7] [IMP] last_ordered_products: code improvement - Improved code by removing unnecessary code --- .../models/product_product.py | 30 +++---------------- .../models/product_template.py | 2 +- .../src/core/autocomplete/autocomplete.js | 5 ---- .../src/core/autocomplete/autocomplete.xml | 2 +- last_ordered_products/views/product_views.xml | 2 +- 5 files changed, 7 insertions(+), 34 deletions(-) delete mode 100644 last_ordered_products/static/src/core/autocomplete/autocomplete.js diff --git a/last_ordered_products/models/product_product.py b/last_ordered_products/models/product_product.py index 60093535fbc..6572592630d 100644 --- a/last_ordered_products/models/product_product.py +++ b/last_ordered_products/models/product_product.py @@ -46,7 +46,7 @@ def _compute_last_order_time(self): last_date = last_ordered_products.get(record.id) record.last_order_time = last_date if last_date else False - record.last_date_str = self._get_time_ago_string(last_date) if last_date else False + record.last_date_str = self.env['product.template']._get_time_ago_string(last_date) if last_date else False def _get_last_sold_products(self, partner_id): '''Fetch products last sold to the given customer''' @@ -100,28 +100,6 @@ def _get_last_purchased_products(self, partner_id): return last_purchased_order_products - def _get_time_ago_string(self, last_date): - '''Convert datetime to human-readable time difference (e.g., "1d", "4h", "4mo")''' - - if not last_date: - return "" - - now = fields.Datetime.now() - diff = now - last_date - - if diff.days > 365: - return f"{diff.days // 365}y" - elif diff.days > 30: - return f"{diff.days // 30}mo" - elif diff.days > 0: - return f"{diff.days}d" - elif diff.seconds >= 3600: - return f"{diff.seconds // 3600}h" - elif diff.seconds >= 60: - return f"{diff.seconds // 60}m" - else: - return f"{diff.seconds}s" - @api.model def name_search(self, name='', args=None, operator='ilike', limit=100): '''Modify product dropdown in sale order line to show last sold date''' @@ -130,7 +108,7 @@ def name_search(self, name='', args=None, operator='ilike', limit=100): partner_id = self.env.context.get('partner_id') order_type = self.env.context.get('order_type') active_id = self.env.context.get('active_id') - if active_id: + if not order_type and active_id: order_type = self.env['account.journal'].browse(active_id).type if partner_id: @@ -147,8 +125,8 @@ def name_search(self, name='', args=None, operator='ilike', limit=100): if limit_rest is None or limit_rest > 0: products |= self.search_fetch(expression.AND([domain, [('id', 'not in', product_ids)], [("name", operator, name)]]), ['display_name'], limit=limit_rest) - products = sorted(products, key=lambda p: p.last_order_time if p.last_order_time else datetime.min, reverse=True) + products = sorted(products, key=lambda p: last_ordered_products.get(p.id, datetime.min), reverse=True) - return [(product.id, product.display_name, product.last_date_str if product.last_date_str else False) for product in products] + return [(product.id, product.display_name, self.env['product.template']._get_time_ago_string(last_ordered_products.get(product.id, False))) for product in products] return super().name_search(name, args, operator, limit) diff --git a/last_ordered_products/models/product_template.py b/last_ordered_products/models/product_template.py index bf19c4f1e1c..33994c6a108 100644 --- a/last_ordered_products/models/product_template.py +++ b/last_ordered_products/models/product_template.py @@ -89,7 +89,7 @@ def name_search(self, name='', args=None, operator='ilike', limit=100): partner_id = self.env.context.get('partner_id') order_type = self.env.context.get('order_type') active_id = self.env.context.get('active_id') - if active_id: + if not order_type and active_id: order_type = self.env['account.journal'].browse(active_id).type if partner_id: diff --git a/last_ordered_products/static/src/core/autocomplete/autocomplete.js b/last_ordered_products/static/src/core/autocomplete/autocomplete.js deleted file mode 100644 index 1b2eae5d97d..00000000000 --- a/last_ordered_products/static/src/core/autocomplete/autocomplete.js +++ /dev/null @@ -1,5 +0,0 @@ -import { AutoComplete } from "@web/core/autocomplete/autocomplete"; - -export class OrderedProductsAutoComplete extends AutoComplete { - static template = "last_ordered_products.OrderedProductsAutoComplete"; -} diff --git a/last_ordered_products/static/src/core/autocomplete/autocomplete.xml b/last_ordered_products/static/src/core/autocomplete/autocomplete.xml index 2cc104497fc..4c08f5790d3 100644 --- a/last_ordered_products/static/src/core/autocomplete/autocomplete.xml +++ b/last_ordered_products/static/src/core/autocomplete/autocomplete.xml @@ -1,6 +1,6 @@ - +
diff --git a/last_ordered_products/views/product_views.xml b/last_ordered_products/views/product_views.xml index 2003a0351a4..e33bbcc6b35 100644 --- a/last_ordered_products/views/product_views.xml +++ b/last_ordered_products/views/product_views.xml @@ -12,7 +12,7 @@ -
On Hand From 558af97dfe80511881311b2eeddf7b61f7c4db17 Mon Sep 17 00:00:00 2001 From: ppch-odoo Date: Wed, 16 Apr 2025 18:19:12 +0530 Subject: [PATCH 7/7] [IMP] last_ordered_products: Bridge modules created - Created 3 other bridge modules to ensure that it will work if any individual module is installed. - Updated main module, Now it has only helper methods and compute method. --- last_ordered_products/__manifest__.py | 6 +- .../data/last_ordered_products_tour.xml | 8 -- last_ordered_products/models/__init__.py | 2 - .../models/product_product.py | 40 +----- .../models/product_template.py | 33 +---- .../src/core/autocomplete/autocomplete.xml | 4 +- .../src/js/tours/last_ordered_products.js | 129 ------------------ .../product_label_section_and_note_field.js | 3 + .../tests/test_last_ordered_products.py | 2 +- last_ordered_products/views/product_views.xml | 17 +-- last_ordered_products_purchase/__init__.py | 1 + .../__manifest__.py | 16 +++ .../models/__init__.py | 1 + .../models/product_product.py | 38 ++++++ .../views/account_move_form.xml | 0 .../views/purchase_order_form.xml | 0 last_ordered_products_sale/__init__.py | 1 + last_ordered_products_sale/__manifest__.py | 16 +++ last_ordered_products_sale/models/__init__.py | 4 + .../models/product_product.py | 36 +++++ .../models/product_template.py | 35 +++++ .../models/sale_order.py | 0 .../models/sale_order_line.py | 0 .../views/account_move_form.xml | 15 ++ .../views/sale_order_form.xml | 0 last_ordered_products_stock/__manifest__.py | 15 ++ .../views/product_views.xml | 23 ++++ 27 files changed, 212 insertions(+), 233 deletions(-) delete mode 100644 last_ordered_products/data/last_ordered_products_tour.xml delete mode 100644 last_ordered_products/static/src/js/tours/last_ordered_products.js create mode 100644 last_ordered_products_purchase/__init__.py create mode 100644 last_ordered_products_purchase/__manifest__.py create mode 100644 last_ordered_products_purchase/models/__init__.py create mode 100644 last_ordered_products_purchase/models/product_product.py rename {last_ordered_products => last_ordered_products_purchase}/views/account_move_form.xml (100%) rename {last_ordered_products => last_ordered_products_purchase}/views/purchase_order_form.xml (100%) create mode 100644 last_ordered_products_sale/__init__.py create mode 100644 last_ordered_products_sale/__manifest__.py create mode 100644 last_ordered_products_sale/models/__init__.py create mode 100644 last_ordered_products_sale/models/product_product.py create mode 100644 last_ordered_products_sale/models/product_template.py rename {last_ordered_products => last_ordered_products_sale}/models/sale_order.py (100%) rename {last_ordered_products => last_ordered_products_sale}/models/sale_order_line.py (100%) create mode 100644 last_ordered_products_sale/views/account_move_form.xml rename {last_ordered_products => last_ordered_products_sale}/views/sale_order_form.xml (100%) create mode 100644 last_ordered_products_stock/__manifest__.py create mode 100644 last_ordered_products_stock/views/product_views.xml diff --git a/last_ordered_products/__manifest__.py b/last_ordered_products/__manifest__.py index feefe3153eb..11dd32d0d47 100644 --- a/last_ordered_products/__manifest__.py +++ b/last_ordered_products/__manifest__.py @@ -1,17 +1,13 @@ { 'name': "Last Ordered Products", 'version': '1.0', - 'depends': ['sale_management', 'purchase', 'stock'], + 'depends': ['product'], 'author': "Parthav Chodvadiya (PPCH)", 'category': '', 'description': """ Show last ordered products for customers in sale order and for vendors in purchase order """, 'data': [ - 'data/last_ordered_products_tour.xml', - 'views/account_move_form.xml', - 'views/sale_order_form.xml', - 'views/purchase_order_form.xml', 'views/product_views.xml', ], 'assets': { diff --git a/last_ordered_products/data/last_ordered_products_tour.xml b/last_ordered_products/data/last_ordered_products_tour.xml deleted file mode 100644 index 5fbebf389fb..00000000000 --- a/last_ordered_products/data/last_ordered_products_tour.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - last_ordered_products_tour - 1 - Congrats, best of luck catching such big fish! :) - - diff --git a/last_ordered_products/models/__init__.py b/last_ordered_products/models/__init__.py index f2f1aeb80ce..18b37e85320 100644 --- a/last_ordered_products/models/__init__.py +++ b/last_ordered_products/models/__init__.py @@ -1,4 +1,2 @@ from . import product_product from . import product_template -from . import sale_order -from . import sale_order_line diff --git a/last_ordered_products/models/product_product.py b/last_ordered_products/models/product_product.py index 6572592630d..eb23f04e92e 100644 --- a/last_ordered_products/models/product_product.py +++ b/last_ordered_products/models/product_product.py @@ -1,6 +1,4 @@ -from datetime import datetime from odoo import api, fields, models -from odoo.osv import expression class ProductProduct(models.Model): @@ -11,7 +9,7 @@ class ProductProduct(models.Model): @api.depends_context('order_id') def _compute_last_order_time(self): - """Compute the last order time for each product based on the latest sale or purchase.""" + '''Compute the last order time for each product based on the latest sale or purchase.''' order_type = False if self.env.context.get('active_model') == 'sale.order.line': @@ -24,10 +22,7 @@ def _compute_last_order_time(self): active_id = self.env.context.get('active_id') if active_id: order_type = self.env['account.journal'].browse(active_id).type - partner_id = self.env.context.get('partner_id') or self.env.context.get('default_partner_id') - else: - partner_id = self.env.context.get('partner_id') - order_type = self.env.context.get('order_type') + partner_id = self.env.context.get('default_partner_id') if not partner_id: for record in self: @@ -99,34 +94,3 @@ def _get_last_purchased_products(self, partner_id): last_purchased_order_products[product_id] = last_date return last_purchased_order_products - - @api.model - def name_search(self, name='', args=None, operator='ilike', limit=100): - '''Modify product dropdown in sale order line to show last sold date''' - - domain = args or [] - partner_id = self.env.context.get('partner_id') - order_type = self.env.context.get('order_type') - active_id = self.env.context.get('active_id') - if not order_type and active_id: - order_type = self.env['account.journal'].browse(active_id).type - - if partner_id: - last_ordered_products = {} - if order_type == 'sale': - last_ordered_products = self._get_last_sold_products(partner_id) - elif order_type == 'purchase': - last_ordered_products = self._get_last_purchased_products(partner_id) - - product_ids = list(last_ordered_products.keys()) - - products = self.search_fetch(expression.AND([domain, [('id', 'in', product_ids)], [("name", operator, name)]]), ['display_name'], limit=limit) - limit_rest = limit and limit - len(products) - if limit_rest is None or limit_rest > 0: - products |= self.search_fetch(expression.AND([domain, [('id', 'not in', product_ids)], [("name", operator, name)]]), ['display_name'], limit=limit_rest) - - products = sorted(products, key=lambda p: last_ordered_products.get(p.id, datetime.min), reverse=True) - - return [(product.id, product.display_name, self.env['product.template']._get_time_ago_string(last_ordered_products.get(product.id, False))) for product in products] - - return super().name_search(name, args, operator, limit) diff --git a/last_ordered_products/models/product_template.py b/last_ordered_products/models/product_template.py index 33994c6a108..edd95d8ec69 100644 --- a/last_ordered_products/models/product_template.py +++ b/last_ordered_products/models/product_template.py @@ -1,6 +1,4 @@ -from datetime import datetime from odoo import api, fields, models -from odoo.osv import expression class ProductTemplate(models.Model): @@ -11,7 +9,7 @@ class ProductTemplate(models.Model): @api.depends_context('order_id') def _compute_last_order_time(self): - """Compute the last order time for each product based on the latest sale or purchase.""" + '''Compute the last order time for each product based on the latest sale or purchase.''' partner_id = self.env.context.get('partner_id') order_type = self.env.context.get('order_type') @@ -80,32 +78,3 @@ def _get_time_ago_string(self, last_date): return f"{diff.seconds // 60}m" else: return f"{diff.seconds}s" - - @api.model - def name_search(self, name='', args=None, operator='ilike', limit=100): - '''Modify product dropdown in sale order line to show last sold date''' - - domain = args or [] - partner_id = self.env.context.get('partner_id') - order_type = self.env.context.get('order_type') - active_id = self.env.context.get('active_id') - if not order_type and active_id: - order_type = self.env['account.journal'].browse(active_id).type - - if partner_id: - last_ordered_products = {} - if order_type == 'sale': - last_ordered_products = self._get_last_sold_products(partner_id) - - product_ids = list(last_ordered_products.keys()) - - products = self.search_fetch(expression.AND([domain, [('id', 'in', product_ids)], [("name", operator, name)]]), ['display_name'], limit=limit) - limit_rest = limit and limit - len(products) - if limit_rest is None or limit_rest > 0: - products |= self.search_fetch(expression.AND([domain, [('id', 'not in', product_ids)], [("name", operator, name)]]), ['display_name'], limit=limit_rest) - - products = sorted(products, key=lambda p: p.last_order_time if p.last_order_time else datetime.min, reverse=True) - - return [(product.id, product.display_name, product.last_date_str if product.last_date_str else False) for product in products] - - return super().name_search(name, args, operator, limit) diff --git a/last_ordered_products/static/src/core/autocomplete/autocomplete.xml b/last_ordered_products/static/src/core/autocomplete/autocomplete.xml index 4c08f5790d3..c805f348933 100644 --- a/last_ordered_products/static/src/core/autocomplete/autocomplete.xml +++ b/last_ordered_products/static/src/core/autocomplete/autocomplete.xml @@ -3,9 +3,9 @@ -
+
- +
diff --git a/last_ordered_products/static/src/js/tours/last_ordered_products.js b/last_ordered_products/static/src/js/tours/last_ordered_products.js deleted file mode 100644 index bed9127e84d..00000000000 --- a/last_ordered_products/static/src/js/tours/last_ordered_products.js +++ /dev/null @@ -1,129 +0,0 @@ -/** @odoo-module **/ - -import { _t } from "@web/core/l10n/translation"; -import { registry } from "@web/core/registry"; -import { stepUtils } from "@web_tour/tour_service/tour_utils"; - -registry.category("web_tour.tours").add('last_ordered_products_tour', { - url: "/odoo", - steps: () => [ - stepUtils.showAppsMenuItem(), - { - isActive: ["community"], - trigger: ".o_app[data-menu-xmlid='sale.sale_menu_root']", - content: _t("Lets create a beautiful quotation in a few clicks ."), - tooltipPosition: "right", - run: "click", - }, - { - isActive: ["enterprise"], - trigger: ".o_app[data-menu-xmlid='sale.sale_menu_root']", - content: _t("Let’s create a beautiful quotation in a few clicks ."), - tooltipPosition: "bottom", - run: "click", - }, - { - trigger: "button.o_list_button_add", - content: _t("Build your first quotation right here!"), - tooltipPosition: "bottom", - run: "click", - }, - { - trigger: ".o_field_res_partner_many2one[name='partner_id'] input", - content: _t("Search a customer name ('Azure Interior'"), - tooltipPosition: "right", - run: "edit Azure", - }, - { - trigger: ".o-autocomplete--dropdown-item > a:contains('Azure')", - content: "Select azure interior", - run: "click", - }, - { - trigger: ".o_field_x2many_list_row_add a", - content: _t("Add a product"), - tooltipPosition: "bottom", - run: "click" - }, - { - trigger: ".o_field_widget[name='product_id'], .o_field_widget[name='product_template_id']", - content: _t("Select a product"), - tooltipPosition: "bottom", - run: "click" - }, - { - trigger: ".o_field_sol_product_many2one[name='product_id'] input, .o_field_sol_product_many2one[name='product_id'] input", - content: _t("Search a product (Large Cabinet)'"), - tooltipPosition: "top", - run: "edit Large Cabinet", - }, - { - trigger: ".o-autocomplete--dropdown-item > a:contains('Cabinet')", - content: _t("Select Large Cabinet"), - run: "click", - }, - { - trigger: ".o_form_button_save", - content: _t("Save Manually"), - run: "click", - }, - { - trigger: "button[name=action_confirm]", - content: _t("Confirm Sale Order"), - tooltipPosition: "bottom", - run: "click" - }, - { - trigger: "#create_invoice_percentage", - content: _t("Create Invoice"), - tooltipPosition: "bottom", - run: "click" - }, - { - trigger: "#create_invoice_open", - content: _t("Create Draft"), - tooltipPosition: "bottom", - run: "click" - }, - { - content: "Breadcrumb back to Quotations", - trigger: ".breadcrumb-item:contains('Quotations')", - run: "click", - }, - { - trigger: ".o_list_button_add", - content: _t("Create New Sale Order"), - tooltipPosition: "bottom", - run: "click" - }, - { - trigger: ".o_field_res_partner_many2one[name='partner_id'] input", - content: _t("Search a customer name ('Azure Interior'"), - tooltipPosition: "right", - run: "edit Azure", - }, - { - trigger: ".o-autocomplete--dropdown-item > a:contains('Azure')", - content: "Select azure interior", - run: "click", - }, - { - trigger: ".o_field_x2many_list_row_add a", - content: _t("Add a product"), - tooltipPosition: "bottom", - run: "click" - }, - { - trigger: ".o_field_widget[name='product_id'], .o_field_widget[name='product_template_id']", - content: _t("Select a product"), - tooltipPosition: "bottom", - run: "click" - }, - { - trigger: "div[name='product_id'] .o-autocomplete--dropdown-item > a:contains('[E-COM07]')", - content: _t("You can see here Product Large Cabinet which is invoiced few time ago to this customer"), - tooltipPosition: "right", - run: "click", - }, - ] -}); diff --git a/last_ordered_products/static/src/views/fields/product_label_section_and_note_field.js b/last_ordered_products/static/src/views/fields/product_label_section_and_note_field.js index 9effd3d0dc5..5b4d472011c 100644 --- a/last_ordered_products/static/src/views/fields/product_label_section_and_note_field.js +++ b/last_ordered_products/static/src/views/fields/product_label_section_and_note_field.js @@ -5,6 +5,9 @@ patch(ProductLabelSectionAndNoteFieldAutocomplete.prototype, { mapRecordToOption(result) { let res = super.mapRecordToOption(result) let time_str = result[2] ? result[2] : "" + if (time_str === ""){ + return res; + } res['time_str'] = time_str return res }, diff --git a/last_ordered_products/tests/test_last_ordered_products.py b/last_ordered_products/tests/test_last_ordered_products.py index 435eb547b8f..f5082a9597d 100644 --- a/last_ordered_products/tests/test_last_ordered_products.py +++ b/last_ordered_products/tests/test_last_ordered_products.py @@ -146,7 +146,7 @@ def test_product_variant_in_sale_order(self): res_ids = [r[0] for r in res] self.assertEqual(self.test_product_1.id, res_ids[0]) self.assertEqual(self.test_product_2.id, res_ids[1]) - + def test_product_variant_in_purchase_order(self): po_context = { 'partner_id': self.test_partner_1.id, diff --git a/last_ordered_products/views/product_views.xml b/last_ordered_products/views/product_views.xml index e33bbcc6b35..004dccaf830 100644 --- a/last_ordered_products/views/product_views.xml +++ b/last_ordered_products/views/product_views.xml @@ -10,23 +10,8 @@
- - -
- On Hand - - - (+) - - - () - -
-
- - ago + ago
diff --git a/last_ordered_products_purchase/__init__.py b/last_ordered_products_purchase/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/last_ordered_products_purchase/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/last_ordered_products_purchase/__manifest__.py b/last_ordered_products_purchase/__manifest__.py new file mode 100644 index 00000000000..50b1567d4f1 --- /dev/null +++ b/last_ordered_products_purchase/__manifest__.py @@ -0,0 +1,16 @@ +{ + 'name': "Last Ordered Products (Purchase)", + 'version': '1.0', + 'depends': ['last_ordered_products', 'purchase'], + 'author': "Parthav Chodvadiya (PPCH)", + 'category': '', + 'description': """ + Show last ordered products for vendors in purchase order + """, + 'data': [ + 'views/purchase_order_form.xml', + 'views/account_move_form.xml', + ], + 'license': 'LGPL-3', + 'auto_install': True, +} diff --git a/last_ordered_products_purchase/models/__init__.py b/last_ordered_products_purchase/models/__init__.py new file mode 100644 index 00000000000..5c74c8c30f1 --- /dev/null +++ b/last_ordered_products_purchase/models/__init__.py @@ -0,0 +1 @@ +from . import product_product diff --git a/last_ordered_products_purchase/models/product_product.py b/last_ordered_products_purchase/models/product_product.py new file mode 100644 index 00000000000..028c78d0372 --- /dev/null +++ b/last_ordered_products_purchase/models/product_product.py @@ -0,0 +1,38 @@ +from datetime import datetime +from odoo import api, models +from odoo.osv import expression + + +class ProductProduct(models.Model): + _inherit = 'product.product' + + @api.model + def name_search(self, name='', args=None, operator='ilike', limit=100): + '''Modify product dropdown in sale order line to show last sold date''' + + domain = args or [] + partner_id = self.env.context.get('partner_id') + order_type = self.env.context.get('order_type') + active_id = self.env.context.get('active_id') + if not order_type and active_id: + order_type = self.env['account.journal'].browse(active_id).type + + if partner_id: + last_ordered_products = {} + # if order_type == 'sale': + # last_ordered_products = self._get_last_sold_products(partner_id) + if order_type == 'purchase': + last_ordered_products = self._get_last_purchased_products(partner_id) + + product_ids = list(last_ordered_products.keys()) + + products = self.search_fetch(expression.AND([domain, [('id', 'in', product_ids)], [("name", operator, name)]]), ['display_name'], limit=limit) + limit_rest = limit and limit - len(products) + if limit_rest is None or limit_rest > 0: + products |= self.search_fetch(expression.AND([domain, [('id', 'not in', product_ids)], [("name", operator, name)]]), ['display_name'], limit=limit_rest) + + products = sorted(products, key=lambda p: last_ordered_products.get(p.id, datetime.min), reverse=True) + + return [(product.id, product.display_name, self.env['product.template']._get_time_ago_string(last_ordered_products.get(product.id, False))) for product in products] + + return super().name_search(name, args, operator, limit) diff --git a/last_ordered_products/views/account_move_form.xml b/last_ordered_products_purchase/views/account_move_form.xml similarity index 100% rename from last_ordered_products/views/account_move_form.xml rename to last_ordered_products_purchase/views/account_move_form.xml diff --git a/last_ordered_products/views/purchase_order_form.xml b/last_ordered_products_purchase/views/purchase_order_form.xml similarity index 100% rename from last_ordered_products/views/purchase_order_form.xml rename to last_ordered_products_purchase/views/purchase_order_form.xml diff --git a/last_ordered_products_sale/__init__.py b/last_ordered_products_sale/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/last_ordered_products_sale/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/last_ordered_products_sale/__manifest__.py b/last_ordered_products_sale/__manifest__.py new file mode 100644 index 00000000000..f91aede868f --- /dev/null +++ b/last_ordered_products_sale/__manifest__.py @@ -0,0 +1,16 @@ +{ + 'name': "Last Ordered Products (Sale)", + 'version': '1.0', + 'depends': ['last_ordered_products', 'sale_management'], + 'author': "Parthav Chodvadiya (PPCH)", + 'category': '', + 'description': """ + Show last ordered products for customers in sale order + """, + 'data': [ + 'views/sale_order_form.xml', + 'views/account_move_form.xml', + ], + 'license': 'LGPL-3', + 'auto_install': True, +} diff --git a/last_ordered_products_sale/models/__init__.py b/last_ordered_products_sale/models/__init__.py new file mode 100644 index 00000000000..f2f1aeb80ce --- /dev/null +++ b/last_ordered_products_sale/models/__init__.py @@ -0,0 +1,4 @@ +from . import product_product +from . import product_template +from . import sale_order +from . import sale_order_line diff --git a/last_ordered_products_sale/models/product_product.py b/last_ordered_products_sale/models/product_product.py new file mode 100644 index 00000000000..5fb375c0888 --- /dev/null +++ b/last_ordered_products_sale/models/product_product.py @@ -0,0 +1,36 @@ +from datetime import datetime +from odoo import api, models +from odoo.osv import expression + + +class ProductProduct(models.Model): + _inherit = 'product.product' + + @api.model + def name_search(self, name='', args=None, operator='ilike', limit=100): + '''Modify product dropdown in sale order line to show last sold date''' + + domain = args or [] + partner_id = self.env.context.get('partner_id') + order_type = self.env.context.get('order_type') + active_id = self.env.context.get('active_id') + if not order_type and active_id: + order_type = self.env['account.journal'].browse(active_id).type + + if partner_id: + last_ordered_products = {} + if order_type == 'sale': + last_ordered_products = self._get_last_sold_products(partner_id) + + product_ids = list(last_ordered_products.keys()) + + products = self.search_fetch(expression.AND([domain, [('id', 'in', product_ids)], [("name", operator, name)]]), ['display_name'], limit=limit) + limit_rest = limit and limit - len(products) + if limit_rest is None or limit_rest > 0: + products |= self.search_fetch(expression.AND([domain, [('id', 'not in', product_ids)], [("name", operator, name)]]), ['display_name'], limit=limit_rest) + + products = sorted(products, key=lambda p: last_ordered_products.get(p.id, datetime.min), reverse=True) + + return [(product.id, product.display_name, self.env['product.template']._get_time_ago_string(last_ordered_products.get(product.id, False))) for product in products] + + return super().name_search(name, args, operator, limit) diff --git a/last_ordered_products_sale/models/product_template.py b/last_ordered_products_sale/models/product_template.py new file mode 100644 index 00000000000..45e947ced9a --- /dev/null +++ b/last_ordered_products_sale/models/product_template.py @@ -0,0 +1,35 @@ +from datetime import datetime +from odoo import api, models +from odoo.osv import expression + + +class ProductTemplate(models.Model): + _inherit = 'product.template' + + @api.model + def name_search(self, name='', args=None, operator='ilike', limit=100): + '''Modify product dropdown in sale order line to show last sold date''' + + domain = args or [] + partner_id = self.env.context.get('partner_id') + order_type = self.env.context.get('order_type') + active_id = self.env.context.get('active_id') + if not order_type and active_id: + order_type = self.env['account.journal'].browse(active_id).type + + if partner_id: + last_ordered_products = {} + if order_type == 'sale': + last_ordered_products = self._get_last_sold_products(partner_id) + + product_ids = list(last_ordered_products.keys()) + + products = self.search_fetch(expression.AND([domain, [('id', 'in', product_ids)], [("name", operator, name)]]), ['display_name'], limit=limit) + limit_rest = limit and limit - len(products) + if limit_rest is None or limit_rest > 0: + products |= self.search_fetch(expression.AND([domain, [('id', 'not in', product_ids)], [("name", operator, name)]]), ['display_name'], limit=limit_rest) + products = sorted(products, key=lambda p: last_ordered_products.get(p.id, datetime.min), reverse=True) + + return [(product.id, product.display_name, self.env['product.template']._get_time_ago_string(last_ordered_products.get(product.id, False))) for product in products] + + return super().name_search(name, args, operator, limit) diff --git a/last_ordered_products/models/sale_order.py b/last_ordered_products_sale/models/sale_order.py similarity index 100% rename from last_ordered_products/models/sale_order.py rename to last_ordered_products_sale/models/sale_order.py diff --git a/last_ordered_products/models/sale_order_line.py b/last_ordered_products_sale/models/sale_order_line.py similarity index 100% rename from last_ordered_products/models/sale_order_line.py rename to last_ordered_products_sale/models/sale_order_line.py diff --git a/last_ordered_products_sale/views/account_move_form.xml b/last_ordered_products_sale/views/account_move_form.xml new file mode 100644 index 00000000000..1a2e0221af2 --- /dev/null +++ b/last_ordered_products_sale/views/account_move_form.xml @@ -0,0 +1,15 @@ + + + + account.move.view.last.product.sold + account.move + + + + { + 'partner_id': parent.partner_id, + } + + + + diff --git a/last_ordered_products/views/sale_order_form.xml b/last_ordered_products_sale/views/sale_order_form.xml similarity index 100% rename from last_ordered_products/views/sale_order_form.xml rename to last_ordered_products_sale/views/sale_order_form.xml diff --git a/last_ordered_products_stock/__manifest__.py b/last_ordered_products_stock/__manifest__.py new file mode 100644 index 00000000000..7b431dc2149 --- /dev/null +++ b/last_ordered_products_stock/__manifest__.py @@ -0,0 +1,15 @@ +{ + 'name': "Last Ordered Products (Stock)", + 'version': '1.0', + 'depends': ['last_ordered_products', 'stock', 'sale_management', 'purchase'], + 'author': "Parthav Chodvadiya (PPCH)", + 'category': '', + 'description': """ + Show On hand quantity in product catalog with + and - forecasted quantity + """, + 'data': [ + 'views/product_views.xml', + ], + 'license': 'LGPL-3', + 'auto_install': True, +} diff --git a/last_ordered_products_stock/views/product_views.xml b/last_ordered_products_stock/views/product_views.xml new file mode 100644 index 00000000000..8860eb9c224 --- /dev/null +++ b/last_ordered_products_stock/views/product_views.xml @@ -0,0 +1,23 @@ + + + + product.view.kanban.catalog.last.order.products + product.product + + + +
+ On Hand + + + (+) + + + () + +
+
+
+
+