fix: pos for small screen and checkout page (#47092)

* feat: Prevent need for scrolling in lower screens (Point of Sale)

* fix: pos checkout page

* refactor: renaming variable in pos profile conf

* fix: change variable name according to pos conf

* fix: removing redundant api call

* refactor: adding function

* refactor: moving pos settings invoice fields to modal

* fix: label for change amount and remaining amount

* fix: always display numpad

* refactor: function rename

* fix: better information

* fix: auto scroll to highlighted cart item

* chore: added patch

---------

Co-authored-by: devdiogenes <diogenes.cruz2001@gmail.com>
This commit is contained in:
Diptanil Saha
2025-05-12 16:07:22 +05:30
committed by GitHub
parent 5c9e591297
commit 62cce38a06
9 changed files with 258 additions and 171 deletions

View File

@@ -31,7 +31,7 @@
"ignore_pricing_rule",
"allow_rate_change",
"allow_discount_change",
"disable_grand_total_to_default_mop",
"set_grand_total_to_default_mop",
"section_break_23",
"item_groups",
"column_break_25",
@@ -402,12 +402,6 @@
"fieldtype": "Check",
"label": "Print Receipt on Order Complete"
},
{
"default": "0",
"fieldname": "disable_grand_total_to_default_mop",
"fieldtype": "Check",
"label": "Disable auto setting Grand Total to default Payment Mode"
},
{
"fieldname": "project",
"fieldtype": "Link",
@@ -415,6 +409,12 @@
"oldfieldname": "cost_center",
"oldfieldtype": "Link",
"options": "Project"
},
{
"default": "1",
"fieldname": "set_grand_total_to_default_mop",
"fieldtype": "Check",
"label": "Set Grand Total to Default Payment Method"
}
],
"grid_page_length": 50,
@@ -443,7 +443,7 @@
"link_fieldname": "pos_profile"
}
],
"modified": "2025-04-09 11:35:13.779613",
"modified": "2025-05-09 11:23:28.632136",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Profile",

View File

@@ -40,7 +40,6 @@ class POSProfile(Document):
currency: DF.Link
customer: DF.Link | None
customer_groups: DF.Table[POSCustomerGroup]
disable_grand_total_to_default_mop: DF.Check
disable_rounded_total: DF.Check
disabled: DF.Check
expense_account: DF.Link | None
@@ -56,6 +55,7 @@ class POSProfile(Document):
project: DF.Link | None
select_print_heading: DF.Link | None
selling_price_list: DF.Link | None
set_grand_total_to_default_mop: DF.Check
tax_category: DF.Link | None
taxes_and_charges: DF.Link | None
tc_name: DF.Link | None

View File

@@ -411,3 +411,4 @@ erpnext.patches.v15_0.update_payment_schedule_fields_in_invoices
erpnext.patches.v15_0.rename_group_by_to_categorize_by
execute:frappe.db.set_single_value("Accounts Settings", "receivable_payable_fetch_method", "Buffered Cursor")
erpnext.patches.v14_0.set_update_price_list_based_on
erpnext.patches.v15_0.set_grand_total_to_default_mop

View File

@@ -0,0 +1,9 @@
import frappe
def execute():
POSProfile = frappe.qb.DocType("POS Profile")
frappe.qb.update(POSProfile).set(POSProfile.set_grand_total_to_default_mop, 1).where(
POSProfile.disable_grand_total_to_default_mop == 0
).run()

View File

@@ -947,9 +947,9 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
var me = this;
var payment_status = true;
if(this.frm.doc.is_pos && (update_paid_amount===undefined || update_paid_amount)) {
let r = await frappe.db.get_value("POS Profile", this.frm.doc.pos_profile, "disable_grand_total_to_default_mop");
let r = await frappe.db.get_value("POS Profile", this.frm.doc.pos_profile, "set_grand_total_to_default_mop");
if (r.message.disable_grand_total_to_default_mop) {
if (!r.message.set_grand_total_to_default_mop) {
return;
}

View File

@@ -5,9 +5,9 @@
padding: 1%;
section {
min-height: 45rem;
height: calc(100vh - 200px);
max-height: calc(100vh - 200px);
min-height: 30rem;
height: calc(100vh - 125px);
max-height: calc(100vh - 125px);
}
.frappe-control {
@@ -375,6 +375,7 @@
flex-direction: column;
flex: 1 1 0%;
overflow-y: scroll;
min-height: 50px;
> .cart-item-wrapper {
@extend .pointer-no-select;
@@ -775,6 +776,7 @@
.submit-order-btn {
@extend .primary-action;
margin-top: 0%;
background-color: var(--btn-primary);
color: var(--neutral);
}
@@ -785,117 +787,136 @@
margin-bottom: var(--margin-md);
}
> .payment-modes {
> .payment-split-container {
display: flex;
padding-bottom: var(--padding-sm);
margin-bottom: var(--margin-sm);
overflow-x: scroll;
overflow-y: hidden;
flex-shrink: 0;
> .payment-mode-wrapper {
min-width: 40%;
padding: var(--padding-xs);
> .payment-container-left {
width: 50%;
margin-bottom: var(--margin-md);
> .mode-of-payment {
@extend .pos-card;
@extend .pointer-no-select;
padding: var(--padding-md) var(--padding-lg);
.payment-modes {
display: flex;
flex-direction: column;
padding-right: var(--padding-sm);
margin-right: var(--margin-sm);
min-height: 15rem;
overflow-y: scroll;
height: calc(100vh - 350px);
> .pay-amount {
display: inline;
float: right;
font-weight: 700;
}
> .payment-mode-wrapper {
min-width: 40%;
padding: var(--padding-xs);
> .mode-of-payment-control {
display: none;
align-items: center;
margin-top: var(--margin-sm);
margin-bottom: var(--margin-xs);
}
> .loyalty-amount-name {
display: none;
float: right;
font-weight: 700;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
> .cash-shortcuts {
display: none;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: var(--margin-sm);
font-size: var(--text-sm);
text-align: center;
> .shortcut {
> .mode-of-payment {
@extend .pos-card;
@extend .pointer-no-select;
border-radius: var(--border-radius-sm);
background-color: var(--control-bg);
font-weight: 500;
padding: var(--padding-xs) var(--padding-sm);
transition: all 0.15s ease-in-out;
padding: var(--padding-md) var(--padding-lg);
&:hover {
background-color: var(--control-bg);
> .pay-amount {
display: inline;
float: right;
font-weight: 700;
}
> .mode-of-payment-control {
display: none;
align-items: center;
margin-top: var(--margin-sm);
margin-bottom: var(--margin-xs);
}
> .loyalty-amount-name {
display: none;
float: right;
font-weight: 700;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
> .cash-shortcuts {
display: none;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: var(--margin-sm);
font-size: var(--text-sm);
text-align: center;
> .shortcut {
@extend .pointer-no-select;
border-radius: var(--border-radius-sm);
background-color: var(--control-bg);
font-weight: 500;
padding: var(--padding-xs) var(--padding-sm);
transition: all 0.15s ease-in-out;
&:hover {
background-color: var(--control-bg);
}
}
}
}
> .loyalty-card {
display: flex;
flex-direction: column;
}
}
}
> .loyalty-card {
display: flex;
flex-direction: column;
}
}
}
> .fields-numpad-container {
display: flex;
flex: 1;
height: 100%;
position: relative;
justify-content: flex-end;
> .fields-section {
flex: 1;
> .payment-container-right {
display: flex;
flex-direction: column;
width: 50%;
height: 100%;
padding-bottom: var(--margin-md);
.invoice-fields {
overflow-y: scroll;
.fields-numpad-container {
display: flex;
flex-direction: column;
flex: 1;
height: 100%;
padding-right: var(--padding-sm);
}
}
position: relative;
justify-content: flex-end;
> .number-pad {
flex: 1;
display: flex;
justify-content: flex-end;
align-items: flex-end;
max-width: 50%;
.numpad-container {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: var(--margin-md);
margin-bottom: var(--margin-md);
> .numpad-btn {
@extend .pointer-no-select;
border-radius: var(--border-radius-md);
> .fields-section {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: var(--padding-md);
box-shadow: var(--shadow-sm);
flex-direction: column;
padding-left: var(--margin-md);
.invoice-fields {
height: 100%;
margin-left: auto;
padding: var(--padding-sm);
}
}
.number-pad {
position: absolute;
z-index: 4;
right: 0px;
flex: 1;
display: flex;
align-items: flex-end;
.numpad-container {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: var(--margin-md);
margin-bottom: var(--margin-md);
background-color: var(--fg-color);
border-radius: var(--border-radius-md);
padding: var(--padding-sm);
> .numpad-btn {
@extend .pointer-no-select;
border-radius: var(--border-radius-md);
display: flex;
align-items: center;
justify-content: center;
padding: var(--padding-md);
box-shadow: var(--shadow-base);
}
}
}
}
}

View File

@@ -156,6 +156,28 @@ erpnext.PointOfSale.Controller = class {
},
});
this.fetch_invoice_fields();
this.setup_listener_for_pos_closing();
}
fetch_invoice_fields() {
const me = this;
frappe.db.get_doc("POS Settings", undefined).then((doc) => {
me.settings.invoice_fields = doc.invoice_fields.map((field) => {
return {
fieldname: field.fieldname,
label: field.label,
fieldtype: field.fieldtype,
reqd: field.reqd,
options: field.options,
default_value: field.default_value,
read_only: field.read_only,
};
});
});
}
setup_listener_for_pos_closing() {
frappe.realtime.on(`poe_${this.pos_opening}_closed`, (data) => {
const route = frappe.get_route_str();
if (data && route == "point-of-sale") {
@@ -426,6 +448,7 @@ erpnext.PointOfSale.Controller = class {
init_payments() {
this.payment = new erpnext.PointOfSale.Payment({
wrapper: this.$components_wrapper,
settings: this.settings,
events: {
get_frm: () => this.frm || {},

View File

@@ -171,6 +171,9 @@ 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 payment_section_hidden = !me.$totals_section.find(".edit-cart-btn").is(":visible");
if (!payment_section_hidden) {
// payment section is visible

View File

@@ -1,8 +1,10 @@
/* eslint-disable no-unused-vars */
erpnext.PointOfSale.Payment = class {
constructor({ events, wrapper }) {
constructor({ events, wrapper, settings }) {
this.wrapper = wrapper;
this.events = events;
this.set_gt_to_default_mop = settings.set_grand_total_to_default_mop;
this.invoice_fields = settings.invoice_fields;
this.init_component();
}
@@ -17,14 +19,23 @@ erpnext.PointOfSale.Payment = class {
prepare_dom() {
this.wrapper.append(
`<section class="payment-container">
<div class="section-label payment-section">${__("Payment Method")}</div>
<div class="payment-modes"></div>
<div class="fields-numpad-container">
<div class="fields-section">
<div class="section-label">${__("Additional Information")}</div>
<div class="invoice-fields"></div>
<div class="payment-split-container">
<div class="payment-container-left">
<div class="section-label payment-section">${__("Payment Method")}</div>
<div class="payment-modes"></div>
</div>
<div class="payment-container-right">
<div class="fields-numpad-container">
<div class="fields-section">
<div class="invoice-fields">
<button class="btn btn-default btn-sm btn-shadow addl-fields hidden">${__(
"Update Additional Information"
)}</button>
</div>
</div>
<div class="number-pad"></div>
</div>
</div>
<div class="number-pad"></div>
</div>
<div class="totals-section">
<div class="totals"></div>
@@ -40,48 +51,61 @@ erpnext.PointOfSale.Payment = class {
this.$invoice_fields_section = this.$component.find(".fields-section");
}
make_invoice_fields_control() {
this.reqd_invoice_fields = [];
frappe.db.get_doc("POS Settings", undefined).then((doc) => {
const fields = doc.invoice_fields;
if (!fields.length) return;
make_invoice_field_dialog() {
const me = this;
if (!me.invoice_fields.length) return;
me.addl_dlg = new frappe.ui.Dialog({
title: __("Additional Information"),
fields: me.invoice_fields,
size: "small",
primary_action_label: __("Save"),
primary_action(values) {
me.set_values_to_frm(values);
this.hide();
},
});
me.add_btn_field_click_listener();
me.set_value_on_dialog_fields();
me.make_addl_info_dialog_btn_visible();
}
this.$invoice_fields = this.$invoice_fields_section.find(".invoice-fields");
this.$invoice_fields.html("");
const frm = this.events.get_frm();
set_values_to_frm(values) {
const frm = this.events.get_frm();
for (const value in values) {
frm.set_value(value, values[value]);
}
frappe.show_alert({
message: __("Additional Information updated successfully."),
indicator: "green",
});
}
fields.forEach((df) => {
this.$invoice_fields.append(
`<div class="invoice_detail_field ${df.fieldname}-field" data-fieldname="${df.fieldname}"></div>`
);
let df_events = {
onchange: function () {
frm.set_value(this.df.fieldname, this.get_value());
},
};
if (df.fieldtype == "Button") {
df_events = {
click: function () {
if (frm.script_manager.has_handlers(df.fieldname, frm.doc.doctype)) {
frm.script_manager.trigger(df.fieldname, frm.doc.doctype, frm.doc.docname);
}
},
};
}
if (df.reqd && (df.fieldtype !== "Button" || !df.read_only)) {
this.reqd_invoice_fields.push({ fieldname: df.fieldname, label: df.label });
}
this[`${df.fieldname}_field`] = frappe.ui.form.make_control({
df: {
...df,
...df_events,
},
parent: this.$invoice_fields.find(`.${df.fieldname}-field`),
render_input: true,
add_btn_field_click_listener() {
const frm = this.events.get_frm();
this.addl_dlg.fields.forEach((df) => {
if (df.fieldtype === "Button") {
this.addl_dlg.fields_dict[df.fieldname].$input.on("click", function () {
if (frm.script_manager.has_handlers(df.fieldname, frm.doc.doctype)) {
frm.script_manager.trigger(df.fieldname, frm.doc.doctype, frm.doc.docname);
}
});
this[`${df.fieldname}_field`].set_value(frm.doc[df.fieldname]);
});
}
});
}
set_value_on_dialog_fields() {
const doc = this.events.get_frm().doc;
this.addl_dlg.fields.forEach((df) => {
if (doc[df.fieldname] || df.default_value) {
this.addl_dlg.set_value(df.fieldname, doc[df.fieldname] || df.default_value);
}
});
}
make_addl_info_dialog_btn_visible() {
this.$invoice_fields_section.find(".addl-fields").removeClass("hidden");
this.$invoice_fields_section.find(".addl-fields").on("click", () => {
this.addl_dlg.show();
});
}
@@ -164,6 +188,16 @@ erpnext.PointOfSale.Payment = class {
}
});
frappe.ui.form.on("POS Invoice", "contact_mobile", (frm) => {
const contact = frm.doc.contact_mobile;
const request_button = $(this.request_for_payment_field?.$input[0]);
if (contact) {
request_button.removeClass("btn-default").addClass("btn-primary");
} else {
request_button.removeClass("btn-primary").addClass("btn-default");
}
});
frappe.ui.form.on("POS Invoice", "coupon_code", (frm) => {
this.bind_coupon_code_event(frm);
});
@@ -355,9 +389,9 @@ erpnext.PointOfSale.Payment = class {
render_payment_section() {
this.render_payment_mode_dom();
this.make_invoice_fields_control();
this.make_invoice_field_dialog();
this.update_totals_section();
this.unset_grand_total_to_default_mop();
this.set_grand_total_to_default_mop();
}
after_render() {
@@ -610,7 +644,7 @@ erpnext.PointOfSale.Payment = class {
const remaining = grand_total - doc.paid_amount;
const change = doc.change_amount || remaining <= 0 ? -1 * remaining : undefined;
const currency = doc.currency;
const label = __("Change Amount");
const label = doc.paid_amount > grand_total ? __("Change Amount") : __("Remaining Amount");
this.$totals.html(
`<div class="col">
@@ -642,32 +676,28 @@ erpnext.PointOfSale.Payment = class {
.toLowerCase();
}
async unset_grand_total_to_default_mop() {
const doc = this.events.get_frm().doc;
let r = await frappe.db.get_value(
"POS Profile",
doc.pos_profile,
"disable_grand_total_to_default_mop"
);
if (!r.message.disable_grand_total_to_default_mop) {
set_grand_total_to_default_mop() {
if (this.set_gt_to_default_mop) {
this.focus_on_default_mop();
}
}
validate_reqd_invoice_fields() {
const doc = this.events.get_frm().doc;
let validation_flag = true;
for (let field of this.reqd_invoice_fields) {
if (!doc[field.fieldname]) {
validation_flag = false;
for (const df of this.addl_dlg.fields) {
if (df.reqd && !doc[df.fieldname]) {
frappe.show_alert({
message: __("{0} is a mandatory field.", [field.label]),
indicator: "orange",
message: __(
"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.fields_dict[df.fieldname].$input.focus();
return false;
}
}
return validation_flag;
return true;
}
};