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_rate_change",
"allow_discount_change", "allow_discount_change",
"set_grand_total_to_default_mop", "set_grand_total_to_default_mop",
"action_on_new_invoice",
"section_break_23", "section_break_23",
"item_groups", "item_groups",
"column_break_25", "column_break_25",
@@ -415,6 +416,13 @@
"fieldname": "set_grand_total_to_default_mop", "fieldname": "set_grand_total_to_default_mop",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Set Grand Total to Default Payment Method" "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, "grid_page_length": 50,
@@ -443,7 +451,7 @@
"link_fieldname": "pos_profile" "link_fieldname": "pos_profile"
} }
], ],
"modified": "2025-05-09 11:23:28.632136", "modified": "2025-05-23 12:12:32.247652",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "POS Profile", "name": "POS Profile",

View File

@@ -28,6 +28,9 @@ class POSProfile(Document):
from erpnext.accounts.doctype.pos_profile_user.pos_profile_user import POSProfileUser from erpnext.accounts.doctype.pos_profile_user.pos_profile_user import POSProfileUser
account_for_change_amount: DF.Link | None 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_discount_change: DF.Check
allow_rate_change: DF.Check allow_rate_change: DF.Check
applicable_for_users: DF.Table[POSProfileUser] 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" web_include_css = "erpnext-web.bundle.css"
email_css = "email_erpnext.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 = { doctype_js = {
"Address": "public/js/address.js", "Address": "public/js/address.js",
"Communication": "public/js/communication.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; align-items: center;
justify-content: center; justify-content: center;
padding: var(--padding-md); padding: var(--padding-md);
box-shadow: var(--shadow-sm); box-shadow: var(--shadow-base);
&:hover {
background-color: var(--control-bg);
}
} }
> .col-span-2 { > .col-span-2 {
@@ -615,26 +619,30 @@
background-color: var(--control-bg); background-color: var(--control-bg);
} }
> .invoice-name-date { &.invoice-selected {
background-color: var(--control-bg);
}
> .invoice-name-customer {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-around; justify-content: space-around;
> .invoice-name { > .invoice-customer {
@extend .nowrap; @extend .nowrap;
font-size: var(--text-md); font-size: var(--text-md);
display: flex;
align-items: center;
font-weight: 700; font-weight: 700;
} }
> .invoice-date { > .invoice-name {
@extend .nowrap; @extend .nowrap;
font-size: var(--text-sm); font-size: var(--text-sm);
display: flex;
align-items: center;
} }
} }
> .invoice-total-status { > .invoice-total-date {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
font-weight: 500; font-weight: 500;
@@ -648,17 +656,19 @@
text-align: right; text-align: right;
} }
> .invoice-status { > .invoice-date {
display: flex; display: flex;
align-items: center; align-items: center;
color: var(--gray-500);
justify-content: right; justify-content: right;
font-weight: 400;
} }
} }
} }
> .item-details-container { > .item-details-container {
@extend .pos-card; @extend .pos-card;
grid-column: span 4 / span 4; grid-column: span 6 / span 6;
display: none; display: none;
flex-direction: column; flex-direction: column;
padding: var(--padding-lg); padding: var(--padding-lg);
@@ -915,6 +925,10 @@
justify-content: center; justify-content: center;
padding: var(--padding-md); padding: var(--padding-md);
box-shadow: var(--shadow-base); box-shadow: var(--shadow-base);
&:hover {
background-color: var(--control-bg);
}
} }
} }
} }
@@ -978,18 +992,23 @@
background-color: var(--fg-color); background-color: var(--fg-color);
padding: var(--padding-lg); padding: var(--padding-lg);
> .search-field { > .status-search-fields {
width: 100%; display: grid;
display: flex; grid-template-columns: 30% auto;
align-items: center; column-gap: 10px;
margin-top: var(--margin-md); margin-top: var(--margin-md);
margin-bottom: var(--margin-xs);
}
> .status-field { > .status-field {
width: 100%; width: 100%;
display: flex; display: flex;
align-items: center; 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_dom();
this.prepare_components(); this.prepare_components();
this.prepare_menu(); this.prepare_menu();
this.prepare_fullscreen_btn(); this.prepare_btns();
this.make_new_invoice(); this.make_new_invoice();
} }
@@ -234,52 +234,42 @@ erpnext.PointOfSale.Controller = class {
prepare_menu() { prepare_menu() {
this.page.clear_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(__("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"); this.page.add_menu_item(__("Close the POS"), this.close_pos.bind(this), false, "Shift+Ctrl+C");
} }
prepare_fullscreen_btn() { prepare_btns() {
this.page.page_actions.find(".custom-actions").empty(); this.page.clear_custom_actions();
this.page.clear_icons();
this.page.add_button(__("Full Screen"), null, { btn_class: "btn-default fullscreen-btn" }); 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.bind_fullscreen_events(); 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() { bind_fullscreen_events() {
this.$fullscreen_btn = this.page.page_actions.find(".fullscreen-btn"); if (!document.fullscreenElement) {
document.documentElement.requestFullscreen();
this.$fullscreen_btn.on("click", function () { this.toggle_fullscreen_btn(".btn-minimize", ".btn-fullscreen");
if (!document.fullscreenElement) { } else if (document.exitFullscreen) {
document.documentElement.requestFullscreen(); document.exitFullscreen();
} else if (document.exitFullscreen) { this.toggle_fullscreen_btn(".btn-fullscreen", ".btn-minimize");
document.exitFullscreen(); }
}
});
$(document).on("fullscreenchange", this.handle_fullscreen_change_event.bind(this));
} }
handle_fullscreen_change_event() { toggle_fullscreen_btn(show, hide) {
let enable_fullscreen_label = __("Full Screen"); this.page.page_actions.find(hide).addClass("hide");
let exit_fullscreen_label = __("Exit Full Screen"); this.page.page_actions.find(show).removeClass("hide");
if (document.fullscreenElement) {
this.$fullscreen_btn[0].innerText = exit_fullscreen_label;
} else {
this.$fullscreen_btn[0].innerText = enable_fullscreen_label;
}
} }
open_form_view() { open_form_view() {
@@ -289,36 +279,48 @@ erpnext.PointOfSale.Controller = class {
toggle_recent_order() { toggle_recent_order() {
const show = this.recent_order_list.$component.is(":hidden"); 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); this.toggle_recent_order_list(show);
} }
save_draft_invoice() { new_invoice_event() {
const me = this;
if (!this.$components_wrapper.is(":visible")) return; if (!this.$components_wrapper.is(":visible")) return;
if (this.frm.doc.items.length == 0) { if (this.frm.doc.items.length !== 0 && (this.frm.is_new() || this.frm.is_dirty())) {
frappe.show_alert({ if (this.settings.action_on_new_invoice === "Always Ask") {
message: __("You must add atleast one item to save it as draft."), frappe.confirm(
indicator: "red", __("You have unsaved changes. Do you want to save the invoice?"),
}); () => {
frappe.utils.play_sound("error"); 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; return;
} }
this.frm if (this.payment.$component.is(":visible")) {
.save(undefined, undefined, undefined, () => { this.load_new_invoice_on_pos();
frappe.show_alert({ }
message: __("There was an error saving the document."), }
indicator: "red",
}); load_new_invoice_on_pos() {
frappe.utils.play_sound("error"); frappe.run_serially([
}) () => frappe.dom.freeze(),
.then(() => { () => this.make_new_invoice(),
frappe.run_serially([ () => this.toggle_recent_order_list(false),
() => frappe.dom.freeze(), () => this.toggle_components(true),
() => this.make_new_invoice(), () => frappe.dom.unfreeze(),
() => frappe.dom.unfreeze(), ]);
]);
});
} }
close_pos() { close_pos() {
@@ -384,7 +386,7 @@ erpnext.PointOfSale.Controller = class {
get_frm: () => this.frm, get_frm: () => this.frm,
toggle_item_selector: (minimize) => { toggle_item_selector: (minimize) => {
this.item_selector.resize_selector(minimize); this.item_selector.toggle_component(!minimize);
this.cart.toggle_numpad(minimize); this.cart.toggle_numpad(minimize);
}, },
@@ -468,8 +470,7 @@ erpnext.PointOfSale.Controller = class {
submit_invoice: () => { submit_invoice: () => {
this.frm.savesubmit().then((r) => { this.frm.savesubmit().then((r) => {
this.toggle_components(false); this.toggle_components(false);
this.order_summary.toggle_component(true); this.toggle_submitted_invoice_summary(true);
this.order_summary.load_summary_of(this.frm.doc, true);
frappe.show_alert({ frappe.show_alert({
indicator: "green", indicator: "green",
message: __("POS invoice {0} created successfully", [r.doc.name]), message: __("POS invoice {0} created successfully", [r.doc.name]),
@@ -518,7 +519,7 @@ erpnext.PointOfSale.Controller = class {
}); });
}, },
edit_order: (doctype, name) => { edit_order: (doctype, name) => {
this.recent_order_list.toggle_component(false); this.toggle_recent_order();
frappe.run_serially([ frappe.run_serially([
() => this.make_invoice_frm(doctype), () => this.make_invoice_frm(doctype),
() => this.sync_draft_invoice_to_frm(doctype, name), () => this.sync_draft_invoice_to_frm(doctype, name),
@@ -550,19 +551,29 @@ erpnext.PointOfSale.Controller = class {
} }
toggle_recent_order_list(show) { 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.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) { toggle_components(show) {
this.cart.toggle_component(show); this.cart.toggle_component(show);
this.cart.toggle_numpad(!show);
this.cart.toggle_checkout_btn(show);
this.item_selector.toggle_component(show); this.item_selector.toggle_component(show);
// do not show item details or payment if recent order is toggled off // 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) : ""; !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() { make_new_invoice() {
return frappe.run_serially([ return frappe.run_serially([
() => frappe.dom.freeze(), () => frappe.dom.freeze(),

View File

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

View File

@@ -188,7 +188,7 @@ erpnext.PointOfSale.ItemSelector = class {
attach_clear_btn() { attach_clear_btn() {
this.search_field.$wrapper.find(".control-input").append( 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")}"> <a class="btn-open no-decoration" title="${__("Clear")}">
${frappe.utils.icon("close", "sm")} ${frappe.utils.icon("close", "sm")}
</a> </a>

View File

@@ -17,8 +17,10 @@ erpnext.PointOfSale.PastOrderList = class {
`<section class="past-order-list"> `<section class="past-order-list">
<div class="filter-section"> <div class="filter-section">
<div class="label">${__("Recent Orders")}</div> <div class="label">${__("Recent Orders")}</div>
<div class="search-field"></div> <div class="status-search-fields">
<div class="status-field"></div> <div class="status-field"></div>
<div class="search-field"></div>
</div>
</div> </div>
<div class="invoices-container"></div> <div class="invoices-container"></div>
</section>` </section>`
@@ -38,8 +40,12 @@ erpnext.PointOfSale.PastOrderList = class {
}); });
const me = this; const me = this;
this.$invoices_container.on("click", ".invoice-wrapper", function () { this.$invoices_container.on("click", ".invoice-wrapper", function () {
const invoice_doctype = $(this).attr("data-invoice-doctype"); const invoice_clicked = $(this);
const invoice_name = unescape($(this).attr("data-invoice-name")); 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); 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="${ return `<div class="invoice-wrapper" data-invoice-doctype="${
invoice.doctype invoice.doctype
}" data-invoice-name="${escape(invoice.name)}"> }" data-invoice-name="${escape(invoice.name)}">
<div class="invoice-name-date"> <div class="invoice-name-customer">
<div class="invoice-name">${invoice.name}</div> <div class="invoice-customer">
<div class="invoice-date">
<svg class="mr-2" width="12" height="12" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"> <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"/> <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> </svg>
${frappe.ellipsis(invoice.customer, 20)} ${frappe.ellipsis(invoice.customer, 20)}
</div> </div>
<div class="invoice-name">${invoice.name}</div>
</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-total">${format_currency(invoice.grand_total, invoice.currency) || 0}</div>
<div class="invoice-date">${posting_datetime}</div> <div class="invoice-date">${posting_datetime}</div>
</div> </div>

View File

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