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..e797a555a30 --- /dev/null +++ b/pos_salesperson/__manifest__.py @@ -0,0 +1,17 @@ +{ + 'name': 'Pos Salesperson', + 'version': '1.0', + 'category': 'Sales/Point of Sale', + 'summary': 'Allow selecting salesperson for pos order', + 'depends': ['point_of_sale', 'hr'], + 'installable': True, + 'data': [ + 'views/pos_order_view.xml' + ], + 'assets': { + 'point_of_sale._assets_pos': [ + 'pos_salesperson/static/src/**/*', + ], + }, + '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..a6ef29e21f8 --- /dev/null +++ b/pos_salesperson/models/pos_order.py @@ -0,0 +1,6 @@ +from odoo import models, fields + + +class InheritedPosOrder(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..611c7929c13 --- /dev/null +++ b/pos_salesperson/models/pos_session.py @@ -0,0 +1,11 @@ +from odoo import models, api + + +class InheritedPosSession(models.Model): + _inherit = "pos.session" + + @api.model + def _load_pos_data_models(self, config_id): + data_models = super()._load_pos_data_models(self.config_id.id) + data_models.append('hr.employee') + return data_models diff --git a/pos_salesperson/static/src/models/hr_employee.js b/pos_salesperson/static/src/models/hr_employee.js new file mode 100644 index 00000000000..12ee58bbc03 --- /dev/null +++ b/pos_salesperson/static/src/models/hr_employee.js @@ -0,0 +1,19 @@ +import { Base } from "@point_of_sale/app/models/related_models"; +import { registry } from "@web/core/registry"; + +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/overrides/control_button.js b/pos_salesperson/static/src/overrides/control_button.js new file mode 100644 index 00000000000..0a035b1387e --- /dev/null +++ b/pos_salesperson/static/src/overrides/control_button.js @@ -0,0 +1,7 @@ +import { patch } from "@web/core/utils/patch"; +import { SalespersonButton } from "../salesperson_button/salesperson_button"; +import { ControlButtons } from "@point_of_sale/app/screens/product_screen/control_buttons/control_buttons"; + +patch(ControlButtons.components, { + SalespersonButton +}) diff --git a/pos_salesperson/static/src/overrides/control_buttons.xml b/pos_salesperson/static/src/overrides/control_buttons.xml new file mode 100644 index 00000000000..c87bd1fcf4c --- /dev/null +++ b/pos_salesperson/static/src/overrides/control_buttons.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/pos_salesperson/static/src/overrides/pos_order.js b/pos_salesperson/static/src/overrides/pos_order.js new file mode 100644 index 00000000000..d748b7d1fbb --- /dev/null +++ b/pos_salesperson/static/src/overrides/pos_order.js @@ -0,0 +1,13 @@ +import { patch } from "@web/core/utils/patch"; +import { PosOrder } from "@point_of_sale/app/models/pos_order"; + +patch(PosOrder.prototype, { + set_salesperson(salesperson) { + this.assert_editable(); + this.update({ salesperson_id: salesperson }) + }, + + get_salesperson() { + return this.salesperson_id; + } +}) diff --git a/pos_salesperson/static/src/overrides/pos_store.js b/pos_salesperson/static/src/overrides/pos_store.js new file mode 100644 index 00000000000..32761af9862 --- /dev/null +++ b/pos_salesperson/static/src/overrides/pos_store.js @@ -0,0 +1,26 @@ +import { makeAwaitable } from "@point_of_sale/app/store/make_awaitable_dialog"; +import { PosStore } from "@point_of_sale/app/store/pos_store"; +import { patch } from "@web/core/utils/patch"; +import { SalespersonList } from "../salesperson_list/salesperson_list"; + +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, + getPayload: (newSalesperson) => { + currentOrder.set_salesperson(newSalesperson) + }, + }); + if (payload) { + currentOrder.set_salesperson(payload); + } else { + currentOrder.set_salesperson(false); + } + return currentSalesperson; + } +}) diff --git a/pos_salesperson/static/src/salesperson_button/salesperson_button.js b/pos_salesperson/static/src/salesperson_button/salesperson_button.js new file mode 100644 index 00000000000..14d02e3234d --- /dev/null +++ b/pos_salesperson/static/src/salesperson_button/salesperson_button.js @@ -0,0 +1,13 @@ +import { Component, markRaw } from "@odoo/owl"; +import { usePos } from "@point_of_sale/app/store/pos_hook"; + +export class SalespersonButton extends Component { + static template = 'pos_salesperson.SelectSalespersonButton'; + setup() { + this.pos = usePos(); + } + + get salesperson() { + return this.pos.get_order()?.get_salesperson(); + } +} diff --git a/pos_salesperson/static/src/salesperson_button/salesperson_button.xml b/pos_salesperson/static/src/salesperson_button/salesperson_button.xml new file mode 100644 index 00000000000..2363e72899c --- /dev/null +++ b/pos_salesperson/static/src/salesperson_button/salesperson_button.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/pos_salesperson/static/src/salesperson_list/sales_person_list.scss b/pos_salesperson/static/src/salesperson_list/sales_person_list.scss new file mode 100644 index 00000000000..3815d7029f9 --- /dev/null +++ b/pos_salesperson/static/src/salesperson_list/sales_person_list.scss @@ -0,0 +1,59 @@ +.salesperson-list { + tr { + border: $border-width solid $border-color; + + &:hover { + cursor: pointer; + } + + &:active { + background-color: $o-component-active-bg; + } + } +} + +.salesperson-list table { + border-collapse: separate; + border-spacing: 0 8px; + margin-top: -8px; + + td { + border: $border-width solid $border-color; + border-style: solid none; + padding: map-get($spacers, 3); + } + + tr.selected td { + border-color: $o-component-active-border; + } + + td:first-child { + border-left-style: solid; + border-top-left-radius: $border-radius-lg; + border-bottom-left-radius: $border-radius-lg; + } + + td:last-child { + border-right-style: solid; + border-bottom-right-radius: $border-radius-lg; + border-top-right-radius: $border-radius-lg; + } +} + +@include media-breakpoint-down(lg) { + .salesperson-list { + table { + border: transparent; + } + + .salesperson-list { + thead { + display: none; + } + + td { + padding: 0; + } + } + } +} \ No newline at end of file diff --git a/pos_salesperson/static/src/salesperson_list/salesperson_line/salesperson_line.js b/pos_salesperson/static/src/salesperson_list/salesperson_line/salesperson_line.js new file mode 100644 index 00000000000..2a17f5225f3 --- /dev/null +++ b/pos_salesperson/static/src/salesperson_list/salesperson_line/salesperson_line.js @@ -0,0 +1,17 @@ +import { Component } from "@odoo/owl"; +import { useService } from "@web/core/utils/hooks"; + +export class SalespersonLine extends Component { + static template = "pos_salesperson.SalespersonLine"; + static props = [ + "close", + "salesperson", + "isSelected", + "onClickUnselect", + "onClickSalesperson", + ]; + + setup() { + this.ui = useService("ui"); + } +} diff --git a/pos_salesperson/static/src/salesperson_list/salesperson_line/salesperson_line.scss b/pos_salesperson/static/src/salesperson_list/salesperson_line/salesperson_line.scss new file mode 100644 index 00000000000..84b89345143 --- /dev/null +++ b/pos_salesperson/static/src/salesperson_list/salesperson_line/salesperson_line.scss @@ -0,0 +1,47 @@ +@include media-breakpoint-down(lg) { + + .salesperson-line { + display: flex; + flex-wrap: wrap; + align-items: center; + padding: 12px 24px; + border-bottom: 1px solid $o-gray-300; + + td { + border: 0; + flex-basis: 100%; + padding: 5px; + background-color: transparent; + } + } + + .salesperson-line .pos-right-align { + text-align: left; + } + + .edit-salesperson-button-cell { + order: 4; + } +} + +.salesperson-line { + &:hover i.oi-chevron-right { + transition: $transition-base; + transform: translateX(.5rem); + color: $o-main-link-color; + } + + &.selected { + background-color: $o-component-active-bg; + border: $border-width solid $o-component-active-border; + } + + &.selected .btn.btn-link:hover .fa-check { + display: none; + } + + &.selected .btn.btn-link:hover::after { + content: "\e852"; + font-family: 'odoo_ui_icons'; + } +} \ No newline at end of file diff --git a/pos_salesperson/static/src/salesperson_list/salesperson_line/salesperson_line.xml b/pos_salesperson/static/src/salesperson_list/salesperson_line/salesperson_line.xml new file mode 100644 index 00000000000..d22f395145b --- /dev/null +++ b/pos_salesperson/static/src/salesperson_list/salesperson_line/salesperson_line.xml @@ -0,0 +1,48 @@ + + + + + +
+
+
+ +
+
+
+
+ +
+
+ +
+
+ + + + + +
+ + + + + + + + + + + + diff --git a/pos_salesperson/static/src/salesperson_list/salesperson_list.js b/pos_salesperson/static/src/salesperson_list/salesperson_list.js new file mode 100644 index 00000000000..65bd04a79ba --- /dev/null +++ b/pos_salesperson/static/src/salesperson_list/salesperson_list.js @@ -0,0 +1,112 @@ +import { Component, markRaw, useState } from "@odoo/owl"; +import { Input } from "@point_of_sale/app/generic_components/inputs/input/input"; +import { usePos } from "@point_of_sale/app/store/pos_hook"; +import { Dialog } from "@web/core/dialog/dialog"; +import { useHotkey } from "@web/core/hotkeys/hotkey_hook"; +import { useService } from "@web/core/utils/hooks"; +import { unaccent } from "@web/core/utils/strings"; +import { SalespersonLine } from "./salesperson_line/salesperson_line"; + +export class SalespersonList extends Component { + static components = { Dialog, Input, SalespersonLine }; + static template = "pos_salesperson.SalespersonList"; + + static props = { + salesperson: { + type: [{ value: null }, Object], + optional: true, + }, + getPayload: { type: Function }, + close: { type: Function }, + }; + + setup() { + this.pos = usePos(); + this.ui = useState(useService("ui")); + this.notification = useService("notification"); + this.state = useState({ + query: null, + previousQuery: "", + currentOffset: 0, + }); + useHotkey("enter", () => this.onEnter()); + } + + async onEnter() { + if (!this.state.query) { + return; + } + const result = await this.searchSalesperson(); + if (result.length > 0) { + this.notification.add( + `${result.length} salesperson(s) found for "${this.state.query}".`, + 3000 + ); + } else { + this.notification.add(`No more salesperson found for "${this.state.query}"`); + } + } + + clickSalesperson(salesperson) { + this.props.getPayload(salesperson); + this.props.close(); + } + + getSalespersons() { + const searchWord = unaccent((this.state.query || "").trim(), false).toLowerCase(); + const salespersons = this.pos.models["hr.employee"].getAll(); + const numberString = searchWord.replace(/[+\s()-]/g, ""); + const isSearchWordNumber = /^[0-9]+$/.test(numberString); + + const availableSalespersons = searchWord + ? salespersons.filter((p) => + unaccent(p.searchString, false).includes(isSearchWordNumber ? numberString : searchWord) + ) + : salespersons + .slice(0, 1000) + .toSorted((a, b) => + this.props.salesperson?.id === a.id + ? -1 + : this.props.salesperson?.id === b.id + ? 1 + : (a.name || "").localeCompare(b.name || "") + ); + return availableSalespersons; + } + + async searchSalesperson() { + if (this.state.previousQuery != this.state.query) { + this.state.currentOffset = 0; + } + const salesperson = await this.getNewSalespersons(); + + if (this.state.previousQuery == this.state.query) { + this.state.currentOffset += salesperson.length; + } else { + this.state.previousQuery = this.state.query; + this.state.currentOffset = salesperson.length; + } + return salesperson; + } + + async getNewSalespersons() { + let domain = []; + const limit = 30; + if (this.state.query) { + const search_fields = [ + "name", + ]; + domain = [ + ...Array(search_fields.length - 1).fill("|"), + ...search_fields.map((field) => [field, "ilike", this.state.query + "%"]), + ]; + } + + const result = await this.pos.data.searchRead("hr.employee", domain, [], { + limit: limit, + offset: this.state.currentOffset, + }); + + return result; + } +} diff --git a/pos_salesperson/static/src/salesperson_list/salesperson_list.xml b/pos_salesperson/static/src/salesperson_list/salesperson_list.xml new file mode 100644 index 00000000000..c74c3c0b3b9 --- /dev/null +++ b/pos_salesperson/static/src/salesperson_list/salesperson_list.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + +
NameContact
+
+ +
+
+
+
diff --git a/pos_salesperson/views/pos_order_view.xml b/pos_salesperson/views/pos_order_view.xml new file mode 100644 index 00000000000..af995290370 --- /dev/null +++ b/pos_salesperson/views/pos_order_view.xml @@ -0,0 +1,24 @@ + + + + pos.order.form.inherit.pos_salesperson + pos.order + + + + + + + + + + pos.order.list.inherit.pos_salesperson + pos.order + + + + + + + +