From 9e50f2e223ec7514351b5d35ef324632748fa0cb Mon Sep 17 00:00:00 2001 From: Diptanil Saha Date: Fri, 24 Jan 2025 12:41:01 +0530 Subject: [PATCH] fix: resolved pos return setting to default mode of payment instead of user selection (#45377) * fix: resolved pos return setting to default mode of payment instead of user selection * refactor: removed console log statement * refactor: moved get_payment_data to sales_and_purchase_return.py (cherry picked from commit 54d234e05de8e28da20189a6cb50d143d12361b1) # Conflicts: # erpnext/controllers/sales_and_purchase_return.py --- .../controllers/sales_and_purchase_return.py | 316 ++++++++++++++++++ .../public/js/controllers/taxes_and_totals.js | 41 ++- 2 files changed, 356 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index b2a4a4e0f7e..dfd03c40001 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -669,3 +669,319 @@ def get_returned_serial_nos(child_doc, parent_doc, serial_no_field="serial_no"): serial_nos.extend(get_serial_nos(row.get(serial_no_field))) return serial_nos +<<<<<<< HEAD +======= + + +def get_returned_batches(child_doc, parent_doc, batch_no_field=None, ignore_voucher_detail_no=None): + from erpnext.stock.serial_batch_bundle import get_batches_from_bundle + + batches = frappe._dict() + + old_field = "batch_no" + if not batch_no_field: + batch_no_field = "serial_and_batch_bundle" + + return_ref_field = frappe.scrub(child_doc.doctype) + if child_doc.doctype == "Delivery Note Item": + return_ref_field = "dn_detail" + + fields = [ + f"`{'tab' + child_doc.doctype}`.`{batch_no_field}`", + f"`{'tab' + child_doc.doctype}`.`batch_no`", + f"`{'tab' + child_doc.doctype}`.`stock_qty`", + ] + + filters = [ + [parent_doc.doctype, "return_against", "=", parent_doc.name], + [parent_doc.doctype, "is_return", "=", 1], + [child_doc.doctype, return_ref_field, "=", child_doc.name], + [parent_doc.doctype, "docstatus", "=", 1], + ] + + if batch_no_field == "rejected_serial_and_batch_bundle": + filters.append([child_doc.doctype, "rejected_qty", ">", 0]) + + # Required for POS Invoice + if ignore_voucher_detail_no: + filters.append([child_doc.doctype, "name", "!=", ignore_voucher_detail_no]) + + ids = [] + for row in frappe.get_all(parent_doc.doctype, fields=fields, filters=filters): + ids.append(row.get("serial_and_batch_bundle")) + if row.get(old_field) and not row.get(batch_no_field): + batches.setdefault(row.get(old_field), row.get("stock_qty")) + + if ids: + batches.update(get_batches_from_bundle(ids)) + + return batches + + +def available_serial_batch_for_return(field, doctype, reference_ids, is_rejected=False): + available_dict = get_available_serial_batches(field, doctype, reference_ids, is_rejected=is_rejected) + if not available_dict: + frappe.throw(_("No Serial / Batches are available for return")) + + return available_dict + + +def get_available_serial_batches(field, doctype, reference_ids, is_rejected=False): + _bundle_ids = get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=is_rejected) + if not _bundle_ids: + return frappe._dict({}) + + return get_serial_batches_based_on_bundle(field, _bundle_ids) + + +def get_serial_batches_based_on_bundle(field, _bundle_ids): + available_dict = frappe._dict({}) + batch_serial_nos = frappe.get_all( + "Serial and Batch Bundle", + fields=[ + "`tabSerial and Batch Entry`.`serial_no`", + "`tabSerial and Batch Entry`.`batch_no`", + "`tabSerial and Batch Entry`.`qty`", + "`tabSerial and Batch Entry`.`incoming_rate`", + "`tabSerial and Batch Bundle`.`voucher_detail_no`", + "`tabSerial and Batch Bundle`.`voucher_type`", + "`tabSerial and Batch Bundle`.`voucher_no`", + ], + filters=[ + ["Serial and Batch Bundle", "name", "in", _bundle_ids], + ["Serial and Batch Entry", "docstatus", "=", 1], + ], + order_by="`tabSerial and Batch Bundle`.`creation`, `tabSerial and Batch Entry`.`idx`", + ) + + for row in batch_serial_nos: + key = row.voucher_detail_no + if frappe.get_cached_value(row.voucher_type, row.voucher_no, "is_return"): + key = frappe.get_cached_value(row.voucher_type + " Item", row.voucher_detail_no, field) + + if row.voucher_type in ["Sales Invoice", "Delivery Note"]: + row.qty = -1 * row.qty + + if key not in available_dict: + available_dict[key] = frappe._dict( + { + "qty": 0.0, + "serial_nos": defaultdict(float), + "batches": defaultdict(float), + "serial_nos_valuation": defaultdict(float), + "batches_valuation": defaultdict(float), + } + ) + + available_dict[key]["qty"] += row.qty + + if row.serial_no: + available_dict[key]["serial_nos"][row.serial_no] += row.qty + available_dict[key]["serial_nos_valuation"][row.serial_no] = row.incoming_rate + elif row.batch_no: + available_dict[key]["batches"][row.batch_no] += row.qty + available_dict[key]["batches_valuation"][row.batch_no] = row.incoming_rate + + return available_dict + + +def get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=False): + filters = {"docstatus": 1, "name": ("in", reference_ids), "serial_and_batch_bundle": ("is", "set")} + + pluck_field = "serial_and_batch_bundle" + if is_rejected: + del filters["serial_and_batch_bundle"] + filters["rejected_serial_and_batch_bundle"] = ("is", "set") + pluck_field = "rejected_serial_and_batch_bundle" + + _bundle_ids = frappe.get_all( + doctype, + filters=filters, + pluck=pluck_field, + ) + + if not _bundle_ids: + return {} + + del filters["name"] + + filters[field] = ("in", reference_ids) + + if not is_rejected: + _bundle_ids.extend( + frappe.get_all( + doctype, + filters=filters, + pluck="serial_and_batch_bundle", + ) + ) + else: + fields = ["serial_and_batch_bundle"] + + if is_rejected: + fields.append("rejected_serial_and_batch_bundle") + + if doctype == "Purchase Receipt Item": + fields.append("return_qty_from_rejected_warehouse") + + del filters["rejected_serial_and_batch_bundle"] + data = frappe.get_all( + doctype, + fields=fields, + filters=filters, + ) + + for d in data: + if not d.get("serial_and_batch_bundle") and not d.get("rejected_serial_and_batch_bundle"): + continue + + if is_rejected: + if d.get("return_qty_from_rejected_warehouse"): + _bundle_ids.append(d.get("serial_and_batch_bundle")) + else: + _bundle_ids.append(d.get("rejected_serial_and_batch_bundle")) + else: + _bundle_ids.append(d.get("serial_and_batch_bundle")) + + return _bundle_ids + + +def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field=None): + if not qty_field: + qty_field = "stock_qty" + + if not warehouse_field: + warehouse_field = "warehouse" + + warehouse = row.get(warehouse_field) + qty = abs(row.get(qty_field)) + + filterd_serial_batch = frappe._dict( + { + "serial_nos": [], + "batches": defaultdict(float), + "serial_nos_valuation": data.get("serial_nos_valuation"), + "batches_valuation": data.get("batches_valuation"), + } + ) + + if data.serial_nos: + available_serial_nos = [] + for serial_no, sn_qty in data.serial_nos.items(): + if sn_qty != 0: + available_serial_nos.append(serial_no) + + if available_serial_nos: + if parent_doc.doctype in ["Purchase Invoice", "Purchase Receipt"]: + available_serial_nos = get_available_serial_nos(available_serial_nos, warehouse) + + if len(available_serial_nos) > qty: + filterd_serial_batch["serial_nos"] = sorted(available_serial_nos[0 : cint(qty)]) + else: + filterd_serial_batch["serial_nos"] = available_serial_nos + + elif data.batches: + for batch_no, batch_qty in data.batches.items(): + if parent_doc.get("is_internal_customer"): + batch_qty = batch_qty * -1 + + if batch_qty <= 0: + continue + + if parent_doc.doctype in ["Purchase Invoice", "Purchase Receipt"]: + batch_qty = get_available_batch_qty( + parent_doc, + batch_no, + warehouse, + ) + + if batch_qty <= 0: + frappe.throw( + _("Batch {0} is not available in warehouse {1}").format(batch_no, warehouse), + title=_("Batch Not Available for Return"), + ) + + if qty <= 0: + break + + if batch_qty > qty: + filterd_serial_batch["batches"][batch_no] = qty + qty = 0 + else: + filterd_serial_batch["batches"][batch_no] += batch_qty + qty -= batch_qty + + return filterd_serial_batch + + +def get_available_batch_qty(parent_doc, batch_no, warehouse): + from erpnext.stock.doctype.batch.batch import get_batch_qty + + return get_batch_qty( + batch_no, + warehouse, + posting_date=parent_doc.posting_date, + posting_time=parent_doc.posting_time, + for_stock_levels=True, + ) + + +def make_serial_batch_bundle_for_return(data, child_doc, parent_doc, warehouse_field=None, qty_field=None): + from erpnext.stock.serial_batch_bundle import SerialBatchCreation + + type_of_transaction = "Outward" + if parent_doc.doctype in ["Sales Invoice", "Delivery Note", "POS Invoice"]: + type_of_transaction = "Inward" + + if not warehouse_field: + warehouse_field = "warehouse" + + if not qty_field: + qty_field = "stock_qty" + + warehouse = child_doc.get(warehouse_field) + if parent_doc.get("is_internal_customer"): + warehouse = child_doc.get("target_warehouse") + type_of_transaction = "Outward" + + if not child_doc.get(qty_field): + frappe.throw( + _("For the {0}, the quantity is required to make the return entry").format( + frappe.bold(child_doc.item_code) + ) + ) + + cls_obj = SerialBatchCreation( + { + "type_of_transaction": type_of_transaction, + "item_code": child_doc.item_code, + "warehouse": warehouse, + "serial_nos": data.get("serial_nos"), + "batches": data.get("batches"), + "serial_nos_valuation": data.get("serial_nos_valuation"), + "batches_valuation": data.get("batches_valuation"), + "posting_date": parent_doc.posting_date, + "posting_time": parent_doc.posting_time, + "voucher_type": parent_doc.doctype, + "voucher_no": parent_doc.name, + "voucher_detail_no": child_doc.name, + "qty": child_doc.get(qty_field), + "company": parent_doc.company, + "do_not_submit": True, + } + ).make_serial_and_batch_bundle() + + return cls_obj.name + + +def get_available_serial_nos(serial_nos, warehouse): + return frappe.get_all( + "Serial No", filters={"warehouse": warehouse, "name": ("in", serial_nos)}, pluck="name" + ) + + +@frappe.whitelist() +def get_payment_data(invoice): + payment = frappe.db.get_all("Sales Invoice Payment", {"parent": invoice}, ["mode_of_payment", "amount"]) + return payment +>>>>>>> 54d234e05d (fix: resolved pos return setting to default mode of payment instead of user selection (#45377)) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 8ca72d55c23..f715aa8836e 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -806,7 +806,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { } } - set_total_amount_to_default_mop() { + async set_total_amount_to_default_mop() { let grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total; let base_grand_total = this.frm.doc.base_rounded_total || this.frm.doc.base_grand_total; @@ -828,6 +828,45 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { ); } + /* + During returns, if an user select mode of payment other than + default mode of payment, it should retain the user selection + instead resetting it to default mode of payment. + */ + + let payment_amount = 0; + this.frm.doc.payments.forEach(payment => { + payment_amount += payment.amount + }); + + if (payment_amount == total_amount_to_pay) { + return; + } + + /* + For partial return, if the payment was made using single mode of payment + it should set the return to that mode of payment only. + */ + + let return_against_mop = await frappe.call({ + method: 'erpnext.controllers.sales_and_purchase_return.get_payment_data', + args: { + invoice: this.frm.doc.return_against + } + }); + + if (return_against_mop.message.length === 1) { + this.frm.doc.payments.forEach(payment => { + if (payment.mode_of_payment == return_against_mop.message[0].mode_of_payment) { + payment.amount = total_amount_to_pay; + } else { + payment.amount = 0; + } + }); + this.frm.refresh_fields(); + return; + } + this.frm.doc.payments.find(payment => { if (payment.default) { payment.amount = total_amount_to_pay;