diff --git a/deposite_in_rental/__init__.py b/deposite_in_rental/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/deposite_in_rental/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/deposite_in_rental/__manifest__.py b/deposite_in_rental/__manifest__.py new file mode 100644 index 00000000000..e698c7ac680 --- /dev/null +++ b/deposite_in_rental/__manifest__.py @@ -0,0 +1,11 @@ +{ + "name": "deposite_in_rental", + "depends": ["base", "account_invoice_extract", "sale_renting", "website_sale"], + "data": [ + "views/res_config_setting_rental_view.xml", + "views/product_template_inherit_view.xml", + "views/website_rental_inherit.xml", + "views/sale_order_line_inherit_view.xml", + ], + "license": "LGPL-3", +} diff --git a/deposite_in_rental/models/__init__.py b/deposite_in_rental/models/__init__.py new file mode 100644 index 00000000000..dad5c481004 --- /dev/null +++ b/deposite_in_rental/models/__init__.py @@ -0,0 +1,3 @@ +from . import res_config_settings_inherit +from . import product_template_inherit +from . import sale_order_line_rental_inherit diff --git a/deposite_in_rental/models/product_template_inherit.py b/deposite_in_rental/models/product_template_inherit.py new file mode 100644 index 00000000000..06dfecf1565 --- /dev/null +++ b/deposite_in_rental/models/product_template_inherit.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class ProductTemplateInherit(models.Model): + _inherit = "product.template" + + require_deposite = fields.Boolean(string="Requires Deposite?") + deposite_amount = fields.Monetary(string="Deposite Amount") diff --git a/deposite_in_rental/models/res_config_settings_inherit.py b/deposite_in_rental/models/res_config_settings_inherit.py new file mode 100644 index 00000000000..d8bb32374cd --- /dev/null +++ b/deposite_in_rental/models/res_config_settings_inherit.py @@ -0,0 +1,21 @@ +from odoo import api, fields, models + + +class ResConfigSettingInherit(models.TransientModel): + _inherit = "res.config.settings" + + rental_product_id = fields.Many2one( + "product.product", + string="Deposite Product", + inverse="_set_rental_product", + compute="_get_rental_product", + ) + + def _set_rental_product(self): + self.env["ir.config_parameter"].set_param( + "rental.deposite_product", self.rental_product_id.id or "" + ) + + @api.depends() + def _get_rental_product(self): + return int(self.env["ir.config_parameter"].get_param("rental.deposite_product")) diff --git a/deposite_in_rental/models/sale_order_line_rental_inherit.py b/deposite_in_rental/models/sale_order_line_rental_inherit.py new file mode 100644 index 00000000000..f196624bcf5 --- /dev/null +++ b/deposite_in_rental/models/sale_order_line_rental_inherit.py @@ -0,0 +1,44 @@ +from odoo import api, models, fields + + +class SaleOrderLineRentalInherit(models.Model): + _inherit = "sale.order.line" + main_rental_id = fields.Many2one("sale.order.line", ondelete="cascade") + child_rental_id = fields.Many2one("sale.order.line", ondelete="cascade") + + @api.model_create_multi + def create(self, vals_list): + res = super().create(vals_list) + deposit_product_id = ( + self.env["ir.config_parameter"].sudo().get_param("rental.deposite_product") + ) + for line in res: + if ( + deposit_product_id + and line.product_template_id.require_deposite + and line.product_template_id.deposite_amount + ): + child_rental_ol = self.env["sale.order.line"].create( + { + "order_id": line.order_id.id, + "product_id": (int)(deposit_product_id), + "name": f"Deposit for {line.product_id.name}", + "product_uom_qty": line.product_uom_qty, + "price_unit": line.product_template_id.deposite_amount, + "main_rental_id": line.id, + } + ) + line.child_rental_id = child_rental_ol.id + return res + + def write(self, vals): + for line in self: + if line.child_rental_id and "product_uom_qty" in vals: + qty = vals["product_uom_qty"] + line.child_rental_id.write( + { + "product_uom_qty": qty, + "price_unit": line.product_template_id.deposite_amount, + } + ) + return super().write(vals) diff --git a/deposite_in_rental/views/product_template_inherit_view.xml b/deposite_in_rental/views/product_template_inherit_view.xml new file mode 100644 index 00000000000..e2a1ccd753b --- /dev/null +++ b/deposite_in_rental/views/product_template_inherit_view.xml @@ -0,0 +1,14 @@ + + + + Product.Template.Rental.Inherit + product.template + + + + + + + + + diff --git a/deposite_in_rental/views/res_config_setting_rental_view.xml b/deposite_in_rental/views/res_config_setting_rental_view.xml new file mode 100644 index 00000000000..3c3d4691c76 --- /dev/null +++ b/deposite_in_rental/views/res_config_setting_rental_view.xml @@ -0,0 +1,16 @@ + + + + Rental config inherit + res.config.settings + + + + + + + + + + + diff --git a/deposite_in_rental/views/sale_order_line_inherit_view.xml b/deposite_in_rental/views/sale_order_line_inherit_view.xml new file mode 100644 index 00000000000..69b031c20b3 --- /dev/null +++ b/deposite_in_rental/views/sale_order_line_inherit_view.xml @@ -0,0 +1,19 @@ + + + + Sale.Order.Inherit.View.Rental + sale.order + + + + main_rental_id + + + main_rental_id + + + main_rental_id + + + + diff --git a/deposite_in_rental/views/website_rental_inherit.xml b/deposite_in_rental/views/website_rental_inherit.xml new file mode 100644 index 00000000000..0275dc67a91 --- /dev/null +++ b/deposite_in_rental/views/website_rental_inherit.xml @@ -0,0 +1,16 @@ + + + + + + Deposite Amount: $ + + + + + + + 'd-none' if line.main_rental_id else ''+('css_quantity input-group mb-2') + + + diff --git a/discount_update/__init__.py b/discount_update/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/discount_update/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/discount_update/__manifest__.py b/discount_update/__manifest__.py new file mode 100644 index 00000000000..2a9443fcc0d --- /dev/null +++ b/discount_update/__manifest__.py @@ -0,0 +1,6 @@ +{ + "name": "Update Discount OL", + "depends": ["base", "sale", "sale_management"], + "data": [], + "license": "LGPL-3", +} diff --git a/discount_update/models/__init__.py b/discount_update/models/__init__.py new file mode 100644 index 00000000000..25087e666a4 --- /dev/null +++ b/discount_update/models/__init__.py @@ -0,0 +1,2 @@ +from . import sale_order_inherit +from . import sale_order_line_inherit diff --git a/discount_update/models/sale_order_inherit.py b/discount_update/models/sale_order_inherit.py new file mode 100644 index 00000000000..f02e7ac1bcd --- /dev/null +++ b/discount_update/models/sale_order_inherit.py @@ -0,0 +1,65 @@ +from collections import defaultdict +from odoo import _, models + + +class SaleOrderInherit(models.Model): + _inherit = "sale.order" + + def recalculate_discount(self): + for order in self: + discount_ol = order.order_line.filtered( + lambda ol: ol.is_global_discount_line + ) + if not discount_ol: + return + + discount_per = discount_ol[0].discount_percentage + discount_product = discount_ol[0].product_id + discount_ol.unlink() + total_price_per_tax_groups = defaultdict(float) + + for line in order.order_line: + if not line.product_uom_qty or not line.price_unit: + continue + + total_price_per_tax_groups[line.tax_id] += ( + line.price_unit * line.product_uom_qty + ) + + if not total_price_per_tax_groups: + return + + elif len(total_price_per_tax_groups) == 1: + taxes = next(iter(total_price_per_tax_groups.keys())) + subtotal = total_price_per_tax_groups[taxes] + self.env["sale.order.line"].create( + { + "order_id": order.id, + "product_id": discount_product.id, + "name": _("Discount: %(percent)s%%", percent=discount_per), + "product_uom_qty": 1, + "tax_id": [(6, 0, taxes.ids)], + "price_unit": -subtotal * discount_per / 100, + } + ) + + else: + vals_list = [ + ( + { + "order_id": order.id, + "product_id": discount_product.id, + "name": _( + "Discount: %(percent)s%%" + "- On products with the following taxes %(taxes)s", + percent=discount_per, + taxes=", ".join(taxes.mapped("name")), + ), + "product_uom_qty": 1, + "tax_id": [(6, 0, taxes.ids)], + "price_unit": -subtotal * discount_per / 100, + } + ) + for taxes, subtotal in total_price_per_tax_groups.items() + ] + self.env["sale.order.line"].create(vals_list) diff --git a/discount_update/models/sale_order_line_inherit.py b/discount_update/models/sale_order_line_inherit.py new file mode 100644 index 00000000000..1804c0c8687 --- /dev/null +++ b/discount_update/models/sale_order_line_inherit.py @@ -0,0 +1,44 @@ +import re +from odoo import api, fields, models + + +class SaleOrderLineInherit(models.Model): + _inherit = "sale.order.line" + + is_global_discount_line = fields.Boolean(default=False) + discount_percentage = fields.Float() + + @api.model_create_multi + def create(self, vals_list): + res = super().create(vals_list) + for line in res: + if "%" in line.name: + match = re.search(r"(\d+(?:\.\d+)?)%", line.name) + if match: + value = float(match.group(1)) + line.discount_percentage = value + line.is_global_discount_line = True + else: + line.order_id.recalculate_discount() + return res + + def write(self, vals): + if self.env.context.get("skip_recalculate_discount"): + return super().write(vals) + + res = super().write(vals) + + for line in self: + line.order_id.with_context( + skip_recalculate_discount=True + ).recalculate_discount() + return res + + def unlink(self): + orders = self.filtered(lambda ol: not ol.is_global_discount_line).mapped( + "order_id" + ) + res = super().unlink() + for order in orders: + order.recalculate_discount() + return res 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 + + + + + + + + + Forecasted + + + + + + + + + Print Labels + + + code + + if records: + action = records.action_open_label_layout() + + + + + Replenish + + + code + + if records: + action = records.action_product_replenish() + + + 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 @@ + + + + + (not line.is_sub_product_ol or (doc.print_in_report and line.is_sub_product_ol)) + + + + + (not line.is_sub_product_ol or (doc.print_in_report and line.is_sub_product_ol)) + + + 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 + + + + + + + + + + is_sub_product_ol + + + is_sub_product_ol + + + is_sub_product_ol + True + + + + + + is_sub_product_ol + + + + + + True + + + + diff --git a/kit_product_type/wizard/__init__.py b/kit_product_type/wizard/__init__.py new file mode 100644 index 00000000000..4d7b396dd8c --- /dev/null +++ b/kit_product_type/wizard/__init__.py @@ -0,0 +1,2 @@ +from . import sale_order_line_wizard +from . import sale_order_line_wizard_lines diff --git a/kit_product_type/wizard/sale_order_line_wizard.py b/kit_product_type/wizard/sale_order_line_wizard.py new file mode 100644 index 00000000000..741d6d7b3fd --- /dev/null +++ b/kit_product_type/wizard/sale_order_line_wizard.py @@ -0,0 +1,88 @@ +from odoo import api, fields, models + + +class SubProducts(models.TransientModel): + _name = "sale.order.line.wizard" + _description = "change.quantity.of.subproducts.in.kit" + + main_product_id = fields.Many2one('sale.order.line', string="Main Order Line", readonly=True) + + line_ids = fields.One2many( + "sale.order.line.wizard.line", "wizard_id", string="Sub Products" + ) + + @api.model + def default_get(self, fields_list): + res = super().default_get(fields_list) + active_id = self.env.context.get("active_id") + main_order_line = self.env["sale.order.line"].browse(active_id) + sale_order = main_order_line.order_id + order_line = self.env["sale.order.line"].browse(active_id) + res["main_product_id"] = main_order_line.id + + if order_line: + lines = [] + for product in order_line.product_template_id.sub_products: + existing_line = self.env["sale.order.line"].search( + [ + ("order_id", "=", sale_order.id), + ("main_order_line_id", "=", main_order_line.id), + ("product_id", "=", product.id), + ], + limit=1, + ) + lines.append( + ( + 0, + 0, + { + "product_id": product.id, + "quantity": existing_line.product_uom_qty if existing_line else 1, + "price": existing_line.price_unit if existing_line else product.list_price, + }, + ) + ) + res["line_ids"] = lines + return res + + def confirm_sub_products(self): + active_id = self.env.context.get("active_id") + main_order_line = self.env["sale.order.line"].browse(active_id) + sale_order = main_order_line.order_id + main_product_subtotal = ( + main_order_line.price_unit * main_order_line.product_uom_qty + ) + for wizard in self: + for line in wizard.line_ids: + if line.quantity > 0: + main_product_subtotal += line.quantity * line.price + + existing_line = self.env["sale.order.line"].search( + [ + ("order_id", "=", sale_order.id), + ("main_order_line_id", "=", main_order_line.id), + ("product_id", "=", line.product_id.id), + ], + limit=1, + ) + + if existing_line: + existing_line.write( + { + "product_uom_qty": line.quantity, + "price_unit": line.price, + } + ) + else: + self.env["sale.order.line"].create( + { + "order_id": sale_order.id, + "product_id": line.product_id.id, + "product_uom_qty": line.quantity, + "name": line.product_id.name, + "is_sub_product_ol": True, + "price_unit": line.price, + "main_order_line_id": main_order_line.id, + } + ) + main_order_line.write({"price_subtotal": main_product_subtotal}) diff --git a/kit_product_type/wizard/sale_order_line_wizard.xml b/kit_product_type/wizard/sale_order_line_wizard.xml new file mode 100644 index 00000000000..2cab606a597 --- /dev/null +++ b/kit_product_type/wizard/sale_order_line_wizard.xml @@ -0,0 +1,30 @@ + + + + Sale order line wizard view form + sale.order.line.wizard + + + + + + + + Sub Products + + + + + + + + + + + + + + diff --git a/kit_product_type/wizard/sale_order_line_wizard_lines.py b/kit_product_type/wizard/sale_order_line_wizard_lines.py new file mode 100644 index 00000000000..dfe60182cac --- /dev/null +++ b/kit_product_type/wizard/sale_order_line_wizard_lines.py @@ -0,0 +1,12 @@ +from odoo import fields, models + + +class SaleOrderLines(models.TransientModel): + _name = "sale.order.line.wizard.line" + _description = "sale.order.lines.for.sub.products" + + wizard_id = fields.Many2one("sale.order.line.wizard") + product_id = fields.Many2one("product.product") + quantity = fields.Float() + wizard_price = fields.Float() + price = fields.Float() diff --git a/pos_salesperson/__init__.py b/pos_salesperson/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/pos_salesperson/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/pos_salesperson/__manifest__.py b/pos_salesperson/__manifest__.py new file mode 100644 index 00000000000..cc0ac3b682f --- /dev/null +++ b/pos_salesperson/__manifest__.py @@ -0,0 +1,11 @@ +{ + "name": "pos_salesperson", + "depends": ["base", "hr", "point_of_sale"], + "data": ["views/pos_order_view_inherit.xml"], + "assets": { + "point_of_sale._assets_pos": [ + "pos_salesperson/static/src/app/**/*", + ], + }, + "license": "LGPL-3", +} diff --git a/pos_salesperson/models/__init__.py b/pos_salesperson/models/__init__.py new file mode 100644 index 00000000000..b2f4b5e054e --- /dev/null +++ b/pos_salesperson/models/__init__.py @@ -0,0 +1,2 @@ +from . import pos_order +from . import pos_session diff --git a/pos_salesperson/models/pos_order.py b/pos_salesperson/models/pos_order.py new file mode 100644 index 00000000000..514da2718bc --- /dev/null +++ b/pos_salesperson/models/pos_order.py @@ -0,0 +1,7 @@ +from odoo import fields, models + + +class PosOrderInherit(models.Model): + _inherit = "pos.order" + + salesperson_id = fields.Many2one("hr.employee", string="SalesPerson") diff --git a/pos_salesperson/models/pos_session.py b/pos_salesperson/models/pos_session.py new file mode 100644 index 00000000000..3858763a7f7 --- /dev/null +++ b/pos_salesperson/models/pos_session.py @@ -0,0 +1,11 @@ +from odoo import api, models + + +class PosSession(models.Model): + _inherit = "pos.session" + + @api.model + def _load_pos_data_models(self, config_id): + data = super()._load_pos_data_models(config_id) + data += ["hr.employee"] + return data diff --git a/pos_salesperson/static/src/app/SalesPersonLine/SalesPersonLine.js b/pos_salesperson/static/src/app/SalesPersonLine/SalesPersonLine.js new file mode 100644 index 00000000000..d6d97e5a5f1 --- /dev/null +++ b/pos_salesperson/static/src/app/SalesPersonLine/SalesPersonLine.js @@ -0,0 +1,19 @@ +import { Component } from "@odoo/owl"; +import { useService } from "@web/core/utils/hooks"; +import { Dropdown } from "@web/core/dropdown/dropdown"; + +export class SalesPersonLine extends Component { + static template = "pos_salesperson.SalesLine"; + static components = { Dropdown }; + static props = [ + "close", + "salesperson", + "isSelected", + "onClickUnselect", + "onClickSalesPerson", + ]; + + setup() { + this.ui = useService("ui"); + } +} diff --git a/pos_salesperson/static/src/app/SalesPersonLine/SalesPersonLine.xml b/pos_salesperson/static/src/app/SalesPersonLine/SalesPersonLine.xml new file mode 100644 index 00000000000..9239b745dfb --- /dev/null +++ b/pos_salesperson/static/src/app/SalesPersonLine/SalesPersonLine.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + UNSELECT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + Name + Address + Contact + Balance + + + + + + + + + + + Search more + + + + Discard + + + + + + 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 @@ + + + + + + Salesperson + + + 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 + + + + + + + +