From 8e047f23a160eed720487ee35611f230deba5cb1 Mon Sep 17 00:00:00 2001 From: mamg-odoo Date: Wed, 2 Apr 2025 15:00:53 +0530 Subject: [PATCH 1/5] [ADD] inventory_task: Customize Inventory UI and Quantity Management Created inventory_task bridge module for custom inventory changes . Removed Update Quantity button from header. Removed On Hand smart button . Updated Forecasted smart button - Red when virtual_available quantity is negative . - Green when virtual_available quantity is positive . Moved Print Labels and Replenish buttons from the header to the actions cog . Added a new field in product.template to easily update Quantity on Hand - Read-only when multi-location is enabled . - Can be updated using the Update button . --- inventory_task/__init__.py | 1 + inventory_task/__manifest__.py | 7 ++ inventory_task/models/__init__.py | 1 + inventory_task/models/product_template.py | 68 ++++++++++++++++ .../views/product_template_views.xml | 80 +++++++++++++++++++ 5 files changed, 157 insertions(+) create mode 100644 inventory_task/__init__.py create mode 100644 inventory_task/__manifest__.py create mode 100644 inventory_task/models/__init__.py create mode 100644 inventory_task/models/product_template.py create mode 100644 inventory_task/views/product_template_views.xml diff --git a/inventory_task/__init__.py b/inventory_task/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/inventory_task/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/inventory_task/__manifest__.py b/inventory_task/__manifest__.py new file mode 100644 index 00000000000..3f42c42828d --- /dev/null +++ b/inventory_task/__manifest__.py @@ -0,0 +1,7 @@ +{ + "name": "Inventory Task", + "description": "Inventory UI customization module", + "depends": ["base", "product", "stock"], + "data": ["views/product_template_views.xml"], + "license": "LGPL-3", +} diff --git a/inventory_task/models/__init__.py b/inventory_task/models/__init__.py new file mode 100644 index 00000000000..e8fa8f6bf1e --- /dev/null +++ b/inventory_task/models/__init__.py @@ -0,0 +1 @@ +from . import product_template diff --git a/inventory_task/models/product_template.py b/inventory_task/models/product_template.py new file mode 100644 index 00000000000..2ff44c05765 --- /dev/null +++ b/inventory_task/models/product_template.py @@ -0,0 +1,68 @@ +from odoo import api, fields, models + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + qty_input = fields.Float( + "Quantity on Hand", compute="_compute_qty_input", inverse="_inverse_qty_input" + ) + + is_multi_location = fields.Boolean( + compute="_compute_is_multi_location", store=False + ) + + @api.depends("company_id") + def _compute_is_multi_location(self): + for product in self: + product.is_multi_location = self.env.user.has_group( + "stock.group_stock_multi_locations" + ) + + @api.depends("qty_available") + def _compute_qty_input(self): + for record in self: + record.qty_input = record.qty_available + + def _inverse_qty_input(self): + return + + @api.onchange("qty_input") + def _onchange_new_qty(self): + for product in self: + if product.qty_input >= 0: + if not product.product_variant_id: + continue + stock_quant = self.env["stock.quant"].search( + [ + ("product_id", "=", product.product_variant_id.id), + ], + limit=1, + ) + + if stock_quant: + stock_quant.sudo().write({"quantity": product.qty_input}) + else: + warehouse = self.env["stock.warehouse"].search( + [("company_id", "=", self.env.company.id)], limit=1 + ) + self.env["stock.quant"].with_context(inventory_mode=True).create( + { + "product_id": product.product_variant_id.id, + "location_id": warehouse.lot_stock_id.id, + "quantity": product.qty_input, + } + ) + + def action_product_replenish(self): + return { + "name": "Low on stock? Let's replenish.", + "type": "ir.actions.act_window", + "res_model": "product.replenish", + "view_mode": "form", + "view_id": self.env.ref("stock.view_product_replenish").id, + "target": "new", + "context": { + "default_product_tmpl_id": self.id, + }, + } diff --git a/inventory_task/views/product_template_views.xml b/inventory_task/views/product_template_views.xml new file mode 100644 index 00000000000..ab813c31e0c --- /dev/null +++ b/inventory_task/views/product_template_views.xml @@ -0,0 +1,80 @@ + + + + product.template.form.view.inherit + product.template + + + + +
+ + +
+ +
+ + 1 + +
+
+ + + product.form.view.print.label.button + product.template + + + + 1 + + + 1 + + + 1 + + + + + + + + + + Print Labels + + + code + + if records: + action = records.action_open_label_layout() + + + + + Replenish + + + code + + if records: + action = records.action_product_replenish() + + +
From 569d783437590878dea2f12b58d9800dff641290 Mon Sep 17 00:00:00 2001 From: mamg-odoo Date: Mon, 7 Apr 2025 18:40:36 +0530 Subject: [PATCH 2/5] [ADD] kit_product_type: implement kit type product and sale order management MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Introduced a new product type Kit via is_kit Boolean field on product.template. - When is_kit is enabled, a Many2many field appears to select sub-products. - On Sales Orders, kit products now show a Add Sub Products button. - Sub-products can be customized (quantity and price) through a wizard. - Confirming the wizard adds sub-products to the order line with unit price and subtotal as 0. - The cost of sub-products is included in the main kit product’s subtotal. - Added `print_in_report` Boolean on sale.order to control printing sub-products in reports/customer preview. - Once the Sales Order is confirmed, the Add Sub Products button is hidden. --- kit_product_type/__init__.py | 2 + kit_product_type/__manifest__.py | 12 +++ kit_product_type/models/__init__.py | 3 + kit_product_type/models/product_template.py | 8 ++ kit_product_type/models/sale_order.py | 7 ++ kit_product_type/models/sales_order_line.py | 57 ++++++++++++ kit_product_type/security/ir.model.access.csv | 3 + .../views/product_template_view.xml | 14 +++ kit_product_type/views/sale_order_report.xml | 13 +++ kit_product_type/views/sale_order_view.xml | 38 ++++++++ kit_product_type/wizard/__init__.py | 2 + .../wizard/sale_order_line_wizard.py | 88 +++++++++++++++++++ .../wizard/sale_order_line_wizard.xml | 30 +++++++ .../wizard/sale_order_line_wizard_lines.py | 12 +++ 14 files changed, 289 insertions(+) create mode 100644 kit_product_type/__init__.py create mode 100644 kit_product_type/__manifest__.py create mode 100644 kit_product_type/models/__init__.py create mode 100644 kit_product_type/models/product_template.py create mode 100644 kit_product_type/models/sale_order.py create mode 100644 kit_product_type/models/sales_order_line.py create mode 100644 kit_product_type/security/ir.model.access.csv create mode 100644 kit_product_type/views/product_template_view.xml create mode 100644 kit_product_type/views/sale_order_report.xml create mode 100644 kit_product_type/views/sale_order_view.xml create mode 100644 kit_product_type/wizard/__init__.py create mode 100644 kit_product_type/wizard/sale_order_line_wizard.py create mode 100644 kit_product_type/wizard/sale_order_line_wizard.xml create mode 100644 kit_product_type/wizard/sale_order_line_wizard_lines.py diff --git a/kit_product_type/__init__.py b/kit_product_type/__init__.py new file mode 100644 index 00000000000..9b4296142f4 --- /dev/null +++ b/kit_product_type/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizard diff --git a/kit_product_type/__manifest__.py b/kit_product_type/__manifest__.py new file mode 100644 index 00000000000..b7ea4bb828d --- /dev/null +++ b/kit_product_type/__manifest__.py @@ -0,0 +1,12 @@ +{ + "name": "kit_product_type", + "depends": ["sale_management"], + "data": [ + "views/product_template_view.xml", + "views/sale_order_view.xml", + "views/sale_order_report.xml", + "wizard/sale_order_line_wizard.xml", + "security/ir.model.access.csv", + ], + "license": "LGPL-3", +} diff --git a/kit_product_type/models/__init__.py b/kit_product_type/models/__init__.py new file mode 100644 index 00000000000..07b132101f3 --- /dev/null +++ b/kit_product_type/models/__init__.py @@ -0,0 +1,3 @@ +from . import product_template +from . import sale_order +from . import sales_order_line diff --git a/kit_product_type/models/product_template.py b/kit_product_type/models/product_template.py new file mode 100644 index 00000000000..8e983c0a07b --- /dev/null +++ b/kit_product_type/models/product_template.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + is_kit = fields.Boolean(string="Is Kit?") + sub_products = fields.Many2many("product.product", string="Sub Products") diff --git a/kit_product_type/models/sale_order.py b/kit_product_type/models/sale_order.py new file mode 100644 index 00000000000..99b4542f74e --- /dev/null +++ b/kit_product_type/models/sale_order.py @@ -0,0 +1,7 @@ +from odoo import fields, models + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + print_in_report = fields.Boolean(string="Print in Report") diff --git a/kit_product_type/models/sales_order_line.py b/kit_product_type/models/sales_order_line.py new file mode 100644 index 00000000000..1300fdc0375 --- /dev/null +++ b/kit_product_type/models/sales_order_line.py @@ -0,0 +1,57 @@ +from odoo import api, fields, models + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + is_kit = fields.Boolean(related="product_template_id.is_kit") + is_sub_product_ol = fields.Boolean() + main_order_line_id = fields.Many2one( + "sale.order.line", + string="Parent Line", + ondelete="cascade", + ) + + child_line_ids = fields.One2many( + "sale.order.line", + "main_order_line_id", + string="Child Lines", + ) + display_price = fields.Float( + compute="_compute_display_price", inverse="_compute_unit_price" + ) + display_sub_total = fields.Float(compute="_compute_amount_price") + + @api.depends("price_unit", "is_sub_product_ol") + def _compute_display_price(self): + for line in self: + line.display_price = 0.0 if line.is_sub_product_ol else line.price_unit + + @api.depends("display_price", "price_subtotal", "is_sub_product_ol") + def _compute_amount_price(self): + for line in self: + line.display_sub_total = ( + 0.0 if line.is_sub_product_ol else line.price_subtotal + ) + + def _compute_unit_price(self): + for line in self: + line.price_unit = line.display_price + + def unlink(self): + for line in self: + line.main_order_line_id.price_subtotal -= ( + line.product_uom_qty * line.price_unit + ) + + return super().unlink() + + def open_sub_product_wizard(self): + return { + "name": "Sale order line wizard action", + "type": "ir.actions.act_window", + "res_model": "sale.order.line.wizard", + "view_mode": "form", + "target": "new", + "context": {"active_id": self.id}, + } diff --git a/kit_product_type/security/ir.model.access.csv b/kit_product_type/security/ir.model.access.csv new file mode 100644 index 00000000000..0e7bafba301 --- /dev/null +++ b/kit_product_type/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_sale_order_line_wizard,sale.order.line.wizard,model_sale_order_line_wizard,base.group_user,1,1,1,1 +access_sale_order_line_wizard_line,sale.order.line.wizard.line,model_sale_order_line_wizard_line,base.group_user,1,1,1,1 diff --git a/kit_product_type/views/product_template_view.xml b/kit_product_type/views/product_template_view.xml new file mode 100644 index 00000000000..b7e6778bff6 --- /dev/null +++ b/kit_product_type/views/product_template_view.xml @@ -0,0 +1,14 @@ + + + + product.template.view.form.inherit.kit + product.template + + + + + + + + + diff --git a/kit_product_type/views/sale_order_report.xml b/kit_product_type/views/sale_order_report.xml new file mode 100644 index 00000000000..55e86992ab2 --- /dev/null +++ b/kit_product_type/views/sale_order_report.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/kit_product_type/views/sale_order_view.xml b/kit_product_type/views/sale_order_view.xml new file mode 100644 index 00000000000..98ec6f8a5c1 --- /dev/null +++ b/kit_product_type/views/sale_order_view.xml @@ -0,0 +1,38 @@ + + + + Sale.Order.View.Form.Inherit.kit + sale.order + + + + + + + + + + + + + + +
+ + +
+ + +
+ + +
+
+ + +
+ + + + + + + + + + + + + + + + + + diff --git a/pos_salesperson/static/src/app/SalesPersonList/SalesPersonList.js b/pos_salesperson/static/src/app/SalesPersonList/SalesPersonList.js new file mode 100644 index 00000000000..f57bfbb088d --- /dev/null +++ b/pos_salesperson/static/src/app/SalesPersonList/SalesPersonList.js @@ -0,0 +1,59 @@ +import { _t } from "@web/core/l10n/translation"; +import { useService } from "@web/core/utils/hooks"; +import { fuzzyLookup } from "@web/core/utils/search"; +import { Dialog } from "@web/core/dialog/dialog"; +import { usePos } from "@point_of_sale/app/store/pos_hook"; +import { Input } from "@point_of_sale/app/generic_components/inputs/input/input"; +import { Component, useState } from "@odoo/owl"; +import { unaccent } from "@web/core/utils/strings"; +import { SalesPersonLine } from "../SalesPersonLine/SalesPersonLine"; + +export class SalesPersonList extends Component { + static template = "pos_salesperson.SalesList"; + static components = { SalesPersonLine, Dialog, Input }; + static props = { + salesperson: { + optional: true, + type: [{ value: null }, Object], + }, + getPayload: { type: Function }, + close: { type: Function }, + }; + setup() { + this.pos = usePos(); + this.ui = useState(useService("ui")); + // this.dialog = useService("dialog"); + this.state = useState({ + query: null, + }); + } + + getSalesPerson() { + const searchWord = unaccent((this.state.query || "").trim(), false); + const salesperson = this.pos.models["hr.employee"].getAll(); + const exactMatches = salesperson.filter( + (person) => (person.name || "").toLowerCase() === searchWord.toLowerCase() + ); + + if (exactMatches.length > 0) { + return exactMatches; + } + const availableSalesPerson = searchWord + ? fuzzyLookup(searchWord, salesperson, (sale) => + unaccent(sale.searchString || "", false) + ) + : salesperson.slice(0, 100).toSorted((a, b) => { + if (this.props.salesperson && this.props.salesperson.id === a.id) { + return -1; + } + return (a.name || "").localeCompare(b.name || ""); + }); + + return availableSalesPerson; + } + + clickSalesPerson(salesperson) { + this.props.getPayload(salesperson); + this.props.close(); + } +} diff --git a/pos_salesperson/static/src/app/SalesPersonList/SalesPersonList.xml b/pos_salesperson/static/src/app/SalesPersonList/SalesPersonList.xml new file mode 100644 index 00000000000..ef092db35b0 --- /dev/null +++ b/pos_salesperson/static/src/app/SalesPersonList/SalesPersonList.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + +
NameAddressContactBalance
+
+ +
+ +
+ +
+
+
+
+ +
diff --git a/pos_salesperson/static/src/app/control_button/control_button.js b/pos_salesperson/static/src/app/control_button/control_button.js new file mode 100644 index 00000000000..dc1b815772c --- /dev/null +++ b/pos_salesperson/static/src/app/control_button/control_button.js @@ -0,0 +1,10 @@ +import { ControlButtons } from "@point_of_sale/app/screens/product_screen/control_buttons/control_buttons"; +import { SelectSalespersonButton } from "../select_salesperson_button/select_salesperson_button"; +import { patch } from "@web/core/utils/patch"; + +patch(ControlButtons, { + components: { + ...ControlButtons.components, + SelectSalespersonButton, + }, +}); diff --git a/pos_salesperson/static/src/app/control_button/control_button.xml b/pos_salesperson/static/src/app/control_button/control_button.xml new file mode 100644 index 00000000000..24fb5247306 --- /dev/null +++ b/pos_salesperson/static/src/app/control_button/control_button.xml @@ -0,0 +1,8 @@ + + diff --git a/pos_salesperson/static/src/app/model/hr_employee.js b/pos_salesperson/static/src/app/model/hr_employee.js new file mode 100644 index 00000000000..de012011e1b --- /dev/null +++ b/pos_salesperson/static/src/app/model/hr_employee.js @@ -0,0 +1,20 @@ +import { registry } from "@web/core/registry"; +import { Base } from "@point_of_sale/app/models/related_models"; + +export class HrEmployee extends Base { + static pythonModel = "hr.employee"; + + get searchString() { + const fields = ["name"]; + return fields + .map((field) => { + return this[field] || ""; + }) + .filter(Boolean) + .join(" "); + } +} + +registry + .category("pos_available_models") + .add(HrEmployee.pythonModel, HrEmployee); diff --git a/pos_salesperson/static/src/app/model/pos_order.js b/pos_salesperson/static/src/app/model/pos_order.js new file mode 100644 index 00000000000..58c8675938d --- /dev/null +++ b/pos_salesperson/static/src/app/model/pos_order.js @@ -0,0 +1,11 @@ +import { PosOrder } from "@point_of_sale/app/models/pos_order"; +import { patch } from "@web/core/utils/patch"; + +patch(PosOrder.prototype, { + get_salesperson() { + return this.salesperson_id; + }, + set_salesperson(salesperson) { + this.salesperson_id = salesperson; + }, +}); diff --git a/pos_salesperson/static/src/app/override/pos_store.js b/pos_salesperson/static/src/app/override/pos_store.js new file mode 100644 index 00000000000..ed1828877cb --- /dev/null +++ b/pos_salesperson/static/src/app/override/pos_store.js @@ -0,0 +1,26 @@ +import { PosStore } from "@point_of_sale/app/store/pos_store"; +import { patch } from "@web/core/utils/patch"; +import { SalesPersonList } from "../SalesPersonList/SalesPersonList"; +import { makeAwaitable } from "@point_of_sale/app/store/make_awaitable_dialog"; + +patch(PosStore.prototype, { + async selectSalesperson() { + const currentOrder = this.get_order(); + if (!currentOrder) { + return false; + } + const currentSalesperson = currentOrder.get_salesperson(); + const payload = await makeAwaitable(this.dialog, SalesPersonList, { + salesperson: currentSalesperson || null, + getPayload: (newPartner) => currentOrder.set_salesperson(newPartner), + }); + + if (payload) { + currentOrder.set_salesperson(payload); + } else { + currentOrder.set_salesperson(false); + } + + return currentSalesperson; + }, +}); diff --git a/pos_salesperson/static/src/app/select_salesperson_button/select_salesperson_button.js b/pos_salesperson/static/src/app/select_salesperson_button/select_salesperson_button.js new file mode 100644 index 00000000000..fe1f58a8ab1 --- /dev/null +++ b/pos_salesperson/static/src/app/select_salesperson_button/select_salesperson_button.js @@ -0,0 +1,9 @@ +import { Component, useState } from "@odoo/owl"; +import { usePos } from "@point_of_sale/app/store/pos_hook"; + +export class SelectSalespersonButton extends Component { + static template = "pos_salesperson.SelectSalespersonButton"; + setup() { + this.pos = usePos(); + } +} diff --git a/pos_salesperson/static/src/app/select_salesperson_button/select_salesperson_button.xml b/pos_salesperson/static/src/app/select_salesperson_button/select_salesperson_button.xml new file mode 100644 index 00000000000..bf9ac466744 --- /dev/null +++ b/pos_salesperson/static/src/app/select_salesperson_button/select_salesperson_button.xml @@ -0,0 +1,9 @@ + + diff --git a/pos_salesperson/views/pos_order_view_inherit.xml b/pos_salesperson/views/pos_order_view_inherit.xml new file mode 100644 index 00000000000..e891eff2169 --- /dev/null +++ b/pos_salesperson/views/pos_order_view_inherit.xml @@ -0,0 +1,23 @@ + + + + Salesperson Pos list view + pos.order + + + + + + + + + Salesperson Pos form view + pos.order + + + + + + + +