Skip to content

Commit 7138525

Browse files
committed
[ADD] pos_salesperson: Added Salesperson button in POS
- Added salesperson_id field in pos.order model pointing to hr.employee. - Displayed salesperson field in POS order list and form views. - Implemented button in POS UI for selecting a salesperson per order. - Created OWL components with search and fuzzy match functionality. - Loaded hr.employee model into POS using pos_available_models registry. - Extended pos.session to include HR employee data during POS loading.
1 parent 9a7ae06 commit 7138525

17 files changed

+341
-0
lines changed

pos_salesperson/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import models

pos_salesperson/__manifest__.py

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"name": "pos_salesperson",
3+
"depends": ["base", "hr", "point_of_sale"],
4+
"data": ["views/pos_order_view_inherit.xml"],
5+
"assets": {
6+
"point_of_sale._assets_pos": [
7+
"pos_salesperson/static/src/app/**/*",
8+
],
9+
},
10+
"license": "LGPL-3",
11+
}

pos_salesperson/models/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from . import pos_order
2+
from . import pos_session

pos_salesperson/models/pos_order.py

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from odoo import fields, models
2+
3+
4+
class PosOrderInherit(models.Model):
5+
_inherit = "pos.order"
6+
7+
salesperson_id = fields.Many2one("hr.employee", string="SalesPerson")

pos_salesperson/models/pos_session.py

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from odoo import api, models
2+
3+
4+
class PosSession(models.Model):
5+
_inherit = "pos.session"
6+
7+
@api.model
8+
def _load_pos_data_models(self, config_id):
9+
data = super()._load_pos_data_models(config_id)
10+
data += ["hr.employee"]
11+
return data
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Component } from "@odoo/owl";
2+
import { useService } from "@web/core/utils/hooks";
3+
import { Dropdown } from "@web/core/dropdown/dropdown";
4+
5+
export class SalesPersonLine extends Component {
6+
static template = "pos_salesperson.SalesLine";
7+
static components = { Dropdown };
8+
static props = [
9+
"close",
10+
"salesperson",
11+
"isSelected",
12+
"onClickUnselect",
13+
"onClickSalesPerson",
14+
];
15+
16+
setup() {
17+
this.ui = useService("ui");
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<templates id="template" xml:space="preserve">
3+
4+
<t t-name="pos_salesperson.SalesLine">
5+
<t t-if="ui.isSmall">
6+
<div class="partner-info d-flex flex-column p-1 border-bottom" t-att-class="{'bg-primary-subtle': props.isSelected}" t-att-data-id="props.salesperson.id" t-on-click="() => this.props.onClickSalesPerson(props.salesperson)">
7+
<div class="d-flex justify-content-between align-items-center p-1">
8+
<div>
9+
<b t-esc="props.salesperson.name or ''" />
10+
<div class="company-field text-bg-muted" t-esc="props.salesperson.parent_name or ''" />
11+
</div>
12+
</div>
13+
<div class="partner-line-adress p-1" t-if="props.salesperson?.work_contact_id?.contact_address" t-esc="props.salesperson?.work_contact_id?.contact_address" />
14+
<div class="partner-line-email p-1">
15+
<div class="mb-1" t-if="props.salesperson?.work_contact_id?.phone">
16+
<i class="fa fa-fw fa-phone me-2" />
17+
<t t-esc="props.salesperson?.work_contact_id?.phone" />
18+
</div>
19+
<div class="mb-1" t-if="props.salesperson?.work_contact_id?.mobile">
20+
<i class="fa fa-fw fa-mobile me-2" />
21+
<t t-esc="props.salesperson?.work_contact_id?.mobile" />
22+
</div>
23+
<div t-if="props.salesperson?.work_contact_id?.email" class="email-field mb-1">
24+
<i class="fa fa-fw fa-paper-plane-o me-2" />
25+
<t t-esc="props.salesperson?.work_contact_id?.email" />
26+
</div>
27+
</div>
28+
<div class="d-flex justify-content-between align-items-center p-1">
29+
<button t-if="props.isSelected" t-on-click.stop="props.onClickUnselect" class="unselect-tag-mobile d-inline-block d-lg-none btn btn-light border ms-2">
30+
<i class="fa fa-times me-1"></i>
31+
<span>UNSELECT</span>
32+
</button>
33+
</div>
34+
</div>
35+
</t>
36+
<t t-else="">
37+
<tr class="partner-line partner-info" t-att-class="{'selected': props.isSelected}" t-att-data-id="props.salesperson.id" t-on-click="() => this.props.onClickSalesPerson(props.salesperson)">
38+
<td>
39+
<b t-esc="props.salesperson.name or ''" />
40+
<div class="company-field text-bg-muted" t-esc="props.salesperson.parent_name or ''" />
41+
</td>
42+
<td>
43+
<div class="partner-line-adress" t-if="props.salesperson?.work_contact_id?.contact_address" t-esc="props.salesperson?.work_contact_id?.contact_address" />
44+
</td>
45+
<td class="partner-line-email ">
46+
<div t-if="props.salesperson?.work_contact_id?.phone">
47+
<i class="fa fa-fw fa-phone me-2"/>
48+
<t t-esc="props.salesperson?.work_contact_id?.phone"/>
49+
</div>
50+
<div t-if="props.salesperson?.work_contact_id?.mobile">
51+
<i class="fa fa-fw fa-mobile me-2"/>
52+
<t t-esc="props.salesperson?.work_contact_id?.mobile"/>
53+
</div>
54+
<div t-if="props.salesperson?.work_contact_id?.email" class="email-field">
55+
<i class="fa fa-fw fa-paper-plane-o me-2"/>
56+
<t t-esc="props.salesperson?.work_contact_id?.email" />
57+
</div>
58+
</td>
59+
60+
<td class="edit-partner-button-cell align-middle pe-0">
61+
<button t-if="props.isSelected" t-on-click.stop="props.onClickUnselect" class="unselect-tag d-lg-inline-block d-none btn btn-link btn-lg mt-1 float-end">
62+
<i class="fa fa-check"/>
63+
</button>
64+
</td>
65+
<td class="edit-partner-button-cell align-middle">
66+
<Dropdown>
67+
<button class="btn btn-light btn-lg lh-lg border float-end">
68+
<i class="fa fa-fw fa-bars"/>
69+
</button>
70+
<t t-set-slot="content">
71+
</t>
72+
</Dropdown>
73+
</td>
74+
</tr>
75+
</t>
76+
</t>
77+
78+
</templates>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { _t } from "@web/core/l10n/translation";
2+
import { useService } from "@web/core/utils/hooks";
3+
import { fuzzyLookup } from "@web/core/utils/search";
4+
import { Dialog } from "@web/core/dialog/dialog";
5+
import { usePos } from "@point_of_sale/app/store/pos_hook";
6+
import { Input } from "@point_of_sale/app/generic_components/inputs/input/input";
7+
import { Component, useState } from "@odoo/owl";
8+
import { unaccent } from "@web/core/utils/strings";
9+
import { SalesPersonLine } from "../SalesPersonLine/SalesPersonLine";
10+
11+
export class SalesPersonList extends Component {
12+
static template = "pos_salesperson.SalesList";
13+
static components = { SalesPersonLine, Dialog, Input };
14+
static props = {
15+
salesperson: {
16+
optional: true,
17+
type: [{ value: null }, Object],
18+
},
19+
getPayload: { type: Function },
20+
close: { type: Function },
21+
};
22+
setup() {
23+
this.pos = usePos();
24+
this.ui = useState(useService("ui"));
25+
// this.dialog = useService("dialog");
26+
this.state = useState({
27+
query: null,
28+
});
29+
}
30+
31+
getSalesPerson() {
32+
const searchWord = unaccent((this.state.query || "").trim(), false);
33+
const salesperson = this.pos.models["hr.employee"].getAll();
34+
const exactMatches = salesperson.filter(
35+
(person) => (person.name || "").toLowerCase() === searchWord.toLowerCase()
36+
);
37+
38+
if (exactMatches.length > 0) {
39+
return exactMatches;
40+
}
41+
const availableSalesPerson = searchWord
42+
? fuzzyLookup(searchWord, salesperson, (sale) =>
43+
unaccent(sale.searchString || "", false)
44+
)
45+
: salesperson.slice(0, 100).toSorted((a, b) => {
46+
if (this.props.salesperson && this.props.salesperson.id === a.id) {
47+
return -1;
48+
}
49+
return (a.name || "").localeCompare(b.name || "");
50+
});
51+
52+
return availableSalesPerson;
53+
}
54+
55+
clickSalesPerson(salesperson) {
56+
this.props.getPayload(salesperson);
57+
this.props.close();
58+
}
59+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<templates id="template" xml:space="preserve">
3+
4+
<t t-name="pos_salesperson.SalesList">
5+
<Dialog bodyClass="'partner-list overflow-y-auto'" contentClass="'h-100'">
6+
<t t-set-slot="header">
7+
8+
<Input tModel="[state, 'query']" class="'ms-auto'" isSmall="ui.isSmall" placeholder.translate="Search SalesPerson..." icon="{type: 'fa', value: 'fa-search'}" autofocus="true" debounceMillis="100" />
9+
</t>
10+
<table class="table table-hover">
11+
<thead t-if="!ui.isSmall">
12+
<tr>
13+
<th class="py-2">Name</th>
14+
<th class="py-2">Address</th>
15+
<th class="partner-line-email py-2">Contact</th>
16+
<th class="pos-right-align py-2" t-if="isBalanceDisplayed">Balance</th>
17+
<th class="py-2"></th>
18+
</tr>
19+
</thead>
20+
<tbody>
21+
<t t-foreach="getSalesPerson()" t-as="salesperson" t-key="salesperson.id">
22+
<SalesPersonLine close="props.close" salesperson="salesperson" isSelected="props.salesperson?.id === salesperson.id" onClickUnselect.bind="() => this.clickSalesPerson()" onClickSalesPerson.bind="clickSalesPerson"/>
23+
</t>
24+
</tbody>
25+
</table>
26+
<div t-if="state.query" class="search-more-button d-flex justify-content-center my-2">
27+
<button class="btn btn-lg btn-primary">Search more</button>
28+
</div>
29+
<t t-set-slot="footer">
30+
<div class="d-flex justify-content-start flex-wrap gap-2 w-100">
31+
<button class="btn btn-secondary btn-lg lh-lg o-default-button" t-on-click="() => this.clickSalesPerson(this.props.salesperson)">Discard</button>
32+
</div>
33+
</t>
34+
</Dialog>
35+
</t>
36+
37+
</templates>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { ControlButtons } from "@point_of_sale/app/screens/product_screen/control_buttons/control_buttons";
2+
import { SelectSalespersonButton } from "../select_salesperson_button/select_salesperson_button";
3+
import { patch } from "@web/core/utils/patch";
4+
5+
patch(ControlButtons, {
6+
components: {
7+
...ControlButtons.components,
8+
SelectSalespersonButton,
9+
},
10+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0"?>
2+
<template xml:space="preserve">
3+
<t t-name="pos_salesperson.ControlButtons" t-inherit="point_of_sale.ControlButtons" t-inherit-mode="extension">
4+
<xpath expr="//button[@t-on-click='props.onClickMore']" position="before">
5+
<SelectSalespersonButton/>
6+
</xpath>
7+
</t>
8+
</template>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { registry } from "@web/core/registry";
2+
import { Base } from "@point_of_sale/app/models/related_models";
3+
4+
export class HrEmployee extends Base {
5+
static pythonModel = "hr.employee";
6+
7+
get searchString() {
8+
const fields = ["name"];
9+
return fields
10+
.map((field) => {
11+
return this[field] || "";
12+
})
13+
.filter(Boolean)
14+
.join(" ");
15+
}
16+
}
17+
18+
registry
19+
.category("pos_available_models")
20+
.add(HrEmployee.pythonModel, HrEmployee);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { PosOrder } from "@point_of_sale/app/models/pos_order";
2+
import { patch } from "@web/core/utils/patch";
3+
4+
patch(PosOrder.prototype, {
5+
get_salesperson() {
6+
return this.salesperson_id;
7+
},
8+
set_salesperson(salesperson) {
9+
this.salesperson_id = salesperson;
10+
},
11+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { PosStore } from "@point_of_sale/app/store/pos_store";
2+
import { patch } from "@web/core/utils/patch";
3+
import { SalesPersonList } from "../SalesPersonList/SalesPersonList";
4+
import { makeAwaitable } from "@point_of_sale/app/store/make_awaitable_dialog";
5+
6+
patch(PosStore.prototype, {
7+
async selectSalesperson() {
8+
const currentOrder = this.get_order();
9+
if (!currentOrder) {
10+
return false;
11+
}
12+
const currentSalesperson = currentOrder.get_salesperson();
13+
const payload = await makeAwaitable(this.dialog, SalesPersonList, {
14+
salesperson: currentSalesperson || null,
15+
getPayload: (newPartner) => currentOrder.set_salesperson(newPartner),
16+
});
17+
18+
if (payload) {
19+
currentOrder.set_salesperson(payload);
20+
} else {
21+
currentOrder.set_salesperson(false);
22+
}
23+
24+
return currentSalesperson;
25+
},
26+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Component, useState } from "@odoo/owl";
2+
import { usePos } from "@point_of_sale/app/store/pos_hook";
3+
4+
export class SelectSalespersonButton extends Component {
5+
static template = "pos_salesperson.SelectSalespersonButton";
6+
setup() {
7+
this.pos = usePos();
8+
}
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?xml version="1.0"?>
2+
<template id='template' xml:space='preserve'>
3+
<t t-name='pos_salesperson.SelectSalespersonButton'>
4+
<button class="btn btn-light btn-lg lh-lg text-truncate w-auto" t-on-click="() => this.pos.selectSalesperson()">
5+
<div t-if="this.pos.get_order().get_salesperson()" t-esc="this.pos.get_order().get_salesperson().name" class="text-truncate text-action" />
6+
<div t-else="">Salesperson</div>
7+
</button>
8+
</t>
9+
</template>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?xml version="1.0"?>
2+
<odoo>
3+
<record id="pos_order_inherit_list_view" model="ir.ui.view">
4+
<field name="name">Salesperson Pos list view</field>
5+
<field name="model">pos.order</field>
6+
<field name="inherit_id" ref="point_of_sale.view_pos_order_tree"/>
7+
<field name="arch" type="xml">
8+
<xpath expr="//field[@name='partner_id']" position="after">
9+
<field name="salesperson_id"/>
10+
</xpath>
11+
</field>
12+
</record>
13+
<record id="pos_order_inherit_form_view" model="ir.ui.view">
14+
<field name="name">Salesperson Pos form view</field>
15+
<field name="model">pos.order</field>
16+
<field name="inherit_id" ref="point_of_sale.view_pos_pos_form"/>
17+
<field name="arch" type="xml">
18+
<xpath expr="//sheet//group//field[@name='fiscal_position_id']" position="after">
19+
<field name="salesperson_id"/>
20+
</xpath>
21+
</field>
22+
</record>
23+
</odoo>

0 commit comments

Comments
 (0)