From 5506b44b6f9bbbc6287bfdc0a7d5e1f642e7cc29 Mon Sep 17 00:00:00 2001 From: Diptanil Saha Date: Mon, 17 Feb 2025 15:00:56 +0530 Subject: [PATCH] fix: improve pos return (#45671) * fix: pos return validation for zero qty item * fix: pos return invoice onload ui * feat: added item qty returned in pos ui * refactor: removed console log statement * feat: check return can be made before loading it on pos * fix: pos edit invoice onload ui * fix: returned * refactor: code cleanup --- erpnext/controllers/accounts_controller.py | 2 +- .../controllers/sales_and_purchase_return.py | 29 ++++++++- erpnext/public/scss/point-of-sale.scss | 65 ++++++++++++------- .../page/point_of_sale/pos_controller.js | 4 ++ .../point_of_sale/pos_past_order_summary.js | 60 ++++++++++++++--- 5 files changed, 128 insertions(+), 32 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index f117ef009e6..93489231cf0 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -173,7 +173,7 @@ class AccountsController(TransactionBase): self.validate_qty_is_not_zero() if ( - self.doctype in ["Sales Invoice", "Purchase Invoice"] + self.doctype in ["Sales Invoice", "Purchase Invoice", "POS Invoice"] and self.get("is_return") and self.get("update_stock") ): diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 24767d97b86..ebbd381eb27 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -258,7 +258,7 @@ def get_already_returned_items(doc): field = ( frappe.scrub(doc.doctype) + "_item" - if doc.doctype in ["Purchase Invoice", "Purchase Receipt", "Sales Invoice"] + if doc.doctype in ["Purchase Invoice", "Purchase Receipt", "Sales Invoice", "POS Invoice"] else "dn_detail" ) data = frappe.db.sql( @@ -770,6 +770,7 @@ def get_return_against_item_fields(voucher_type): "Delivery Note": "dn_detail", "Sales Invoice": "sales_invoice_item", "Subcontracting Receipt": "subcontracting_receipt_item", + "POS Invoice": "sales_invoice_item", } return return_against_item_fields[voucher_type] @@ -1162,3 +1163,29 @@ def get_available_serial_nos(serial_nos, warehouse): def get_payment_data(invoice): payment = frappe.db.get_all("Sales Invoice Payment", {"parent": invoice}, ["mode_of_payment", "amount"]) return payment + + +@frappe.whitelist() +def get_pos_invoice_item_returned_qty(pos_invoice, customer, item_row_name): + is_return, docstatus = frappe.db.get_value("POS Invoice", pos_invoice, ["is_return", "docstatus"]) + if not is_return and docstatus == 1: + return get_returned_qty_map_for_row(pos_invoice, customer, item_row_name, "POS Invoice") + + +@frappe.whitelist() +def is_pos_invoice_returnable(pos_invoice): + is_return, docstatus, customer = frappe.db.get_value( + "POS Invoice", pos_invoice, ["is_return", "docstatus", "customer"] + ) + if is_return or docstatus == 0: + return False + + invoice_item_qty = frappe.db.get_all("POS Invoice Item", {"parent": pos_invoice}, ["name", "qty"]) + + already_full_returned = 0 + for d in invoice_item_qty: + returned_qty = get_returned_qty_map_for_row(pos_invoice, customer, d.name, "POS Invoice") + if returned_qty.qty == d.qty: + already_full_returned += 1 + + return len(invoice_item_qty) != already_full_returned diff --git a/erpnext/public/scss/point-of-sale.scss b/erpnext/public/scss/point-of-sale.scss index 1ed34a25730..b0990295105 100644 --- a/erpnext/public/scss/point-of-sale.scss +++ b/erpnext/public/scss/point-of-sale.scss @@ -1087,34 +1087,49 @@ > .item-row-wrapper { display: flex; - align-items: center; + gap: 2px; + flex-direction: column; padding: var(--padding-sm) var(--padding-md); + border: 1px solid lightgray; + border-radius: 10px; + background: var(--bg-light-gray); - > .item-name { - @extend .nowrap; - font-weight: 500; - margin-right: var(--margin-md); - } - - > .item-qty { - font-weight: 500; - margin-left: auto; - } - - > .item-rate-disc { + > .item-row-data { display: flex; - text-align: right; - margin-left: var(--margin-md); - justify-content: flex-end; + align-items: center; - > .item-disc { - color: var(--dark-green-500); - } - - > .item-rate { + > .item-name { + @extend .nowrap; font-weight: 500; - margin-left: var(--margin-md); + margin-right: var(--margin-md); } + + > .item-qty { + font-weight: 500; + margin-left: auto; + font-size: small; + } + + > .item-rate-disc { + display: flex; + text-align: right; + margin-left: var(--margin-md); + justify-content: flex-end; + font-size: small; + + > .item-disc { + color: var(--dark-green-500); + } + + > .item-rate { + font-weight: 500; + margin-left: var(--margin-md); + } + } + } + + > .item-row-refund { + font-size: x-small; } } @@ -1127,6 +1142,12 @@ } } + > .order-summary-container { + display: flex; + background: white; + gap: 8px; + } + > .summary-btns { display: flex; justify-content: space-between; diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index 3c1a72d680f..644e4faa156 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -459,6 +459,8 @@ erpnext.PointOfSale.Controller = class { () => this.make_return_invoice(doc), () => this.cart.load_invoice(), () => this.item_selector.toggle_component(true), + () => this.item_selector.resize_selector(false), + () => this.item_details.toggle_component(false), ]); }); }, @@ -469,6 +471,8 @@ erpnext.PointOfSale.Controller = class { () => this.frm.call("reset_mode_of_payments"), () => this.cart.load_invoice(), () => this.item_selector.toggle_component(true), + () => this.item_selector.resize_selector(false), + () => this.item_details.toggle_component(false), ]); }, delete_order: (name) => { 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 d4b5562c218..0aa464f7c6b 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 @@ -24,7 +24,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
${__("Items")}
-
+
${__("Totals")}
${__("Payments")}
@@ -90,12 +90,18 @@ erpnext.PointOfSale.PastOrderSummary = class {
`; } - get_item_html(doc, item_data) { + async get_item_html(doc, item_data) { + const item_refund_data = doc.is_return || doc.docstatus === 0 ? "" : await get_returned_qty(); + return `
+
${item_data.item_name}
${item_data.qty || 0} ${item_data.uom}
${get_rate_discount_html()}
-
`; +
+ + ${item_refund_data} + `; function get_rate_discount_html() { if (item_data.rate && item_data.price_list_rate && item_data.rate !== item_data.price_list_rate) { @@ -108,6 +114,25 @@ erpnext.PointOfSale.PastOrderSummary = class { )}`; } } + + async function get_returned_qty() { + const r = await frappe.call({ + method: "erpnext.controllers.sales_and_purchase_return.get_pos_invoice_item_returned_qty", + args: { + pos_invoice: doc.name, + customer: doc.customer, + item_row_name: item_data.name, + }, + }); + + if (!r.message.qty) { + return ""; + } + + return `
+ ${r.message.qty} ${__("Returned")} +
`; + } } get_discount_html(doc) { @@ -166,7 +191,16 @@ erpnext.PointOfSale.PastOrderSummary = class { } bind_events() { - this.$summary_container.on("click", ".return-btn", () => { + this.$summary_container.on("click", ".return-btn", async () => { + const r = await this.is_pos_invoice_returnable(this.doc.name); + if (!r) { + frappe.msgprint({ + title: __("Invalid Return"), + indicator: "orange", + message: __("All the items have been already returned."), + }); + return; + } this.events.process_return(this.doc.name); this.toggle_component(false); this.$component.find(".no-summary-placeholder").css("display", "flex"); @@ -370,13 +404,13 @@ erpnext.PointOfSale.PastOrderSummary = class { }); } - attach_items_info(doc) { + async attach_items_info(doc) { this.$items_container.html(""); - doc.items.forEach((item) => { - const item_dom = this.get_item_html(doc, item); + for (const item of doc.items) { + const item_dom = await this.get_item_html(doc, item); this.$items_container.append(item_dom); this.set_dynamic_rate_header_width(); - }); + } } set_dynamic_rate_header_width() { @@ -438,4 +472,14 @@ erpnext.PointOfSale.PastOrderSummary = class { this.print_receipt(); } } + + async is_pos_invoice_returnable(invoice) { + const r = await frappe.call({ + method: "erpnext.controllers.sales_and_purchase_return.is_pos_invoice_returnable", + args: { + pos_invoice: invoice, + }, + }); + return r.message; + } };