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..feefe3153eb --- /dev/null +++ b/last_ordered_products/__manifest__.py @@ -0,0 +1,24 @@ +{ + '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': [ + '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': { + 'web.assets_backend': [ + 'last_ordered_products/static/src/**/*.js', + 'last_ordered_products/static/src/**/*.xml', + ], + }, + 'license': 'LGPL-3', +} 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/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..6572592630d --- /dev/null +++ b/last_ordered_products/models/product_product.py @@ -0,0 +1,132 @@ +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: + for record in self: + record.last_order_time = False + record.last_date_str = False + return + + 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.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''' + + sale_order_lines = self.env['sale.order.line'].search([ + ('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_order_lines.order_id.mapped('name')) + ]) + + last_sale_ordered_products = {} + 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 + 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_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.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 + + @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 new file mode 100644 index 00000000000..33994c6a108 --- /dev/null +++ b/last_ordered_products/models/product_template.py @@ -0,0 +1,111 @@ +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.""" + + partner_id = self.env.context.get('partner_id') + order_type = self.env.context.get('order_type') + + if not partner_id: + for record in self: + record.last_order_time = False + record.last_date_str = False + return + + last_ordered_products = {} + + if order_type == 'sale': + last_ordered_products = self._get_last_sold_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(last_date) if last_date else False + + def _get_last_sold_products(self, partner_id): + '''Fetch products last sold to the given customer''' + + sale_order_lines = self.env['sale.order.line'].search([ + ('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_order_lines.order_id.mapped('name')) + ]) + + last_sale_ordered_products = {} + 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 + 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_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 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/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..dec3985a981 --- /dev/null +++ b/last_ordered_products/models/sale_order_line.py @@ -0,0 +1,19 @@ +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['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 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..4c08f5790d3 --- /dev/null +++ b/last_ordered_products/static/src/core/autocomplete/autocomplete.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + 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_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/kanban_record.js b/last_ordered_products/static/src/product_catalog/kanban_record.js new file mode 100644 index 00000000000..f1aa9de256c --- /dev/null +++ b/last_ordered_products/static/src/product_catalog/kanban_record.js @@ -0,0 +1,12 @@ +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, { + 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..2647e0847eb --- /dev/null +++ b/last_ordered_products/static/src/product_catalog/order_line/order_line.js @@ -0,0 +1,10 @@ +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/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]) 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..e33bbcc6b35 --- /dev/null +++ b/last_ordered_products/views/product_views.xml @@ -0,0 +1,34 @@ + + + + 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' + } + + + +