From 3008c7ad82eec6d7ef157421f5cf39660b851cc0 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 13 Jan 2026 17:00:45 +0530 Subject: [PATCH 1/2] fix: valuation rate for non batchwise valuation (cherry picked from commit b6312bca9c8d163c8771e73a60545d226d3e559e) # Conflicts: # erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py --- .../purchase_receipt/test_purchase_receipt.py | 280 ++++++++++++++++++ .../serial_and_batch_bundle.py | 21 +- 2 files changed, 290 insertions(+), 11 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 14d6ee7700d..2f6788c9552 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -4529,6 +4529,286 @@ class TestPurchaseReceipt(FrappeTestCase): self.assertEqual(sles, [1500.0, 1500.0]) +<<<<<<< HEAD +======= + @IntegrationTestCase.change_settings("Stock Settings", {"allow_negative_stock": 0}) + def test_multiple_transactions_with_same_posting_datetime(self): + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + from erpnext.stock.stock_ledger import NegativeStockError + + item_code = make_item( + "Test Item for Multiple Txn with Same Posting Datetime", {"is_stock_item": 1} + ).name + + pr = make_purchase_receipt( + item_code=item_code, + qty=100, + rate=100, + posting_date=today(), + posting_time="10:00:00", + ) + + create_delivery_note( + item_code=item_code, + qty=100, + rate=100, + posting_date=today(), + posting_time="10:00:00", + ) + + make_purchase_receipt( + item_code=item_code, + qty=150, + rate=100, + posting_date=today(), + posting_time="10:00:00", + ) + + self.assertRaises(NegativeStockError, pr.cancel) + + @IntegrationTestCase.change_settings( + "Buying Settings", {"set_landed_cost_based_on_purchase_invoice_rate": 1, "maintain_same_rate": 0} + ) + def test_set_lcv_from_pi_created_against_po(self): + from erpnext.buying.doctype.purchase_order.purchase_order import ( + make_purchase_invoice as make_pi_against_po, + ) + from erpnext.buying.doctype.purchase_order.purchase_order import ( + make_purchase_receipt as make_pr_against_po, + ) + from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order + + original_value = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance") + + frappe.db.set_single_value("Accounts Settings", "over_billing_allowance", 100) + + item_code = create_item("Test Item for LCV from PI against PO").name + + po = create_purchase_order(item_code=item_code, qty=10, rate=400) + pr = make_pr_against_po(po.name) + pr.items[0].qty = 5 + item = frappe.copy_doc(pr.items[0]) + item.qty = 2 + pr.append("items", item) + + item = frappe.copy_doc(pr.items[0]) + item.qty = 3 + pr.append("items", item) + pr.submit() + + pi = make_pi_against_po(po.name) + pi.items[0].rate = 500 + pi.submit() + + pr.reload() + for row in pr.items: + self.assertTrue(row.amount_difference_with_purchase_invoice) + amt_diff = 5000 * (row.qty / 10) - row.amount + self.assertEqual(row.amount_difference_with_purchase_invoice, amt_diff) + + frappe.db.set_single_value("Accounts Settings", "over_billing_allowance", original_value) + + def test_purchase_return_with_and_without_return_against_rejected_qty(self): + from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( + make_purchase_return as _make_purchase_return, + ) + from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( + make_purchase_return_against_rejected_warehouse, + ) + + item_code = create_item("Test Item for PR against Rejected Qty").name + warehouse = "_Test Warehouse - _TC" + + company = frappe.db.get_value("Warehouse", warehouse, "company") + rejected_wh = create_warehouse("_Test Rejected Warehouse", company=company) + + pr = make_purchase_receipt( + item_code=item_code, + qty=10, + rejected_qty=5, + rate=100, + warehouse=warehouse, + rejected_warehouse=rejected_wh, + ) + + # Purchase Return against rejected qty partially + return_entry = make_purchase_return_against_rejected_warehouse(pr.name) + return_entry.items[0].qty = -2 + return_entry.items[0].received_qty = -2 + return_entry.save() + return_entry.submit() + pr.reload() + + # Purchase Return against rejected qty partially + return_entry = _make_purchase_return(pr.name) + + self.assertEqual(return_entry.items[0].qty, -10) + self.assertEqual(return_entry.items[0].rejected_qty, -3) # 5-2=3 + + return_entry.items[0].qty = -8 + return_entry.items[0].stock_qty = -8 + return_entry.items[0].received_qty = -11 + + return_entry.save() + return_entry.submit() + + pr.reload() + + # Purchase Return against rejected qty partially + return_entry = _make_purchase_return(pr.name) + + self.assertEqual(return_entry.items[0].qty, -2) + self.assertEqual(return_entry.items[0].rejected_qty, 0) # 3-3=0 + + def test_do_not_use_batchwise_valuation_with_fifo(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + item_code = make_item( + "Test Item Do Not Use Batchwise Valuation with FIFO", + { + "is_stock_item": 1, + "has_batch_no": 1, + "batch_number_series": "BN-TESTDNUBVWF-.#####", + "valuation_method": "FIFO", + }, + ).name + + doc = frappe.new_doc("Batch") + doc.update( + { + "batch_id": "BN-TESTDNUBVWF-00001", + "item": item_code, + } + ).insert() + + doc.db_set("use_batchwise_valuation", 0) + doc.reload() + + self.assertTrue(doc.use_batchwise_valuation == 0) + + doc = frappe.new_doc("Batch") + doc.update( + { + "batch_id": "BN-TESTDNUBVWF-00002", + "item": item_code, + } + ).insert() + + self.assertTrue(doc.use_batchwise_valuation == 1) + + warehouse = "_Test Warehouse - _TC" + make_stock_entry( + item_code=item_code, + qty=10, + rate=100, + target=warehouse, + batch_no="BN-TESTDNUBVWF-00001", + use_serial_batch_fields=1, + ) + + se1 = make_stock_entry( + item_code=item_code, + qty=10, + rate=200, + target=warehouse, + batch_no="BN-TESTDNUBVWF-00001", + use_serial_batch_fields=1, + ) + + stock_queue = frappe.db.get_value( + "Stock Ledger Entry", + { + "item_code": item_code, + "warehouse": warehouse, + "is_cancelled": 0, + "voucher_type": "Stock Entry", + "voucher_no": se1.name, + }, + "stock_queue", + ) + + stock_queue = frappe.parse_json(stock_queue) + + self.assertEqual(stock_queue, [[10, 100.0], [10, 200.0]]) + + se2 = make_stock_entry( + item_code=item_code, + qty=10, + rate=2, + target=warehouse, + batch_no="BN-TESTDNUBVWF-00002", + use_serial_batch_fields=1, + ) + + stock_queue = frappe.db.get_value( + "Stock Ledger Entry", + { + "item_code": item_code, + "warehouse": warehouse, + "is_cancelled": 0, + "voucher_type": "Stock Entry", + "voucher_no": se2.name, + }, + "stock_queue", + ) + + stock_queue = frappe.parse_json(stock_queue) + self.assertEqual(stock_queue, [[10, 100.0], [10, 200.0]]) + + se3 = make_stock_entry( + item_code=item_code, + qty=20, + source=warehouse, + batch_no="BN-TESTDNUBVWF-00001", + use_serial_batch_fields=1, + ) + + ste_details = frappe.db.get_value( + "Stock Ledger Entry", + { + "item_code": item_code, + "warehouse": warehouse, + "is_cancelled": 0, + "voucher_type": "Stock Entry", + "voucher_no": se3.name, + }, + ["stock_queue", "stock_value_difference"], + as_dict=1, + ) + + stock_queue = frappe.parse_json(ste_details.stock_queue) + self.assertEqual(stock_queue, []) + self.assertEqual(ste_details.stock_value_difference, 3000 * -1) + + se4 = make_stock_entry( + item_code=item_code, + qty=20, + rate=0, + target=warehouse, + batch_no="BN-TESTDNUBVWF-00001", + use_serial_batch_fields=1, + do_not_submit=1, + ) + + se4.items[0].basic_rate = 0.0 + se4.items[0].allow_zero_valuation_rate = 1 + se4.submit() + + stock_queue = frappe.db.get_value( + "Stock Ledger Entry", + { + "item_code": item_code, + "warehouse": warehouse, + "is_cancelled": 0, + "voucher_type": "Stock Entry", + "voucher_no": se4.name, + }, + "stock_queue", + ) + + self.assertEqual(frappe.parse_json(stock_queue), [[20, 0.0]]) + +>>>>>>> b6312bca9c (fix: valuation rate for non batchwise valuation) 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 ad88b571a91..b6caf53e470 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 @@ -713,17 +713,16 @@ class SerialandBatchBundle(Document): is_packed_item = True stock_queue = [] - batches = [] - if prev_sle and prev_sle.stock_queue: - batches = frappe.get_all( - "Batch", - filters={ - "name": ("in", [d.batch_no for d in self.entries if d.batch_no]), - "use_batchwise_valuation": 0, - }, - pluck="name", - ) + batches = frappe.get_all( + "Batch", + filters={ + "name": ("in", [d.batch_no for d in self.entries if d.batch_no]), + "use_batchwise_valuation": 0, + }, + pluck="name", + ) + if prev_sle and prev_sle.stock_queue and parse_json(prev_sle.stock_queue): if batches and valuation_method == "FIFO": stock_queue = parse_json(prev_sle.stock_queue) @@ -750,7 +749,7 @@ class SerialandBatchBundle(Document): if d.qty: d.stock_value_difference = flt(d.qty) * d.incoming_rate - if stock_queue and valuation_method == "FIFO" and d.batch_no in batches: + if valuation_method == "FIFO" and d.batch_no in batches and d.incoming_rate is not None: stock_queue.append([d.qty, d.incoming_rate]) d.stock_queue = json.dumps(stock_queue) From 0a363f879d46e04b42a2bee809f647401969d980 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Wed, 14 Jan 2026 19:43:39 +0530 Subject: [PATCH 2/2] chore: fix conflicts Removed multiple test cases related to purchase receipts and negative stock handling. --- .../purchase_receipt/test_purchase_receipt.py | 132 ------------------ 1 file changed, 132 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 2f6788c9552..5570fc265b0 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -4529,137 +4529,6 @@ class TestPurchaseReceipt(FrappeTestCase): self.assertEqual(sles, [1500.0, 1500.0]) -<<<<<<< HEAD -======= - @IntegrationTestCase.change_settings("Stock Settings", {"allow_negative_stock": 0}) - def test_multiple_transactions_with_same_posting_datetime(self): - from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note - from erpnext.stock.stock_ledger import NegativeStockError - - item_code = make_item( - "Test Item for Multiple Txn with Same Posting Datetime", {"is_stock_item": 1} - ).name - - pr = make_purchase_receipt( - item_code=item_code, - qty=100, - rate=100, - posting_date=today(), - posting_time="10:00:00", - ) - - create_delivery_note( - item_code=item_code, - qty=100, - rate=100, - posting_date=today(), - posting_time="10:00:00", - ) - - make_purchase_receipt( - item_code=item_code, - qty=150, - rate=100, - posting_date=today(), - posting_time="10:00:00", - ) - - self.assertRaises(NegativeStockError, pr.cancel) - - @IntegrationTestCase.change_settings( - "Buying Settings", {"set_landed_cost_based_on_purchase_invoice_rate": 1, "maintain_same_rate": 0} - ) - def test_set_lcv_from_pi_created_against_po(self): - from erpnext.buying.doctype.purchase_order.purchase_order import ( - make_purchase_invoice as make_pi_against_po, - ) - from erpnext.buying.doctype.purchase_order.purchase_order import ( - make_purchase_receipt as make_pr_against_po, - ) - from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order - - original_value = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance") - - frappe.db.set_single_value("Accounts Settings", "over_billing_allowance", 100) - - item_code = create_item("Test Item for LCV from PI against PO").name - - po = create_purchase_order(item_code=item_code, qty=10, rate=400) - pr = make_pr_against_po(po.name) - pr.items[0].qty = 5 - item = frappe.copy_doc(pr.items[0]) - item.qty = 2 - pr.append("items", item) - - item = frappe.copy_doc(pr.items[0]) - item.qty = 3 - pr.append("items", item) - pr.submit() - - pi = make_pi_against_po(po.name) - pi.items[0].rate = 500 - pi.submit() - - pr.reload() - for row in pr.items: - self.assertTrue(row.amount_difference_with_purchase_invoice) - amt_diff = 5000 * (row.qty / 10) - row.amount - self.assertEqual(row.amount_difference_with_purchase_invoice, amt_diff) - - frappe.db.set_single_value("Accounts Settings", "over_billing_allowance", original_value) - - def test_purchase_return_with_and_without_return_against_rejected_qty(self): - from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( - make_purchase_return as _make_purchase_return, - ) - from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( - make_purchase_return_against_rejected_warehouse, - ) - - item_code = create_item("Test Item for PR against Rejected Qty").name - warehouse = "_Test Warehouse - _TC" - - company = frappe.db.get_value("Warehouse", warehouse, "company") - rejected_wh = create_warehouse("_Test Rejected Warehouse", company=company) - - pr = make_purchase_receipt( - item_code=item_code, - qty=10, - rejected_qty=5, - rate=100, - warehouse=warehouse, - rejected_warehouse=rejected_wh, - ) - - # Purchase Return against rejected qty partially - return_entry = make_purchase_return_against_rejected_warehouse(pr.name) - return_entry.items[0].qty = -2 - return_entry.items[0].received_qty = -2 - return_entry.save() - return_entry.submit() - pr.reload() - - # Purchase Return against rejected qty partially - return_entry = _make_purchase_return(pr.name) - - self.assertEqual(return_entry.items[0].qty, -10) - self.assertEqual(return_entry.items[0].rejected_qty, -3) # 5-2=3 - - return_entry.items[0].qty = -8 - return_entry.items[0].stock_qty = -8 - return_entry.items[0].received_qty = -11 - - return_entry.save() - return_entry.submit() - - pr.reload() - - # Purchase Return against rejected qty partially - return_entry = _make_purchase_return(pr.name) - - self.assertEqual(return_entry.items[0].qty, -2) - self.assertEqual(return_entry.items[0].rejected_qty, 0) # 3-3=0 - def test_do_not_use_batchwise_valuation_with_fifo(self): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry @@ -4808,7 +4677,6 @@ class TestPurchaseReceipt(FrappeTestCase): self.assertEqual(frappe.parse_json(stock_queue), [[20, 0.0]]) ->>>>>>> b6312bca9c (fix: valuation rate for non batchwise valuation) def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier