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 @@
+
+
+
+
+
+
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
+
+
+
+
+
+
+
+