From ce42d847b3c98f29f82ecc165dfa7a371a916399 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 3 Nov 2024 04:20:20 +0530 Subject: [PATCH] fix: valuation rate for sales / purchase return for serial / batch nos (backport #43925) (#43942) fix: valuation rate for sales / purchase return for serial / batch nos (#43925) (cherry picked from commit 01bb1612da5e28a6b80640db4f0e77107ad00017) Co-authored-by: rohitwaghchaure --- .../controllers/sales_and_purchase_return.py | 37 ++- erpnext/controllers/stock_controller.py | 5 + .../delivery_note/test_delivery_note.py | 262 ++++++++++++++++++ .../purchase_receipt/test_purchase_receipt.py | 228 +++++++++++++++ .../serial_and_batch_bundle.py | 94 ++++++- erpnext/stock/serial_batch_bundle.py | 30 +- 6 files changed, 643 insertions(+), 13 deletions(-) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 6da23834b61..b1ba8e22c2e 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -597,6 +597,10 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai if default_warehouse_for_sales_return: target_doc.warehouse = default_warehouse_for_sales_return + if not source_doc.use_serial_batch_fields and source_doc.serial_and_batch_bundle: + target_doc.serial_no = None + target_doc.batch_no = None + if ( (source_doc.serial_no or source_doc.batch_no) and not source_doc.serial_and_batch_bundle @@ -899,6 +903,7 @@ def get_serial_batches_based_on_bundle(field, _bundle_ids): "`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`", @@ -920,15 +925,23 @@ def get_serial_batches_based_on_bundle(field, _bundle_ids): if key not in available_dict: available_dict[key] = frappe._dict( - {"qty": 0.0, "serial_nos": defaultdict(float), "batches": defaultdict(float)} + { + "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 @@ -964,12 +977,13 @@ def get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=False ) ) else: - fields = [ - "serial_and_batch_bundle", - ] + fields = ["serial_and_batch_bundle"] if is_rejected: - fields.extend(["rejected_serial_and_batch_bundle", "return_qty_from_rejected_warehouse"]) + 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( @@ -1003,7 +1017,14 @@ def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field warehouse = row.get(warehouse_field) qty = abs(row.get(qty_field)) - filterd_serial_batch = frappe._dict({"serial_nos": [], "batches": defaultdict(float)}) + 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 = [] @@ -1013,7 +1034,7 @@ def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field if available_serial_nos: if parent_doc.doctype in ["Purchase Invoice", "Purchase Reecipt"]: - available_serial_nos = get_available_serial_nos(available_serial_nos) + 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)]) @@ -1098,6 +1119,8 @@ def make_serial_batch_bundle_for_return(data, child_doc, parent_doc, warehouse_f "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, diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 0714bdd3a63..046a0c7da30 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -334,6 +334,11 @@ class StockController(AccountsController): } ) + if self.doctype in ["Sales Invoice", "Delivery Note"]: + row.db_set( + "incoming_rate", frappe.db.get_value("Serial and Batch Bundle", bundle, "avg_rate") + ) + def get_reference_ids(self, table_name, qty_field=None, bundle_field=None) -> tuple[str, list[str]]: field = { "Sales Invoice": "sales_invoice_item", diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 0cfb427c670..9acdce8bebc 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -3,6 +3,7 @@ import json +from collections import defaultdict import frappe from frappe.tests.utils import FrappeTestCase @@ -2080,6 +2081,264 @@ class TestDeliveryNote(FrappeTestCase): self.assertEqual(stock_value_difference, 100.0 * 5) + def test_delivery_note_return_valuation_without_use_serial_batch_field(self): + from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return + + batch_item = make_item( + "_Test Delivery Note Return Valuation Batch Item", + properties={ + "has_batch_no": 1, + "create_new_batch": 1, + "is_stock_item": 1, + "batch_number_series": "BRTN-DNN-BI-.#####", + }, + ).name + + serial_item = make_item( + "_Test Delivery Note Return Valuation Serial Item", + properties={"has_serial_no": 1, "is_stock_item": 1, "serial_no_series": "SRTN-DNN-TP-.#####"}, + ).name + + batches = {} + serial_nos = [] + for qty, rate in {3: 300, 2: 100}.items(): + se = make_stock_entry( + item_code=batch_item, target="_Test Warehouse - _TC", qty=qty, basic_rate=rate + ) + batches[get_batch_from_bundle(se.items[0].serial_and_batch_bundle)] = qty + + for qty, rate in {2: 100, 1: 50}.items(): + make_stock_entry(item_code=serial_item, target="_Test Warehouse - _TC", qty=qty, basic_rate=rate) + serial_nos.extend(get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle)) + + dn = create_delivery_note( + item_code=batch_item, + qty=5, + rate=1000, + use_serial_batch_fields=0, + batches=batches, + do_not_submit=True, + ) + + bundle_id = make_serial_batch_bundle( + frappe._dict( + { + "item_code": serial_item, + "warehouse": dn.items[0].warehouse, + "qty": 3, + "voucher_type": "Delivery Note", + "serial_nos": serial_nos, + "posting_date": dn.posting_date, + "posting_time": dn.posting_time, + "type_of_transaction": "Outward", + "do_not_submit": True, + } + ) + ).name + + dn.append( + "items", + { + "item_code": serial_item, + "qty": 3, + "rate": 700, + "base_rate": 700, + "item_name": serial_item, + "uom": "Nos", + "stock_uom": "Nos", + "conversion_factor": 1, + "warehouse": dn.items[0].warehouse, + "use_serial_batch_fields": 0, + "serial_and_batch_bundle": bundle_id, + }, + ) + + dn.save() + dn.submit() + dn.reload() + + batch_no_valuation = defaultdict(float) + serial_no_valuation = defaultdict(float) + + for row in dn.items: + if row.serial_and_batch_bundle: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.serial_and_batch_bundle}, + fields=["incoming_rate", "serial_no", "batch_no"], + ) + + for d in bundle_data: + if d.batch_no: + batch_no_valuation[d.batch_no] = d.incoming_rate + elif d.serial_no: + serial_no_valuation[d.serial_no] = d.incoming_rate + + return_entry = make_sales_return(dn.name) + + return_entry.save() + return_entry.submit() + return_entry.reload() + + for row in return_entry.items: + if row.item_code == batch_item: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.serial_and_batch_bundle}, + fields=["incoming_rate", "batch_no"], + ) + + for d in bundle_data: + self.assertEqual(d.incoming_rate, batch_no_valuation[d.batch_no]) + else: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.serial_and_batch_bundle}, + fields=["incoming_rate", "serial_no"], + ) + + for d in bundle_data: + self.assertEqual(d.incoming_rate, serial_no_valuation[d.serial_no]) + + def test_delivery_note_return_valuation_with_use_serial_batch_field(self): + from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return + + batch_item = make_item( + "_Test Delivery Note Return Valuation WITH Batch Item", + properties={ + "has_batch_no": 1, + "create_new_batch": 1, + "is_stock_item": 1, + "batch_number_series": "BRTN-DNN-BIW-.#####", + }, + ).name + + serial_item = make_item( + "_Test Delivery Note Return Valuation WITH Serial Item", + properties={"has_serial_no": 1, "is_stock_item": 1, "serial_no_series": "SRTN-DNN-TPW-.#####"}, + ).name + + batches = [] + serial_nos = [] + for qty, rate in {3: 300, 2: 100}.items(): + se = make_stock_entry( + item_code=batch_item, target="_Test Warehouse - _TC", qty=qty, basic_rate=rate + ) + batches.append(get_batch_from_bundle(se.items[0].serial_and_batch_bundle)) + + for qty, rate in {2: 100, 1: 50}.items(): + se = make_stock_entry( + item_code=serial_item, target="_Test Warehouse - _TC", qty=qty, basic_rate=rate + ) + serial_nos.extend(get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle)) + + dn = create_delivery_note( + item_code=batch_item, + qty=3, + rate=1000, + use_serial_batch_fields=1, + batch_no=batches[0], + do_not_submit=True, + ) + + dn.append( + "items", + { + "item_code": batch_item, + "qty": 2, + "rate": 1000, + "base_rate": 1000, + "item_name": batch_item, + "uom": dn.items[0].uom, + "stock_uom": dn.items[0].uom, + "conversion_factor": 1, + "warehouse": dn.items[0].warehouse, + "use_serial_batch_fields": 1, + "batch_no": batches[1], + }, + ) + + dn.append( + "items", + { + "item_code": serial_item, + "qty": 2, + "rate": 700, + "base_rate": 700, + "item_name": serial_item, + "uom": "Nos", + "stock_uom": "Nos", + "conversion_factor": 1, + "warehouse": dn.items[0].warehouse, + "use_serial_batch_fields": 1, + "serial_no": "\n".join(serial_nos[0:2]), + }, + ) + + dn.append( + "items", + { + "item_code": serial_item, + "qty": 1, + "rate": 700, + "base_rate": 700, + "item_name": serial_item, + "uom": "Nos", + "stock_uom": "Nos", + "conversion_factor": 1, + "warehouse": dn.items[0].warehouse, + "use_serial_batch_fields": 1, + "serial_no": serial_nos[-1], + }, + ) + + dn.save() + dn.submit() + dn.reload() + + batch_no_valuation = defaultdict(float) + serial_no_valuation = defaultdict(float) + + for row in dn.items: + if row.serial_and_batch_bundle: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.serial_and_batch_bundle}, + fields=["incoming_rate", "serial_no", "batch_no"], + ) + + for d in bundle_data: + if d.batch_no: + batch_no_valuation[d.batch_no] = d.incoming_rate + elif d.serial_no: + serial_no_valuation[d.serial_no] = d.incoming_rate + + return_entry = make_sales_return(dn.name) + + return_entry.save() + return_entry.submit() + return_entry.reload() + + for row in return_entry.items: + if row.item_code == batch_item: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.serial_and_batch_bundle}, + fields=["incoming_rate", "batch_no"], + ) + + for d in bundle_data: + self.assertEqual(d.incoming_rate, batch_no_valuation[d.batch_no]) + else: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.serial_and_batch_bundle}, + fields=["incoming_rate", "serial_no"], + ) + + for d in bundle_data: + self.assertEqual(d.incoming_rate, serial_no_valuation[d.serial_no]) + def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") @@ -2107,6 +2366,9 @@ def create_delivery_note(**args): if args.get("batch_no"): batches = frappe._dict({args.batch_no: qty}) + if args.get("batches"): + batches = frappe._dict(args.batches) + bundle_id = make_serial_batch_bundle( frappe._dict( { diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 6d0fe27033f..64c3d2c67b2 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -3672,6 +3672,234 @@ class TestPurchaseReceipt(FrappeTestCase): self.assertEqual(pr.items[0].conversion_factor, 1.0) + def test_purchase_receipt_return_valuation_without_use_serial_batch_field(self): + from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_return + + batch_item = make_item( + "_Test Purchase Receipt Return Valuation Batch Item", + properties={ + "has_batch_no": 1, + "create_new_batch": 1, + "is_stock_item": 1, + "batch_number_series": "BRTN-TPRBI-.#####", + }, + ).name + + serial_item = make_item( + "_Test Purchase Receipt Return Valuation Serial Item", + properties={"has_serial_no": 1, "is_stock_item": 1, "serial_no_series": "SRTN-TPRSI-.#####"}, + ).name + + rej_warehouse = create_warehouse("_Test Purchase Warehouse For Rejected Qty") + + pr = make_purchase_receipt( + item_code=batch_item, + received_qty=10, + qty=8, + rejected_qty=2, + rejected_warehouse=rej_warehouse, + rate=300, + do_not_submit=1, + use_serial_batch_fields=0, + ) + + pr.append( + "items", + { + "item_code": serial_item, + "qty": 2, + "rate": 100, + "base_rate": 100, + "item_name": serial_item, + "uom": "Nos", + "stock_uom": "Nos", + "conversion_factor": 1, + "rejected_qty": 1, + "warehouse": pr.items[0].warehouse, + "use_serial_batch_fields": 0, + "rejected_warehouse": rej_warehouse, + }, + ) + + pr.save() + pr.submit() + pr.reload() + + batch_no = get_batch_from_bundle(pr.items[0].serial_and_batch_bundle) + rejected_batch_no = get_batch_from_bundle(pr.items[0].rejected_serial_and_batch_bundle) + + self.assertEqual(batch_no, rejected_batch_no) + + return_entry = make_purchase_return(pr.name) + + return_entry.save() + return_entry.submit() + return_entry.reload() + + for row in return_entry.items: + if row.item_code == batch_item: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.serial_and_batch_bundle}, + pluck="incoming_rate", + ) + + for incoming_rate in bundle_data: + self.assertEqual(incoming_rate, 300.00) + else: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.serial_and_batch_bundle}, + pluck="incoming_rate", + ) + + for incoming_rate in bundle_data: + self.assertEqual(incoming_rate, 100.00) + + for row in return_entry.items: + if row.item_code == batch_item: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.rejected_serial_and_batch_bundle}, + pluck="incoming_rate", + ) + + for incoming_rate in bundle_data: + self.assertEqual(incoming_rate, 0) + else: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.rejected_serial_and_batch_bundle}, + pluck="incoming_rate", + ) + + for incoming_rate in bundle_data: + self.assertEqual(incoming_rate, 0) + + def test_purchase_receipt_return_valuation_with_use_serial_batch_field(self): + from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_return + + batch_item = make_item( + "_Test Purchase Receipt Return Valuation With Batch Item", + properties={"has_batch_no": 1, "create_new_batch": 1, "is_stock_item": 1}, + ).name + + serial_item = make_item( + "_Test Purchase Receipt Return Valuation With Serial Item", + properties={"has_serial_no": 1, "is_stock_item": 1}, + ).name + + rej_warehouse = create_warehouse("_Test Purchase Warehouse For Rejected Qty") + + batch_no = "BATCH-RTN-BNU-TPRBI-0001" + serial_nos = ["SNU-RTN-TPRSI-0001", "SNU-RTN-TPRSI-0002", "SNU-RTN-TPRSI-0003"] + + if not frappe.db.exists("Batch", batch_no): + frappe.get_doc( + { + "doctype": "Batch", + "batch_id": batch_no, + "item": batch_item, + } + ).insert() + + for serial_no in serial_nos: + if not frappe.db.exists("Serial No", serial_no): + frappe.get_doc( + { + "doctype": "Serial No", + "item_code": serial_item, + "serial_no": serial_no, + } + ).insert() + + pr = make_purchase_receipt( + item_code=batch_item, + received_qty=10, + qty=8, + rejected_qty=2, + rejected_warehouse=rej_warehouse, + batch_no=batch_no, + use_serial_batch_fields=1, + rate=300, + do_not_submit=1, + ) + + pr.append( + "items", + { + "item_code": serial_item, + "qty": 2, + "rate": 100, + "base_rate": 100, + "item_name": serial_item, + "uom": "Nos", + "stock_uom": "Nos", + "conversion_factor": 1, + "rejected_qty": 1, + "warehouse": pr.items[0].warehouse, + "use_serial_batch_fields": 1, + "rejected_warehouse": rej_warehouse, + "serial_no": "\n".join(serial_nos[:2]), + "rejected_serial_no": serial_nos[2], + }, + ) + + pr.save() + pr.submit() + pr.reload() + + batch_no = get_batch_from_bundle(pr.items[0].serial_and_batch_bundle) + rejected_batch_no = get_batch_from_bundle(pr.items[0].rejected_serial_and_batch_bundle) + + self.assertEqual(batch_no, rejected_batch_no) + + return_entry = make_purchase_return(pr.name) + + return_entry.save() + return_entry.submit() + return_entry.reload() + + for row in return_entry.items: + if row.item_code == batch_item: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.serial_and_batch_bundle}, + pluck="incoming_rate", + ) + + for incoming_rate in bundle_data: + self.assertEqual(incoming_rate, 300.00) + else: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.serial_and_batch_bundle}, + pluck="incoming_rate", + ) + + for incoming_rate in bundle_data: + self.assertEqual(incoming_rate, 100.00) + + for row in return_entry.items: + if row.item_code == batch_item: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.rejected_serial_and_batch_bundle}, + pluck="incoming_rate", + ) + + for incoming_rate in bundle_data: + self.assertEqual(incoming_rate, 0) + else: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.rejected_serial_and_batch_bundle}, + pluck="incoming_rate", + ) + + for incoming_rate in bundle_data: + self.assertEqual(incoming_rate, 0) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 94ec8675db8..ed6d5e155d7 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -197,7 +197,7 @@ class SerialandBatchBundle(Document): def throw_error_message(self, message, exception=frappe.ValidationError): frappe.throw(_(message), exception, title=_("Error")) - def set_incoming_rate(self, row=None, save=False, allow_negative_stock=False): + def set_incoming_rate(self, parent=None, row=None, save=False, allow_negative_stock=False): if self.type_of_transaction not in ["Inward", "Outward"] or self.voucher_type in [ "Installation Note", "Job Card", @@ -206,13 +206,70 @@ class SerialandBatchBundle(Document): ]: return - if self.type_of_transaction == "Outward": + if return_aginst := self.get_return_aginst(parent=parent): + self.set_valuation_rate_for_return_entry(return_aginst, save) + elif self.type_of_transaction == "Outward": self.set_incoming_rate_for_outward_transaction( row, save, allow_negative_stock=allow_negative_stock ) else: self.set_incoming_rate_for_inward_transaction(row, save) + def set_valuation_rate_for_return_entry(self, return_aginst, save=False): + if valuation_details := self.get_valuation_rate_for_return_entry(return_aginst): + for row in self.entries: + if row.serial_no: + valuation_rate = valuation_details["serial_nos"].get(row.serial_no) + else: + valuation_rate = valuation_details["batches"].get(row.batch_no) + + row.incoming_rate = valuation_rate + row.stock_value_difference = flt(row.qty) * flt(row.incoming_rate) + + if save: + row.db_set( + { + "incoming_rate": row.incoming_rate, + "stock_value_difference": row.stock_value_difference, + } + ) + + def get_valuation_rate_for_return_entry(self, return_aginst): + valuation_details = frappe._dict( + { + "serial_nos": defaultdict(float), + "batches": defaultdict(float), + } + ) + + bundle_data = frappe.get_all( + "Serial and Batch Bundle", + fields=[ + "`tabSerial and Batch Entry`.`serial_no`", + "`tabSerial and Batch Entry`.`batch_no`", + "`tabSerial and Batch Entry`.`incoming_rate`", + ], + filters=[ + ["Serial and Batch Bundle", "voucher_no", "=", return_aginst], + ["Serial and Batch Entry", "docstatus", "=", 1], + ["Serial and Batch Bundle", "is_cancelled", "=", 0], + ["Serial and Batch Bundle", "item_code", "=", self.item_code], + ["Serial and Batch Bundle", "warehouse", "=", self.warehouse], + ], + order_by="`tabSerial and Batch Bundle`.`creation`, `tabSerial and Batch Entry`.`idx`", + ) + + if not bundle_data: + return {} + + for row in bundle_data: + if row.serial_no: + valuation_details["serial_nos"][row.serial_no] = row.incoming_rate + else: + valuation_details["batches"][row.batch_no] = row.incoming_rate + + return valuation_details + def calculate_total_qty(self, save=True): self.total_qty = 0.0 for d in self.entries: @@ -327,6 +384,33 @@ class SerialandBatchBundle(Document): return sle + def get_return_aginst(self, parent=None): + return_aginst = None + + if parent and parent.get("is_return") and parent.get("return_against"): + return parent.get("return_against") + + if ( + self.voucher_type + in [ + "Delivery Note", + "Sales Invoice", + "Purchase Invoice", + "Purchase Receipt", + "POS Invoice", + "Subcontracting Receipt", + ] + and self.voucher_type + and self.voucher_no + ): + voucher_details = frappe.db.get_value( + self.voucher_type, self.voucher_no, ["is_return", "return_against"], as_dict=True + ) + if voucher_details and voucher_details.get("is_return") and voucher_details.get("return_against"): + return voucher_details.get("return_against") + + return return_aginst + def set_incoming_rate_for_inward_transaction(self, row=None, save=False): valuation_field = "valuation_rate" if self.voucher_type in ["Sales Invoice", "Delivery Note", "Quotation"]: @@ -354,7 +438,9 @@ class SerialandBatchBundle(Document): rate = frappe.db.get_value(child_table, self.voucher_detail_no, valuation_field) for d in self.entries: - if (d.incoming_rate == rate) and d.qty and d.stock_value_difference: + if self.is_rejected: + rate = 0.0 + elif (d.incoming_rate == rate) and d.qty and d.stock_value_difference: continue d.incoming_rate = flt(rate, precision) @@ -403,7 +489,7 @@ class SerialandBatchBundle(Document): # If user has changed the rate in the child table if self.docstatus == 0: - self.set_incoming_rate(save=True, row=row) + self.set_incoming_rate(parent=parent, row=row, save=True) if self.docstatus == 0 and parent.get("is_return") and parent.is_new(): self.reset_qty(row, qty_field=qty_field) diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 46724be5927..47521475d0a 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -1088,6 +1088,8 @@ class SerialBatchCreation: frappe.db.bulk_insert("Serial No", fields=fields, values=set(serial_nos_details)) def set_serial_batch_entries(self, doc): + incoming_rate = self.get("incoming_rate") + if self.get("serial_nos"): serial_no_wise_batch = frappe._dict({}) if self.has_batch_no: @@ -1095,30 +1097,54 @@ class SerialBatchCreation: qty = -1 if self.type_of_transaction == "Outward" else 1 for serial_no in self.serial_nos: + if self.get("serial_nos_valuation"): + incoming_rate = self.get("serial_nos_valuation").get(serial_no) + doc.append( "entries", { "serial_no": serial_no, "qty": qty, "batch_no": serial_no_wise_batch.get(serial_no) or self.get("batch_no"), - "incoming_rate": self.get("incoming_rate"), + "incoming_rate": incoming_rate, }, ) elif self.get("batches"): for batch_no, batch_qty in self.batches.items(): + if self.get("batches_valuation"): + incoming_rate = self.get("batches_valuation").get(batch_no) + doc.append( "entries", { "batch_no": batch_no, "qty": batch_qty * (-1 if self.type_of_transaction == "Outward" else 1), - "incoming_rate": self.get("incoming_rate"), + "incoming_rate": incoming_rate, }, ) def create_batch(self): from erpnext.stock.doctype.batch.batch import make_batch + if self.is_rejected: + bundle = frappe.db.get_value( + "Serial and Batch Bundle", + { + "voucher_no": self.voucher_no, + "voucher_type": self.voucher_type, + "voucher_detail_no": self.voucher_detail_no, + "is_rejected": 0, + "docstatus": 1, + "is_cancelled": 0, + }, + "name", + ) + + if bundle: + if batch_no := frappe.db.get_value("Serial and Batch Entry", {"parent": bundle}, "batch_no"): + return batch_no + return make_batch( frappe._dict( {