Skip to content

Commit 7f474ea

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 037b396 commit 7f474ea

File tree

17 files changed

+341
-0
lines changed

17 files changed

+341
-0
lines changed

pos_salesperson/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import models

pos_salesperson/__manifest__.py

Lines changed: 11 additions & 0 deletions
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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from . import pos_order
2+
from . import pos_session
Lines changed: 7 additions & 0 deletions
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")
Lines changed: 11 additions & 0 deletions
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
Lines changed: 19 additions & 0 deletions
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+
}
Lines changed: 78 additions & 0 deletions
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>
Lines changed: 59 additions & 0 deletions
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+
}
Lines changed: 37 additions & 0 deletions
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>
Lines changed: 10 additions & 0 deletions
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+
});

0 commit comments

Comments
 (0)