[IMP] point_of_sale: simplify PartnerListScreen

In this commit we follow a set of changes to
simplify the view of PartnerListScreen.This includes:
- directly select a parnter by clicking on them
- rename "EDIT" to "DETAILS" and display always
- use short address
- add company name below the partner name (if any)
- searching by company name is possible
- merge phone and e-mail in one column
- merge loyalty points and total due in one column
- save automatically selects the partner
- header buttons are reordered
- selected partner is displayed first
- selecter partner has an "unselect" tag
- visual imporovements (lowlighting etc.)
- move "settle due" button and "more info" link
into details edit

These changes expose less info to the user and
take some burden off the UI. Also, by selecting
a partner directly when clicking on them we follow
the "make the common case fast" design principle.
This way, the main action of the screen is accomplished
in 1 click instead of 2.

task-2669747

Part-of: odoo/odoo#81226
This commit is contained in:
Eleftherios Kritsotakis
2022-06-02 18:58:05 +02:00
committed by Pierre Masereel
parent 2ae58e7741
commit 3e8ae40d05
13 changed files with 200 additions and 170 deletions
+1 -1
View File
@@ -1756,7 +1756,7 @@ class PosSession(models.Model):
'domain': [],
'fields': [
'name', 'street', 'city', 'state_id', 'country_id', 'vat', 'lang', 'phone', 'zip', 'mobile', 'email',
'barcode', 'write_date', 'property_account_position_id', 'property_product_pricelist'
'barcode', 'write_date', 'property_account_position_id', 'property_product_pricelist', 'parent_name'
],
},
}
+85 -10
View File
@@ -898,6 +898,10 @@ td {
flex-wrap: wrap;
gap: 2px;
}
.pos .product-list-container .product-list-empty p {
margin: 10px;
text-align: center;
}
@media screen and (max-width: 768px) {
.pos .product-list {
padding: 0px;
@@ -1798,12 +1802,46 @@ td {
}
/* e) The Client List Screen */
/* e) The Partner List Screen */
.pos .partnerlist-screen .full-content{
overflow: auto;
}
.pos .no-results-message {
text-align: center;
}
.pos .no-results-message {
margin-top: 45px;
padding: 0 10%;
}
.pos .no-results-message .button {
margin: auto;
width: 180px;
border-radius: 25px;
padding: 10px 0;
cursor: pointer;
font-size: large;
box-shadow: 0px -2px 5px -3px rgb(82,82,82) inset;
transition: all linear 75ms;
}
.pos .no-results-message .button:hover {
opacity: 0.8;
}
.pos .no-results-message .button:active {
opacity: 0.5;
}
.pos .partnerlist-screen .top-content div.top-right-buttons {
display: flex;
justify-content: flex-end;
flex: 1;
}
.pos .partnerlist-screen .partner-list{
font-size: 16px;
width: 100%;
@@ -1813,6 +1851,30 @@ td {
padding: 12px 8px;
}
.pos .partnerlist-screen .fa.fa-phone,
.pos .partnerlist-screen .fa.fa-paper-plane-o {
margin-right: 4px;
}
.pos .partnerlist-screen .e-mail-field {
white-space: nowrap;
}
.pos .partnerlist-screen tr div.company-field {
color: #888c8f;
}
.pos .partnerlist-screen tr.highlight div.company-field {
color: white;
}
.pos .partnerlist-screen tr.highlight div.unselect-tag {
font-size: large;
color:#555555;
font-weight: bold;
padding-top: 5px;
}
.pos .partnerlist-screen .partner-list th.partner-line-last-column-placeholder,
.pos .partnerlist-screen .partner-list td.partner-line-last-column-placeholder {
padding: 0;
@@ -1822,22 +1884,31 @@ td {
transition: all 150ms linear;
background: rgb(230,230,230);
}
.pos .partnerlist-screen tr.partner-line:hover {
cursor: pointer;
}
.pos .partnerlist-screen .partner-list thead > tr,
.pos .partnerlist-screen .partner-list tr:nth-child(even) {
background: rgb(247,247,247);
background: rgb(247,247,247,0.1);
}
.pos .partnerlist-screen .partner-list tr.highlight{
transition: all 150ms linear;
background: rgb(110,200,155) !important;
background: rgb(110,200,155);
color: white;
}
.pos .partnerlist-screen .partner-list tr.lowlight{
transition: all 150ms linear;
background: rgb(216, 238, 227);
.pos .partnerlist-screen .partner-list tr.highlight:active {
background-color: black;
color: white;
}
.pos .partnerlist-screen .partner-list tr.lowlight:nth-child(even){
transition: all 150ms linear;
background: rgb(227, 246, 237);
.pos .partnerlist-screen tr.partner-line:hover {
cursor: pointer;
background: rgb(207, 238, 222);
}
.pos .partnerlist-screen tr.partner-line:active {
background-color: black;
color: white;
}
.pos .edit-partner-button:hover {
background: #c7c7c7;
}
.pos .partner-line {
vertical-align: text-top;
@@ -1846,6 +1917,10 @@ td {
margin-top: 6px;
color: black;
}
.pos .partner-list tr.partner-line.highlight{
background: rgb(110,200,155);
color: white;
}
.pos .partnerlist-screen .partner-details{
padding: 16px;
}
@@ -6,7 +6,14 @@ odoo.define('point_of_sale.PartnerLine', function(require) {
class PartnerLine extends PosComponent {
get highlight() {
return this.props.partner !== this.props.selectedPartner ? '' : 'highlight';
return this._isPartnerSelected ? 'highlight' : '';
}
get shortAddress() {
const { partner } = this.props;
return [partner.zip, partner.city, partner.state_id[1]].filter(field => field).join(', ');
}
get _isPartnerSelected() {
return this.props.partner === this.props.selectedPartner;
}
}
PartnerLine.template = 'PartnerLine';
@@ -29,7 +29,6 @@ odoo.define('point_of_sale.PartnerListScreen', function(require) {
setup() {
super.setup();
useListener('click-save', () => this.env.bus.trigger('save-partner'));
useListener('click-edit', () => this.editPartner());
useListener('save-changes', this.saveChanges);
// We are not using useState here because the object
@@ -41,12 +40,8 @@ odoo.define('point_of_sale.PartnerListScreen', function(require) {
query: null,
selectedPartner: this.props.partner,
detailIsShown: false,
// isEditMode: false,
editModeProps: {
partner: {
country_id: this.env.pos.company.country_id,
state_id: this.env.pos.company.state_id,
}
partner: null,
},
};
this.updatePartnerList = debounce(this.updatePartnerList, 70);
@@ -66,6 +61,10 @@ odoo.define('point_of_sale.PartnerListScreen', function(require) {
this.props.resolve({ confirmed: true, payload: this.state.selectedPartner });
this.trigger('close-temp-screen');
}
activateEditMode() {
this.state.detailIsShown = true;
this.render(true);
}
// Getters
get currentOrder() {
@@ -79,23 +78,27 @@ odoo.define('point_of_sale.PartnerListScreen', function(require) {
} else {
res = this.env.pos.db.get_partners_sorted(1000);
}
return res.sort(function (a, b) { return (a.name || '').localeCompare(b.name || '') });
}
get isNextButtonVisible() {
return this.state.selectedPartner ? true : false;
}
/**
* Returns the text and command of the next button.
* The command field is used by the clickNext call.
*/
get nextButton() {
if (!this.props.partner) {
return { command: 'set', text: this.env._t('Set Customer') };
} else if (this.props.partner && this.props.partner === this.state.selectedPartner) {
return { command: 'deselect', text: this.env._t('Deselect Customer') };
} else {
return { command: 'set', text: this.env._t('Change Customer') };
res.sort(function (a, b) { return (a.name || '').localeCompare(b.name || '') });
// the selected partner (if any) is displayed at the top of the list
if (this.state.selectedPartner) {
let indexOfSelectedPartner = res.findIndex( partner =>
partner.id === this.state.selectedPartner.id
);
if (indexOfSelectedPartner !== -1) {
res.splice(indexOfSelectedPartner, 1);
res.unshift(this.state.selectedPartner);
}
}
return res
}
get isEveryPartnerLoadedAfterStartOfSession() {
return !this.env.pos.config.limited_partners_loading || this.env.pos.config.partner_load_background;
}
get isBalanceDisplayed() {
return this.env.pos.config.module_pos_loyalty;
}
get partnerLink() {
return `/web#model=res.partner&id=${this.state.editModeProps.partner.id}`;
}
// Methods
@@ -104,43 +107,37 @@ odoo.define('point_of_sale.PartnerListScreen', function(require) {
// order to lower its trigger rate.
async updatePartnerList(event) {
this.state.query = event.target.value;
const partners = this.partners;
if (event.code === 'Enter' && partners.length === 1) {
this.state.selectedPartner = partners[0];
this.clickNext();
if (event.code === 'Enter') {
if (!this.isEveryPartnerLoadedAfterStartOfSession) {
await this.searchPartner();
}
const partners = this.partners;
if (partners.length === 1) {
this.clickPartner(partners[0]);
}
} else {
this.render(true);
}
}
clickPartner(partner) {
if (this.state.selectedPartner === partner) {
if (this.state.selectedPartner && this.state.selectedPartner.id === partner.id) {
this.state.selectedPartner = null;
} else {
this.state.selectedPartner = partner;
}
this.render(true);
}
editPartner() {
this.state.editModeProps = {
partner: this.state.selectedPartner,
};
this.state.detailIsShown = true;
this.render(true);
}
clickNext() {
this.state.selectedPartner = this.nextButton.command === 'set' ? this.state.selectedPartner : null;
this.confirm();
}
activateEditMode(isNewPartner) {
// this.state.isEditMode = true;
this.state.detailIsShown = true;
this.state.isNewPartner = isNewPartner;
if (!isNewPartner) {
this.state.editModeProps = {
partner: this.state.selectedPartner,
};
editPartner(partner) {
this.state.editModeProps.partner = partner;
this.activateEditMode();
}
createPartner() {
// initialize the edit screen with default details about country & state
this.state.editModeProps.partner = {
country_id: this.env.pos.company.country_id,
state_id: this.env.pos.company.state_id,
}
this.render(true);
this.activateEditMode();
}
async saveChanges(event) {
try {
@@ -151,8 +148,7 @@ odoo.define('point_of_sale.PartnerListScreen', function(require) {
});
await this.env.pos.load_new_partners();
this.state.selectedPartner = this.env.pos.db.get_partner_by_id(partnerId);
this.state.detailIsShown = false;
this.render(true);
this.confirm();
} catch (error) {
if (isConnectionError(error)) {
await this.showPopup('OfflineErrorPopup', {
@@ -178,7 +174,8 @@ odoo.define('point_of_sale.PartnerListScreen', function(require) {
async getNewPartners() {
let domain = [];
if(this.state.query) {
domain = [["name", "ilike", this.state.query + "%"]];
domain = ['|', ["name", "ilike", this.state.query + "%"],
["parent_name", "ilike", this.state.query + "%"]];
}
const result = await this.env.services.rpc(
{
+3
View File
@@ -254,6 +254,9 @@ var PosDB = core.Class.extend({
if(partner.vat){
str += '|' + partner.vat;
}
if(partner.parent_name){
str += '|' + partner.parent_name;
}
str = '' + partner.id + ':' + str.replace(':', '').replace(/\n/g, ' ') + '\n';
return str;
},
@@ -5,24 +5,35 @@
<tr t-attf-class="partner-line {{highlight}}" t-att-data-id="props.partner.id"
t-on-click="() => this.props.onClickPartner(props.partner)">
<td>
<t t-esc="props.partner.name or ''" />
<span t-if="highlight">
<br/><button class="edit-partner-button" t-on-click.stop="() => this.trigger('click-edit')">EDIT</button>
</span>
<div>
<t t-esc="props.partner.name or ''" />
</div>
<div class="company-field">
<t t-esc="props.partner.parent_name or ''" />
</div>
<div t-if="_isPartnerSelected" class="unselect-tag">
<i class="fa fa-times"></i>
<span> unselect </span>
</div>
</td>
<td t-if="!env.isMobile">
<t t-esc="props.partner.address" />
<t t-esc="shortAddress" />
</td>
<td t-if="!env.isMobile" style="width: 130px;">
<t t-esc="props.partner.phone || ''" />
<td class="partner-line-email" t-if="!env.isMobile">
<div t-if="props.partner.phone">
<i class="fa fa-phone"/><t t-esc="props.partner.phone"/>
</div>
<div t-if="props.partner.email" class="email-field">
<i class="fa fa-paper-plane-o"/><t t-esc="props.partner.email" />
</div>
</td>
<td t-if="env.isMobile">
<t t-esc="props.partner.zip or ''" />
<span t-if="highlight"><br/></span>
</td>
<td class="partner-line-email">
<t t-esc="props.partner.email or ''" />
<span t-if="highlight"><br/></span>
<td class="partner-line-balance" t-if="props.isBalanceDisplayed"></td>
<td>
<button class="edit-partner-button" t-on-click.stop="() => props.onClickEdit(props.partner)">DETAILS</button>
</td>
<td class="partner-line-last-column-placeholder oe_invisible"></td>
</tr>
@@ -5,45 +5,33 @@
<div class="partnerlist-screen screen">
<div class="screen-content">
<div class="top-content">
<div class="button back" t-on-click="back">
<i class="fa fa-angle-double-left"/>
<t t-if="!env.isMobile"> Discard</t>
</div>
<div t-if="!state.detailIsShown &amp;&amp; !state.selectedClient" class="button" role="img" aria-label="Add a customer"
t-on-click="() => this.activateEditMode(true)"
title="Add a customer">
<i class="fa fa-plus"/>
<t t-if="!env.isMobile"> Create</t>
</div>
<div t-if="isNextButtonVisible" t-on-click="clickNext"
class="button next highlight">
<t t-if="!env.isMobile">
<t t-esc="nextButton.text" />
</t>
<t t-else="">
<i t-if="nextButton.command === 'deselect'" class="fa fa-trash"/>
<i t-elif="nextButton.command === 'set'" class="fa fa-check"/>
</t>
</div>
<div class="button" t-if="state.detailIsShown" t-on-click="() => this.trigger('click-save')">
<i class="fa fa-floppy-o"/>
<div class="button highlight" t-if="state.detailIsShown" t-on-click="() => this.trigger('click-save')">
<i t-if="env.isMobile" class="fa fa-floppy-o"/>
<t t-if="!env.isMobile">
<span> Save</span>
</t>
</div>
<div t-if="!state.detailIsShown" class="searchbox-partner top-content-center">
<input placeholder="Search Customers" size="1" t-on-keyup="updatePartnerList" />
<span class="search-clear-partner"></span>
<div t-if="!state.detailIsShown" class="button new-customer" role="img" aria-label="Add a customer"
t-on-click="createPartner"
title="Add a customer">
<i t-if="env.isMobile" class="fa fa-plus"/>
<t t-if="!env.isMobile"> Create</t>
</div>
<div class="button back" t-on-click="back">
<i t-if="env.isMobile" class="fa fa-angle-double-left"/>
<t t-if="!env.isMobile"> Discard</t>
</div>
<div class="top-right-buttons">
<div t-if="state.detailIsShown" class="button more-info">
<a t-att-href="partnerLink" target="_blank"> More info</a>
</div>
<div class="search-bar-container sb-partner" t-if="!state.detailIsShown">
<div class="pos-search-bar">
<i class="fa fa-search" t-on-click="_onPressEnterKey"/>
<input t-ref="search-word-input-partner" placeholder="Search Customers..." size="1" t-on-keyup="updatePartnerList" />
<i class="fa fa-times" t-on-click="_clearSearch"/>
</div>
</div>
<div class="button back" t-on-click="searchPartner" t-if="!state.detailIsShown">
<span class="database-icon">
<i class="fa fa-database"/>
</span>
<t t-if="!env.isMobile">
<span>
Load Customers
</span>
</t>
</div>
</div>
<section class="full-content">
@@ -63,9 +51,10 @@
<tr>
<th>Name</th>
<th t-if="!env.isMobile">Address</th>
<th t-if="!env.isMobile">Phone</th>
<th t-if="!env.isMobile" class="partner-line-email">Contact</th>
<th t-if="env.isMobile">ZIP</th>
<th class="partner-line-email">Email</th>
<th class="pos-right-align" t-if="isBalanceDisplayed">Balance</th>
<th class="partner-line-details"></th>
<th class="partner-line-last-column-placeholder oe_invisible"></th>
</tr>
</thead>
@@ -75,6 +64,8 @@
<PartnerLine partner="partner"
selectedPartner="state.selectedPartner"
detailIsShown="state.detailIsShown"
isBalanceDisplayed="isBalanceDisplayed"
onClickEdit.bind="editPartner"
onClickPartner.bind="clickPartner"/>
</t>
</tbody>
@@ -11,18 +11,12 @@
</t>
</div>
<div t-else="" class="product-list-empty">
<div class="product-list-empty">
<t t-if="searchWord !== ''">
<p>
No results found for "
<b t-esc="searchWord"></b>
".
</p>
</t>
<t t-else="">
<p>There are no products in this category.</p>
</t>
</div>
<p t-if="searchWord !== ''">
No results found for "<b t-esc="searchWord"></b>". Press "Enter" to search the database.
</p>
<p t-else="">
There are no products in this category.
</p>
</div>
</div>
</div>
@@ -94,7 +94,6 @@ odoo.define('point_of_sale.tour.Chrome', function (require) {
ProductScreen.exec.addOrderline('Whiteboard Pen', '5', '6');
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('Nicole Ford');
ProductScreen.do.clickSetCustomer();
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Bank');
PaymentScreen.do.clickInvoiceButton();
@@ -18,14 +18,12 @@ odoo.define('point_of_sale.tour.TicketScreen', function (require) {
ProductScreen.exec.addOrderline('Desk Pad', '1', '2');
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('Nicole Ford');
ProductScreen.do.clickSetCustomer();
Chrome.do.clickTicketButton();
TicketScreen.check.nthRowContains(2, 'Nicole Ford');
TicketScreen.do.clickNewTicket();
ProductScreen.exec.addOrderline('Desk Pad', '1', '3');
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('Brandon Freeman');
ProductScreen.do.clickSetCustomer();
ProductScreen.do.clickPayButton();
PaymentScreen.check.isShown();
Chrome.do.clickTicketButton();
@@ -79,7 +77,7 @@ odoo.define('point_of_sale.tour.TicketScreen', function (require) {
TicketScreen.do.clickControlButton('Invoice');
Chrome.do.confirmPopup();
PartnerListScreen.check.isShown();
PartnerListScreen.exec.setPartner('Colleen Diaz');
PartnerListScreen.do.clickPartner('Colleen Diaz');
TicketScreen.check.partnerIs('Colleen Diaz');
// Reprint receipt
TicketScreen.do.clickControlButton('Print Receipt');
@@ -10,24 +10,6 @@ odoo.define('point_of_sale.tour.PartnerListScreenTourMethods', function (require
content: `click partner '${name}' from partner list screen`,
trigger: `.partnerlist-screen .partner-list-contents .partner-line td:contains("${name}")`,
},
{
content: `check if partner '${name}' is highlighted`,
trigger: `.partnerlist-screen .partner-list-contents .partner-line.highlight td:contains("${name}")`,
run: () => {},
},
];
}
clickSet() {
return [
{
content: 'check if set button shown',
trigger: '.partnerlist-screen .button.next.highlight',
run: () => {},
},
{
content: 'click set button',
trigger: '.partnerlist-screen .button.next.highlight',
},
];
}
}
@@ -44,14 +26,7 @@ odoo.define('point_of_sale.tour.PartnerListScreenTourMethods', function (require
}
}
class Execute {
setPartner(name) {
const steps = [];
steps.push(...this._do.clickPartner(name));
steps.push(...this._do.clickSet());
return steps;
}
}
class Execute {}
return createTourMethods('PartnerListScreen', Do, Check, Execute);
});
@@ -108,20 +108,6 @@ odoo.define('point_of_sale.tour.ProductScreenTourMethods', function (require) {
content: `select customer '${name}'`,
trigger: `.partnerlist-screen .partner-line td:contains("${name}")`,
},
{
content: `partner line '${name}' is highlighted`,
trigger: `.partnerlist-screen .partner-line.highlight td:contains("${name}")`,
run: () => {},
},
];
}
clickSetCustomer() {
return [
{
content: 'click set customer',
trigger: '.partnerlist-screen .button.next.highlight',
},
];
}
@@ -100,9 +100,6 @@ odoo.define('point_of_sale.tour.pricelist', function (require) {
}, {
content: "select Deco Addict",
trigger: ".partner-line:contains('Deco Addict')",
}, {
content: "confirm selection",
trigger: ".partnerlist-screen .next",
}, {
content: "click pricelist button",
trigger: ".control-button.o_pricelist_button",
@@ -120,9 +117,6 @@ odoo.define('point_of_sale.tour.pricelist', function (require) {
content: "select Lumber Inc",
trigger: ".partner-line:contains('Lumber Inc')",
}, {
content: "confirm selection",
trigger: ".partnerlist-screen .next",
}, {
content: "click pricelist button",
trigger: ".control-button.o_pricelist_button",
}, {