fix: pos screen ui ux (#47680)

* fix: pos addl info dialog submit form on save

* feat: new invoice and recent order button on page action

* fix: item cart highlighted item scrolling

* fix: using icon instead of text in fullscreen button

* fix: search field clear button alignment

* fix: hide item selector on item details display

* fix: using add_action_icon

* fix: action of 'New Invoice' for unsaved changes

* fix: highlight numpad btns on hover

* fix: pos recent orders filter and list items

* chore: added icons for pos icon buttons

* fix: recent order toggle after invoice submission

* fix: capitalized text in select options
This commit is contained in:
Diptanil Saha
2025-05-23 12:36:21 +05:30
committed by GitHub
parent c3b17024bd
commit 89115688f7
11 changed files with 184 additions and 108 deletions

View File

@@ -32,6 +32,7 @@
"allow_rate_change",
"allow_discount_change",
"set_grand_total_to_default_mop",
"action_on_new_invoice",
"section_break_23",
"item_groups",
"column_break_25",
@@ -415,6 +416,13 @@
"fieldname": "set_grand_total_to_default_mop",
"fieldtype": "Check",
"label": "Set Grand Total to Default Payment Method"
},
{
"default": "Always Ask",
"fieldname": "action_on_new_invoice",
"fieldtype": "Select",
"label": "Action on New Invoice",
"options": "Always Ask\nSave Changes and Load New Invoice\nDiscard Changes and Load New Invoice"
}
],
"grid_page_length": 50,
@@ -443,7 +451,7 @@
"link_fieldname": "pos_profile"
}
],
"modified": "2025-05-09 11:23:28.632136",
"modified": "2025-05-23 12:12:32.247652",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Profile",

View File

@@ -28,6 +28,9 @@ class POSProfile(Document):
from erpnext.accounts.doctype.pos_profile_user.pos_profile_user import POSProfileUser
account_for_change_amount: DF.Link | None
action_on_new_invoice: DF.Literal[
"Always Ask", "Save Changes and Load New Invoice", "Discard Changes and Load New Invoice"
]
allow_discount_change: DF.Check
allow_rate_change: DF.Check
applicable_for_users: DF.Table[POSProfileUser]

View File

@@ -28,6 +28,14 @@ web_include_js = "erpnext-web.bundle.js"
web_include_css = "erpnext-web.bundle.css"
email_css = "email_erpnext.bundle.css"
app_include_icons = [
"/assets/erpnext/icons/pos-icons.svg",
]
web_include_icons = [
"/assets/erpnext/icons/pos-icons.svg",
]
doctype_js = {
"Address": "public/js/address.js",
"Communication": "public/js/communication.js",

View File

@@ -0,0 +1,15 @@
<!-- Icons for POS Icon Buttons. Taken from frappe/public/icons/lucide.svg -->
<svg id="frappe-symbols" aria-hidden="true" style="display: none;" class="icon" xmlns="http://www.w3.org/2000/svg">
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="icon-fullscreen">
<path d="M3 7V5a2 2 0 0 1 2-2h2" /> <path d="M17 3h2a2 2 0 0 1 2 2v2" /> <path d="M21 17v2a2 2 0 0 1-2 2h-2" /> <path d="M7 21H5a2 2 0 0 1-2-2v-2" /> <rect width="10" height="8" x="7" y="8" rx="1" />
</symbol>
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="icon-maximize">
<path d="M8 3H5a2 2 0 0 0-2 2v3" /> <path d="M21 8V5a2 2 0 0 0-2-2h-3" /> <path d="M3 16v3a2 2 0 0 0 2 2h3" /> <path d="M16 21h3a2 2 0 0 0 2-2v-3" />
</symbol>
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="icon-minimize">
<path d="M8 3v3a2 2 0 0 1-2 2H3" /> <path d="M21 8h-3a2 2 0 0 1-2-2V3" /> <path d="M3 16h3a2 2 0 0 1 2 2v3" /> <path d="M16 21v-3a2 2 0 0 1 2-2h3" />
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 1011 B

View File

@@ -579,7 +579,11 @@
align-items: center;
justify-content: center;
padding: var(--padding-md);
box-shadow: var(--shadow-sm);
box-shadow: var(--shadow-base);
&:hover {
background-color: var(--control-bg);
}
}
> .col-span-2 {
@@ -615,26 +619,30 @@
background-color: var(--control-bg);
}
> .invoice-name-date {
&.invoice-selected {
background-color: var(--control-bg);
}
> .invoice-name-customer {
display: flex;
flex-direction: column;
justify-content: space-around;
> .invoice-name {
> .invoice-customer {
@extend .nowrap;
font-size: var(--text-md);
display: flex;
align-items: center;
font-weight: 700;
}
> .invoice-date {
> .invoice-name {
@extend .nowrap;
font-size: var(--text-sm);
display: flex;
align-items: center;
}
}
> .invoice-total-status {
> .invoice-total-date {
display: flex;
flex-direction: column;
font-weight: 500;
@@ -648,17 +656,19 @@
text-align: right;
}
> .invoice-status {
> .invoice-date {
display: flex;
align-items: center;
color: var(--gray-500);
justify-content: right;
font-weight: 400;
}
}
}
> .item-details-container {
@extend .pos-card;
grid-column: span 4 / span 4;
grid-column: span 6 / span 6;
display: none;
flex-direction: column;
padding: var(--padding-lg);
@@ -915,6 +925,10 @@
justify-content: center;
padding: var(--padding-md);
box-shadow: var(--shadow-base);
&:hover {
background-color: var(--control-bg);
}
}
}
}
@@ -978,18 +992,23 @@
background-color: var(--fg-color);
padding: var(--padding-lg);
> .search-field {
width: 100%;
display: flex;
align-items: center;
> .status-search-fields {
display: grid;
grid-template-columns: 30% auto;
column-gap: 10px;
margin-top: var(--margin-md);
margin-bottom: var(--margin-xs);
}
> .status-field {
width: 100%;
display: flex;
align-items: center;
> .status-field {
width: 100%;
display: flex;
align-items: center;
}
> .search-field {
width: 100%;
display: flex;
align-items: center;
}
}
}

View File

@@ -213,7 +213,7 @@ erpnext.PointOfSale.Controller = class {
this.prepare_dom();
this.prepare_components();
this.prepare_menu();
this.prepare_fullscreen_btn();
this.prepare_btns();
this.make_new_invoice();
}
@@ -234,52 +234,42 @@ erpnext.PointOfSale.Controller = class {
prepare_menu() {
this.page.clear_menu();
this.page.add_menu_item(__("Open Form View"), this.open_form_view.bind(this), false, "Ctrl+F");
this.page.add_menu_item(
__("Toggle Recent Orders"),
this.toggle_recent_order.bind(this),
false,
"Ctrl+O"
);
this.page.add_menu_item(__("Save as Draft"), this.save_draft_invoice.bind(this), false, "Ctrl+S");
this.page.add_menu_item(__("Close the POS"), this.close_pos.bind(this), false, "Shift+Ctrl+C");
}
prepare_fullscreen_btn() {
this.page.page_actions.find(".custom-actions").empty();
this.page.add_button(__("Full Screen"), null, { btn_class: "btn-default fullscreen-btn" });
this.bind_fullscreen_events();
prepare_btns() {
this.page.clear_custom_actions();
this.page.clear_icons();
this.page.set_primary_action(__("New Invoice"), this.new_invoice_event.bind(this));
this.page.set_secondary_action(__("Recent Orders"), this.toggle_recent_order.bind(this));
this.page.add_action_icon(
"fullscreen",
this.bind_fullscreen_events.bind(this),
"btn-fullscreen",
"Fullscreen"
);
this.page.add_action_icon(
"minimize",
this.bind_fullscreen_events.bind(this),
"btn-minimize hide",
"Minimize"
);
}
bind_fullscreen_events() {
this.$fullscreen_btn = this.page.page_actions.find(".fullscreen-btn");
this.$fullscreen_btn.on("click", function () {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen();
} else if (document.exitFullscreen) {
document.exitFullscreen();
}
});
$(document).on("fullscreenchange", this.handle_fullscreen_change_event.bind(this));
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen();
this.toggle_fullscreen_btn(".btn-minimize", ".btn-fullscreen");
} else if (document.exitFullscreen) {
document.exitFullscreen();
this.toggle_fullscreen_btn(".btn-fullscreen", ".btn-minimize");
}
}
handle_fullscreen_change_event() {
let enable_fullscreen_label = __("Full Screen");
let exit_fullscreen_label = __("Exit Full Screen");
if (document.fullscreenElement) {
this.$fullscreen_btn[0].innerText = exit_fullscreen_label;
} else {
this.$fullscreen_btn[0].innerText = enable_fullscreen_label;
}
toggle_fullscreen_btn(show, hide) {
this.page.page_actions.find(hide).addClass("hide");
this.page.page_actions.find(show).removeClass("hide");
}
open_form_view() {
@@ -289,36 +279,48 @@ erpnext.PointOfSale.Controller = class {
toggle_recent_order() {
const show = this.recent_order_list.$component.is(":hidden");
this.page.btn_secondary.get(0).innerText = show ? __("Hide Recent Orders") : __("Recent Orders");
this.toggle_recent_order_list(show);
}
save_draft_invoice() {
new_invoice_event() {
const me = this;
if (!this.$components_wrapper.is(":visible")) return;
if (this.frm.doc.items.length == 0) {
frappe.show_alert({
message: __("You must add atleast one item to save it as draft."),
indicator: "red",
});
frappe.utils.play_sound("error");
if (this.frm.doc.items.length !== 0 && (this.frm.is_new() || this.frm.is_dirty())) {
if (this.settings.action_on_new_invoice === "Always Ask") {
frappe.confirm(
__("You have unsaved changes. Do you want to save the invoice?"),
() => {
me.frm.save().then(me.load_new_invoice_on_pos.bind(me));
},
() => {
me.load_new_invoice_on_pos();
}
);
return;
} else if (this.settings.action_on_new_invoice === "Save Changes and Load New Invoice") {
this.frm.save().then(me.load_new_invoice_on_pos.bind(me));
return;
}
this.load_new_invoice_on_pos();
return;
}
this.frm
.save(undefined, undefined, undefined, () => {
frappe.show_alert({
message: __("There was an error saving the document."),
indicator: "red",
});
frappe.utils.play_sound("error");
})
.then(() => {
frappe.run_serially([
() => frappe.dom.freeze(),
() => this.make_new_invoice(),
() => frappe.dom.unfreeze(),
]);
});
if (this.payment.$component.is(":visible")) {
this.load_new_invoice_on_pos();
}
}
load_new_invoice_on_pos() {
frappe.run_serially([
() => frappe.dom.freeze(),
() => this.make_new_invoice(),
() => this.toggle_recent_order_list(false),
() => this.toggle_components(true),
() => frappe.dom.unfreeze(),
]);
}
close_pos() {
@@ -384,7 +386,7 @@ erpnext.PointOfSale.Controller = class {
get_frm: () => this.frm,
toggle_item_selector: (minimize) => {
this.item_selector.resize_selector(minimize);
this.item_selector.toggle_component(!minimize);
this.cart.toggle_numpad(minimize);
},
@@ -468,8 +470,7 @@ erpnext.PointOfSale.Controller = class {
submit_invoice: () => {
this.frm.savesubmit().then((r) => {
this.toggle_components(false);
this.order_summary.toggle_component(true);
this.order_summary.load_summary_of(this.frm.doc, true);
this.toggle_submitted_invoice_summary(true);
frappe.show_alert({
indicator: "green",
message: __("POS invoice {0} created successfully", [r.doc.name]),
@@ -518,7 +519,7 @@ erpnext.PointOfSale.Controller = class {
});
},
edit_order: (doctype, name) => {
this.recent_order_list.toggle_component(false);
this.toggle_recent_order();
frappe.run_serially([
() => this.make_invoice_frm(doctype),
() => this.sync_draft_invoice_to_frm(doctype, name),
@@ -550,19 +551,29 @@ erpnext.PointOfSale.Controller = class {
}
toggle_recent_order_list(show) {
this.toggle_components(!show);
this.frm.doc.docstatus === 1
? this.toggle_submitted_invoice_summary(!show)
: this.toggle_components(!show);
this.recent_order_list.toggle_component(show);
this.order_summary.toggle_component(show);
if (this.frm.doc.docstatus === 0) this.order_summary.toggle_component(show);
}
toggle_components(show) {
this.cart.toggle_component(show);
this.cart.toggle_numpad(!show);
this.cart.toggle_checkout_btn(show);
this.item_selector.toggle_component(show);
// do not show item details or payment if recent order is toggled off
!show ? this.item_details.toggle_component(false) || this.payment.toggle_component(false) : "";
}
toggle_submitted_invoice_summary(show) {
this.order_summary.toggle_component(show);
this.order_summary.load_summary_of(this.frm.doc, true);
}
make_new_invoice() {
return frappe.run_serially([
() => frappe.dom.freeze(),

View File

@@ -171,8 +171,11 @@ erpnext.PointOfSale.ItemCart = class {
me.toggle_item_highlight(this);
const scrollTop = $cart_item.offset().top - me.$cart_items_wrapper.offset().top;
me.$cart_items_wrapper.animate({ scrollTop });
const numpad_section_hidden = !me.$numpad_section.is(":visible");
if (numpad_section_hidden) {
const scrollTop = $cart_item.offset().top - me.$cart_items_wrapper.offset().top;
me.$cart_items_wrapper.animate({ scrollTop });
}
const payment_section_hidden = !me.$totals_section.find(".edit-cart-btn").is(":visible");
if (!payment_section_hidden) {

View File

@@ -188,7 +188,7 @@ erpnext.PointOfSale.ItemSelector = class {
attach_clear_btn() {
this.search_field.$wrapper.find(".control-input").append(
`<span class="link-btn" style="top: 2px;">
`<span class="link-btn">
<a class="btn-open no-decoration" title="${__("Clear")}">
${frappe.utils.icon("close", "sm")}
</a>

View File

@@ -17,8 +17,10 @@ erpnext.PointOfSale.PastOrderList = class {
`<section class="past-order-list">
<div class="filter-section">
<div class="label">${__("Recent Orders")}</div>
<div class="search-field"></div>
<div class="status-field"></div>
<div class="status-search-fields">
<div class="status-field"></div>
<div class="search-field"></div>
</div>
</div>
<div class="invoices-container"></div>
</section>`
@@ -38,8 +40,12 @@ erpnext.PointOfSale.PastOrderList = class {
});
const me = this;
this.$invoices_container.on("click", ".invoice-wrapper", function () {
const invoice_doctype = $(this).attr("data-invoice-doctype");
const invoice_name = unescape($(this).attr("data-invoice-name"));
const invoice_clicked = $(this);
const invoice_doctype = invoice_clicked.attr("data-invoice-doctype");
const invoice_name = unescape(invoice_clicked.attr("data-invoice-name"));
$(".invoice-wrapper").removeClass("invoice-selected");
invoice_clicked.addClass("invoice-selected");
me.events.open_invoice_data(invoice_doctype, invoice_name);
});
@@ -103,16 +109,16 @@ erpnext.PointOfSale.PastOrderList = class {
return `<div class="invoice-wrapper" data-invoice-doctype="${
invoice.doctype
}" data-invoice-name="${escape(invoice.name)}">
<div class="invoice-name-date">
<div class="invoice-name">${invoice.name}</div>
<div class="invoice-date">
<div class="invoice-name-customer">
<div class="invoice-customer">
<svg class="mr-2" width="12" height="12" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>
</svg>
${frappe.ellipsis(invoice.customer, 20)}
</div>
<div class="invoice-name">${invoice.name}</div>
</div>
<div class="invoice-total-status">
<div class="invoice-total-date">
<div class="invoice-total">${format_currency(invoice.grand_total, invoice.currency) || 0}</div>
<div class="invoice-date">${posting_datetime}</div>
</div>

View File

@@ -459,6 +459,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
}
toggle_component(show) {
this.$component.css("grid-column", "span 6 / span 6");
show ? this.$component.css("display", "flex") : this.$component.css("display", "none");
}

View File

@@ -61,9 +61,15 @@ erpnext.PointOfSale.Payment = class {
primary_action_label: __("Save"),
primary_action(values) {
me.set_values_to_frm(values);
if (this.complete_order) {
me.events.submit_invoice();
}
this.hide();
},
});
me.addl_dlg.$wrapper.on("hide.bs.modal", function () {
me.addl_dlg.complete_order = false;
});
me.add_btn_field_click_listener();
me.set_value_on_dialog_fields();
me.make_addl_info_dialog_btn_visible();
@@ -218,10 +224,6 @@ erpnext.PointOfSale.Payment = class {
const paid_amount = doc.paid_amount;
const items = doc.items;
if (!this.validate_reqd_invoice_fields()) {
return;
}
if (!items.length || (paid_amount == 0 && doc.additional_discount_percentage != 100)) {
const message = items.length
? __("You cannot submit the order without payment.")
@@ -231,6 +233,10 @@ erpnext.PointOfSale.Payment = class {
return;
}
if (!this.validate_reqd_invoice_fields()) {
return;
}
this.events.submit_invoice();
});
@@ -683,16 +689,12 @@ erpnext.PointOfSale.Payment = class {
}
validate_reqd_invoice_fields() {
if (this.invoice_fields.length === 0) return true;
const doc = this.events.get_frm().doc;
for (const df of this.addl_dlg.fields) {
if (df.reqd && !doc[df.fieldname]) {
frappe.show_alert({
message: __(
"Invoice cannot be submitted without filling the mandatory Additional Information fields."
),
indicator: "red",
});
frappe.utils.play_sound("error");
this.addl_dlg.primary_action_label = "Submit";
this.addl_dlg.complete_order = true;
this.addl_dlg.show();
this.addl_dlg.fields_dict[df.fieldname].$input.focus();
return false;