From c742a1dbe930fe113d3a1d5e721a5c044d55a8de Mon Sep 17 00:00:00 2001 From: Diptanil Saha Date: Sat, 28 Jun 2025 00:48:23 +0530 Subject: [PATCH] feat: partly paid pos invoices (#48246) * fix: partial payment in pos * fix: show alerts for update failure * fix: partial payment validation * fix: remove setting clearance date * fix: partly paid invoices in pos * fix: throw error if user tries to make payment for consolidated invoice * fix: include unpaid invoices in partly paid invoice filter * refactor: function rename * feat: button to open form view for partly paid invoices in pos order summary * fix: payment menu item visible for unpaid invoices * refactor: update_payments function * fix: set outstanding amount for pos invoice * test: partly paid pos invoices * test: removed frappe.db.commit * refactor: using before_submit to set outstanding amount --- .../doctype/pos_invoice/pos_invoice.js | 139 ++++++++++++++++++ .../doctype/pos_invoice/pos_invoice.json | 5 +- .../doctype/pos_invoice/pos_invoice.py | 77 ++++++++++ .../doctype/pos_invoice/pos_invoice_list.js | 2 + .../doctype/pos_invoice/test_pos_invoice.py | 50 +++++++ .../doctype/pos_profile/pos_profile.json | 11 +- .../doctype/pos_profile/pos_profile.py | 1 + .../doctype/sales_invoice/sales_invoice.py | 23 ++- .../page/point_of_sale/point_of_sale.py | 24 +-- .../page/point_of_sale/pos_controller.js | 7 + .../page/point_of_sale/pos_item_cart.js | 1 + .../page/point_of_sale/pos_past_order_list.js | 2 +- .../point_of_sale/pos_past_order_summary.js | 18 ++- .../selling/page/point_of_sale/pos_payment.js | 8 +- 14 files changed, 338 insertions(+), 30 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js index 3a38432ad53..17024e249c1 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js @@ -66,6 +66,13 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex if (doc.docstatus == 1 && !doc.is_return) { this.frm.add_custom_button(__("Return"), this.make_sales_return.bind(this), __("Create")); + if (["Partly Paid", "Overdue", "Unpaid"].includes(doc.status)) { + this.frm.add_custom_button( + __("Payment"), + this.collect_outstanding_payment.bind(this), + __("Create") + ); + } this.frm.page.set_inner_btn_group_as_primary(__("Create")); } @@ -210,6 +217,138 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex frm: this.frm, }); } + + async collect_outstanding_payment() { + const total_amount = flt(this.frm.doc.rounded_total) | flt(this.frm.doc.grand_total); + const paid_amount = flt(this.frm.doc.paid_amount); + const outstanding_amount = flt(this.frm.doc.outstanding_amount); + const me = this; + + const table_fields = [ + { + fieldname: "mode_of_payment", + fieldtype: "Link", + in_list_view: 1, + label: __("Mode of Payment"), + options: "Mode of Payment", + reqd: 1, + }, + { + fieldname: "amount", + fieldtype: "Currency", + in_list_view: 1, + label: __("Amount"), + options: this.frm.doc.currency, + reqd: 1, + onchange: function () { + dialog.fields_dict.payments.df.data.some((d) => { + if (d.idx == this.doc.idx) { + d.amount = this.value === null ? 0 : this.value; + dialog.fields_dict.payments.grid.refresh(); + return true; + } + }); + + let amount = 0; + for (let d of dialog.fields_dict.payments.df.data) { + amount += d.amount; + } + + let change_amount = total_amount - (paid_amount + amount); + + dialog.fields_dict.outstanding_amount.set_value( + outstanding_amount - amount < 0 ? 0 : outstanding_amount - amount + ); + dialog.fields_dict.paid_amount.set_value(paid_amount + amount); + dialog.fields_dict.change_amount.set_value(change_amount < 0 ? change_amount * -1 : 0); + }, + }, + ]; + const payment_method_data = await this.fetch_pos_payment_methods(); + const dialog = new frappe.ui.Dialog({ + title: __("Collect Outstanding Amount"), + fields: [ + { + fieldname: "payments", + fieldtype: "Table", + label: __("Payments"), + cannot_add_rows: false, + in_place_edit: true, + reqd: 1, + data: payment_method_data, + fields: table_fields, + }, + { + fieldname: "section_break_1", + fieldtype: "Section Break", + }, + { + fieldname: "outstanding_amount", + fieldtype: "Currency", + label: __("Outstanding Amount"), + read_only: 1, + default: outstanding_amount, + }, + { + fieldname: "column_break_1", + fieldtype: "Column Break", + }, + { + fieldname: "paid_amount", + fieldtype: "Currency", + label: __("Paid Amount"), + read_only: 1, + default: paid_amount, + }, + { + fieldname: "change_amount", + fieldtype: "Currency", + label: __("Change Amount"), + read_only: 1, + default: 0, + }, + ], + primary_action_label: __("Submit"), + primary_action(values) { + dialog.hide(); + me.frm.call({ + doc: me.frm.doc, + method: "update_payments", + args: { + payments: values.payments.filter((d) => d.amount != 0), + }, + freeze: true, + callback: function (r) { + if (!r.exc) { + frappe.show_alert({ + message: __("Payments updated."), + indicator: "green", + }); + me.frm.reload_doc(); + } else { + frappe.show_alert({ + message: __("Payments could not be updated."), + indicator: "red", + }); + } + }, + }); + }, + }); + dialog.show(); + } + + async fetch_pos_payment_methods() { + const pos_profile = this.frm.doc.pos_profile; + if (!pos_profile) return; + const pos_profile_doc = await frappe.db.get_doc("POS Profile", pos_profile); + const data = []; + pos_profile_doc.payments.forEach((pay) => { + const { mode_of_payment } = pay; + data.push({ mode_of_payment, amount: 0 }); + }); + return data; + } }; extend_cscript(cur_frm.cscript, new erpnext.selling.POSInvoiceController({ frm: cur_frm })); diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json index 684b0b0ff49..5750d51fdff 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json @@ -1330,7 +1330,7 @@ "in_standard_filter": 1, "label": "Status", "no_copy": 1, - "options": "\nDraft\nReturn\nCredit Note Issued\nConsolidated\nSubmitted\nPaid\nUnpaid\nUnpaid and Discounted\nOverdue and Discounted\nOverdue\nCancelled", + "options": "\nDraft\nReturn\nCredit Note Issued\nConsolidated\nSubmitted\nPaid\nPartly Paid\nUnpaid\nPartly Paid and Discounted\nUnpaid and Discounted\nOverdue and Discounted\nOverdue\nCancelled", "print_hide": 1, "read_only": 1 }, @@ -1573,7 +1573,7 @@ "icon": "fa fa-file-text", "is_submittable": 1, "links": [], - "modified": "2025-01-06 15:03:19.957277", + "modified": "2025-06-24 12:13:28.242649", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice", @@ -1618,6 +1618,7 @@ "role": "All" } ], + "row_format": "Dynamic", "search_fields": "posting_date, due_date, customer, base_grand_total, outstanding_amount", "show_name_in_global_search": 1, "sort_field": "creation", diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 195bda08151..f8516d6932d 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -149,7 +149,9 @@ class POSInvoice(SalesInvoice): "Consolidated", "Submitted", "Paid", + "Partly Paid", "Unpaid", + "Partly Paid and Discounted", "Unpaid and Discounted", "Overdue and Discounted", "Overdue", @@ -220,6 +222,9 @@ class POSInvoice(SalesInvoice): validate_coupon_code(self.coupon_code) + def before_submit(self): + self.set_outstanding_amount() + def on_submit(self): # create the loyalty point ledger entry if the customer is enrolled in any loyalty program if not self.is_return and self.loyalty_program: @@ -525,6 +530,10 @@ class POSInvoice(SalesInvoice): ) ) + def set_outstanding_amount(self): + total = flt(self.rounded_total) or flt(self.grand_total) + self.outstanding_amount = total - flt(self.paid_amount) if total > flt(self.paid_amount) else 0 + def validate_loyalty_transaction(self): if self.redeem_loyalty_points and ( not self.loyalty_redemption_account or not self.loyalty_redemption_cost_center @@ -546,6 +555,8 @@ class POSInvoice(SalesInvoice): self.status = "Draft" return + total = flt(self.rounded_total) or flt(self.grand_total) + if not status: if self.docstatus == 2: status = "Cancelled" @@ -561,6 +572,14 @@ class POSInvoice(SalesInvoice): self.status = "Overdue and Discounted" elif flt(self.outstanding_amount) > 0 and getdate(self.due_date) < getdate(nowdate()): self.status = "Overdue" + elif ( + 0 < flt(self.outstanding_amount) < total + and self.is_discounted + and self.get_discounting_status() == "Disbursed" + ): + self.status = "Partly Paid and Discounted" + elif 0 < flt(self.outstanding_amount) < total: + self.status = "Partly Paid" elif ( flt(self.outstanding_amount) > 0 and getdate(self.due_date) >= getdate(nowdate()) @@ -781,6 +800,48 @@ class POSInvoice(SalesInvoice): if pr: return frappe.get_doc("Payment Request", pr) + @frappe.whitelist() + def update_payments(self, payments): + if self.status == "Consolidated": + frappe.throw(_("Create Payment Entry for Consolidated POS Invoices.")) + + paid_amount = flt(self.paid_amount) + total = flt(self.rounded_total) or flt(self.grand_total) + + if paid_amount >= total: + frappe.throw(title=_("Invoice Paid"), msg=_("This invoice has already been paid.")) + + idx = self.payments[-1].idx if self.payments else -1 + + for d in payments: + idx += 1 + payment = create_payments_on_invoice(self, idx, frappe._dict(d)) + paid_amount += flt(payment.amount) + payment.submit() + + paid_amount = flt(flt(paid_amount), self.precision("paid_amount")) + base_paid_amount = flt(flt(paid_amount * self.conversion_rate), self.precision("base_paid_amount")) + outstanding_amount = ( + flt(flt(total - paid_amount), self.precision("outstanding_amount")) if total > paid_amount else 0 + ) + change_amount = ( + flt(flt(paid_amount - total), self.precision("change_amount")) if paid_amount > total else 0 + ) + + pi = frappe.qb.DocType("POS Invoice") + query = ( + frappe.qb.update(pi) + .set(pi.paid_amount, paid_amount) + .set(pi.base_paid_amount, base_paid_amount) + .set(pi.outstanding_amount, outstanding_amount) + .set(pi.change_amount, change_amount) + .where(pi.name == self.name) + ) + query.run() + self.reload() + + self.set_status(update=True) + @frappe.whitelist() def get_stock_availability(item_code, warehouse): @@ -932,3 +993,19 @@ def get_item_group(pos_profile): item_groups.extend(get_descendants_of("Item Group", row.item_group)) return list(set(item_groups)) + + +def create_payments_on_invoice(doc, idx, payment_details): + from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_bank_cash_account + + payment = frappe.new_doc("Sales Invoice Payment") + payment.idx = idx + payment.mode_of_payment = payment_details.mode_of_payment + payment.amount = payment_details.amount + payment.base_amount = payment.amount * doc.conversion_rate + payment.parent = doc.name + payment.parentfield = "payments" + payment.parenttype = doc.doctype + payment.account = get_bank_cash_account(payment.mode_of_payment, doc.company).get("account") + + return payment diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice_list.js b/erpnext/accounts/doctype/pos_invoice/pos_invoice_list.js index 0379932bb7a..3778e3dc0a2 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice_list.js +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice_list.js @@ -18,11 +18,13 @@ frappe.listview_settings["POS Invoice"] = { Draft: "red", Unpaid: "orange", Paid: "green", + "Partly Paid": "yellow", Submitted: "blue", Consolidated: "green", Return: "darkgrey", "Unpaid and Discounted": "orange", "Overdue and Discounted": "red", + "Partly Paid and Discounted": "yellow", Overdue: "red", }; return [__(doc.status), status_color[doc.status], "status,=," + doc.status]; diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index 73cb6634b91..b9c479b012c 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -401,6 +401,50 @@ class TestPOSInvoice(IntegrationTestCase): pos_inv.insert() self.assertRaises(PartialPaymentValidationError, pos_inv.submit) + def test_partly_paid_invoices(self): + set_allow_partial_payment(self.pos_profile, 1) + + pos_inv = create_pos_invoice(pos_profile=self.pos_profile.name, rate=100, do_not_save=1) + pos_inv.append( + "payments", + {"mode_of_payment": "Cash", "amount": 90}, + ) + pos_inv.save() + pos_inv.submit() + + self.assertEqual(pos_inv.paid_amount, 90) + self.assertEqual(pos_inv.status, "Partly Paid") + + pos_inv.update_payments(payments=[{"mode_of_payment": "Cash", "amount": 10}]) + self.assertEqual(pos_inv.paid_amount, 100) + self.assertEqual(pos_inv.status, "Paid") + + set_allow_partial_payment(self.pos_profile, 0) + + def test_multi_payment_for_partly_paid_invoices(self): + set_allow_partial_payment(self.pos_profile, 1) + + pos_inv = create_pos_invoice(pos_profile=self.pos_profile.name, rate=100, do_not_save=1) + pos_inv.append( + "payments", + {"mode_of_payment": "Cash", "amount": 90}, + ) + pos_inv.save() + pos_inv.submit() + + self.assertEqual(pos_inv.paid_amount, 90) + self.assertEqual(pos_inv.status, "Partly Paid") + + pos_inv.update_payments(payments=[{"mode_of_payment": "Cash", "amount": 5}]) + self.assertEqual(pos_inv.paid_amount, 95) + self.assertEqual(pos_inv.status, "Partly Paid") + + pos_inv.update_payments(payments=[{"mode_of_payment": "Cash", "amount": 5}]) + self.assertEqual(pos_inv.paid_amount, 100) + self.assertEqual(pos_inv.status, "Paid") + + set_allow_partial_payment(self.pos_profile, 0) + def test_serialized_item_transaction(self): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item @@ -1094,3 +1138,9 @@ def make_batch_item(item_name): if not frappe.db.exists(item_name): return make_item(item_name, dict(has_batch_no=1, create_new_batch=1, is_stock_item=1)) + + +def set_allow_partial_payment(pos_profile, value): + pos_profile.reload() + pos_profile.allow_partial_payment = value + pos_profile.save() diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.json b/erpnext/accounts/doctype/pos_profile/pos_profile.json index 4e37791e078..c0e5c895403 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.json +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.json @@ -26,13 +26,14 @@ "auto_add_item_to_cart", "validate_stock_on_save", "print_receipt_on_order_complete", + "action_on_new_invoice", "column_break_16", "update_stock", "ignore_pricing_rule", "allow_rate_change", "allow_discount_change", "set_grand_total_to_default_mop", - "action_on_new_invoice", + "allow_partial_payment", "section_break_23", "item_groups", "column_break_25", @@ -423,6 +424,12 @@ "fieldtype": "Select", "label": "Action on New Invoice", "options": "Always Ask\nSave Changes and Load New Invoice\nDiscard Changes and Load New Invoice" + }, + { + "default": "0", + "fieldname": "allow_partial_payment", + "fieldtype": "Check", + "label": "Allow Partial Payment" } ], "grid_page_length": 50, @@ -451,7 +458,7 @@ "link_fieldname": "pos_profile" } ], - "modified": "2025-05-23 12:12:32.247652", + "modified": "2025-06-24 11:19:19.834905", "modified_by": "Administrator", "module": "Accounts", "name": "POS Profile", diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.py b/erpnext/accounts/doctype/pos_profile/pos_profile.py index e3e5c84d3d9..6f96137274d 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.py +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.py @@ -32,6 +32,7 @@ class POSProfile(Document): "Always Ask", "Save Changes and Load New Invoice", "Discard Changes and Load New Invoice" ] allow_discount_change: DF.Check + allow_partial_payment: DF.Check allow_rate_change: DF.Check applicable_for_users: DF.Table[POSProfileUser] apply_discount_on: DF.Literal["Grand Total", "Net Total"] diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 9e3c1c58aec..46e3413a656 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1104,20 +1104,19 @@ class SalesInvoice(SellingController): self.validate_pos_opening_entry() def validate_full_payment(self): + allow_partial_payment = frappe.db.get_value("POS Profile", self.pos_profile, "allow_partial_payment") invoice_total = flt(self.rounded_total) or flt(self.grand_total) - if self.docstatus == 1: - if self.is_return and self.paid_amount != invoice_total: - frappe.throw( - msg=_("Partial Payment in POS Transactions are not allowed."), - exc=PartialPaymentValidationError, - ) - - if self.paid_amount < invoice_total: - frappe.throw( - msg=_("Partial Payment in POS Transactions are not allowed."), - exc=PartialPaymentValidationError, - ) + if ( + self.docstatus == 1 + and not self.is_return + and not allow_partial_payment + and self.paid_amount < invoice_total + ): + frappe.throw( + msg=_("Partial Payment in POS Transactions are not allowed."), + exc=PartialPaymentValidationError, + ) def validate_pos_opening_entry(self): opening_entries = frappe.get_all( diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index e9f8826865b..ecf22bb6a45 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -477,22 +477,28 @@ def get_invoice_filters(doctype, status, name=None, customer=None): if doctype == "POS Invoice": filters["status"] = status + if status == "Partly Paid": + filters["status"] = ["in", ["Partly Paid", "Overdue", "Unpaid"]] return filters if doctype == "Sales Invoice": filters["is_created_using_pos"] = 1 filters["is_consolidated"] = 0 - if status == "Draft": - filters["docstatus"] = 0 + if status == "Consolidated": + filters["pos_closing_entry"] = ["is", "set"] else: - filters["docstatus"] = 1 - if status == "Paid": - filters["is_return"] = 0 - if status == "Return": - filters["is_return"] = 1 - - filters["pos_closing_entry"] = ["is", "set"] if status == "Consolidated" else ["is", "not set"] + filters["pos_closing_entry"] = ["is", "not set"] + if status == "Draft": + filters["docstatus"] = 0 + elif status == "Partly Paid": + filters["status"] = ["in", ["Partly Paid", "Overdue", "Unpaid"]] + else: + filters["docstatus"] = 1 + if status == "Paid": + filters["is_return"] = 0 + if status == "Return": + filters["is_return"] = 1 return filters diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index 5166c895367..3896fd2ed3e 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -561,6 +561,13 @@ erpnext.PointOfSale.Controller = class { () => frappe.dom.unfreeze(), ]); }, + open_in_form_view: (doctype, name) => { + frappe.run_serially([ + () => frappe.dom.freeze(), + () => frappe.set_route("Form", doctype, name), + () => frappe.dom.unfreeze(), + ]); + }, }, }); } diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index 40c364355a9..d15c5080081 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -1042,6 +1042,7 @@ erpnext.PointOfSale.ItemCart = class { "Credit Note Issued": "gray", "Partly Paid": "yellow", Overdue: "yellow", + Unpaid: "red", }; transaction_container.append( diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_list.js b/erpnext/selling/page/point_of_sale/pos_past_order_list.js index 08c34c95786..735593c2cb8 100644 --- a/erpnext/selling/page/point_of_sale/pos_past_order_list.js +++ b/erpnext/selling/page/point_of_sale/pos_past_order_list.js @@ -66,7 +66,7 @@ erpnext.PointOfSale.PastOrderList = class { df: { label: __("Invoice Status"), fieldtype: "Select", - options: `Draft\nPaid\nConsolidated\nReturn`, + options: ["Draft", "Paid", "Consolidated", "Return", "Partly Paid"].join("\n"), placeholder: __("Filter by invoice status"), onchange: function () { if (me.$component.is(":visible")) me.refresh_list(); diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js index c26b53e5144..aff2092879e 100644 --- a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js +++ b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js @@ -75,8 +75,9 @@ erpnext.PointOfSale.PastOrderSummary = class { let indicator_color = ""; ["Paid", "Consolidated"].includes(status) && (indicator_color = "green"); - status === "Draft" && (indicator_color = "red"); - status === "Return" && (indicator_color = "grey"); + ["Partly Paid", "Overdue"].includes(status) && (indicator_color = "yellow"); + ["Draft", "Unpaid"].includes(status) && (indicator_color = "red"); + ["Credit Note Issued", "Return"].includes(status) && (indicator_color = "grey"); return `
${doc.customer}
@@ -243,6 +244,10 @@ erpnext.PointOfSale.PastOrderSummary = class { this.$summary_container.on("click", ".print-btn", () => { this.print_receipt(); }); + + this.$summary_container.on("click", ".open-btn", () => { + this.events.open_in_form_view(this.doc.doctype, this.doc.name); + }); } print_receipt() { @@ -361,7 +366,14 @@ erpnext.PointOfSale.PastOrderSummary = class { return [ { condition: this.doc.docstatus === 0, visible_btns: ["Edit Order", "Delete Order"] }, { - condition: !this.doc.is_return && this.doc.docstatus === 1, + condition: ["Partly Paid", "Overdue", "Unpaid"].includes(this.doc.status), + visible_btns: ["Print Receipt", "Email Receipt", "Open in Form View"], + }, + { + condition: + !this.doc.is_return && + this.doc.docstatus === 1 && + !["Partly Paid", "Overdue", "Unpaid"].includes(this.doc.status), visible_btns: ["Print Receipt", "Email Receipt", "Return"], }, { diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index b4851586557..5c38171fa6b 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -5,6 +5,7 @@ erpnext.PointOfSale.Payment = class { this.events = events; this.set_gt_to_default_mop = settings.set_grand_total_to_default_mop; this.invoice_fields = settings.invoice_fields; + this.allow_partial_payment = settings.allow_partial_payment; this.init_component(); } @@ -224,7 +225,12 @@ erpnext.PointOfSale.Payment = class { const paid_amount = doc.paid_amount; const items = doc.items; - if (!items.length || (paid_amount == 0 && doc.additional_discount_percentage != 100)) { + if ( + !items.length || + (paid_amount == 0 && + doc.additional_discount_percentage != 100 && + this.allow_partial_payment === 0) + ) { const message = items.length ? __("You cannot submit the order without payment.") : __("You cannot submit empty order.");