diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index 404758ce94f..b444e864e3d 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -13,7 +13,6 @@ from erpnext.stock.doctype.item.test_item import create_item, make_item
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice
from erpnext.stock.doctype.serial_no.serial_no import SerialNoDuplicateError, get_serial_nos
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
-from erpnext.stock.stock_ledger import SerialNoExistsInFutureTransaction
class TestPurchaseReceipt(FrappeTestCase):
@@ -197,84 +196,6 @@ class TestPurchaseReceipt(FrappeTestCase):
self.assertFalse(frappe.db.get_value("Batch", {"item": item.name, "reference_name": pr.name}))
self.assertFalse(frappe.db.get_all("Serial No", {"batch_no": batch_no}))
- def test_duplicate_serial_nos(self):
- from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
-
- item = frappe.db.exists("Item", {"item_name": "Test Serialized Item 123"})
- if not item:
- item = create_item("Test Serialized Item 123")
- item.has_serial_no = 1
- item.serial_no_series = "TSI123-.####"
- item.save()
- else:
- item = frappe.get_doc("Item", {"item_name": "Test Serialized Item 123"})
-
- # First make purchase receipt
- pr = make_purchase_receipt(item_code=item.name, qty=2, rate=500)
- pr.load_from_db()
-
- serial_nos = frappe.db.get_value(
- "Stock Ledger Entry",
- {"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "item_code": item.name},
- "serial_no",
- )
-
- serial_nos = get_serial_nos(serial_nos)
-
- self.assertEquals(get_serial_nos(pr.items[0].serial_no), serial_nos)
-
- # Then tried to receive same serial nos in difference company
- pr_different_company = make_purchase_receipt(
- item_code=item.name,
- qty=2,
- rate=500,
- serial_no="\n".join(serial_nos),
- company="_Test Company 1",
- do_not_submit=True,
- warehouse="Stores - _TC1",
- )
-
- self.assertRaises(SerialNoDuplicateError, pr_different_company.submit)
-
- # Then made delivery note to remove the serial nos from stock
- dn = create_delivery_note(item_code=item.name, qty=2, rate=1500, serial_no="\n".join(serial_nos))
- dn.load_from_db()
- self.assertEquals(get_serial_nos(dn.items[0].serial_no), serial_nos)
-
- posting_date = add_days(today(), -3)
-
- # Try to receive same serial nos again in the same company with backdated.
- pr1 = make_purchase_receipt(
- item_code=item.name,
- qty=2,
- rate=500,
- posting_date=posting_date,
- serial_no="\n".join(serial_nos),
- do_not_submit=True,
- )
-
- self.assertRaises(SerialNoExistsInFutureTransaction, pr1.submit)
-
- # Try to receive same serial nos with different company with backdated.
- pr2 = make_purchase_receipt(
- item_code=item.name,
- qty=2,
- rate=500,
- posting_date=posting_date,
- serial_no="\n".join(serial_nos),
- company="_Test Company 1",
- do_not_submit=True,
- warehouse="Stores - _TC1",
- )
-
- self.assertRaises(SerialNoExistsInFutureTransaction, pr2.submit)
-
- # Receive the same serial nos after the delivery note posting date and time
- make_purchase_receipt(item_code=item.name, qty=2, rate=500, serial_no="\n".join(serial_nos))
-
- # Raise the error for backdated deliver note entry cancel
- self.assertRaises(SerialNoExistsInFutureTransaction, dn.cancel)
-
def test_purchase_receipt_gl_entry(self):
pr = make_purchase_receipt(
company="_Test Company with perpetual inventory",
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index dd39f103cd7..9a46ae71ad2 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -634,30 +634,48 @@ class StockReconciliation(StockController):
if voucher_detail_no != row.name:
continue
- current_qty = get_batch_qty_for_stock_reco(
- row.item_code, row.warehouse, row.batch_no, self.posting_date, self.posting_time, self.name
- )
+ if row.serial_no:
+ item_dict = get_stock_balance_for(
+ row.item_code,
+ row.warehouse,
+ self.posting_date,
+ self.posting_time,
+ voucher_no=self.name,
+ )
+
+ current_qty = item_dict.get("qty")
+ row.current_serial_no = item_dict.get("serial_nos")
+ row.current_valuation_rate = item_dict.get("rate")
+ else:
+ current_qty = get_batch_qty_for_stock_reco(
+ row.item_code, row.warehouse, row.batch_no, self.posting_date, self.posting_time, self.name
+ )
precesion = row.precision("current_qty")
if flt(current_qty, precesion) != flt(row.current_qty, precesion):
- val_rate = get_valuation_rate(
- row.item_code,
- row.warehouse,
- self.doctype,
- self.name,
- company=self.company,
- batch_no=row.batch_no,
- )
+ if not row.serial_no:
+ val_rate = get_valuation_rate(
+ row.item_code,
+ row.warehouse,
+ self.doctype,
+ self.name,
+ company=self.company,
+ batch_no=row.batch_no,
+ )
+
+ row.current_valuation_rate = val_rate
- row.current_valuation_rate = val_rate
row.current_qty = current_qty
- row.db_set(
- {
- "current_qty": row.current_qty,
- "current_valuation_rate": row.current_valuation_rate,
- "current_amount": flt(row.current_qty * row.current_valuation_rate),
- }
- )
+ values_to_update = {
+ "current_qty": row.current_qty,
+ "current_valuation_rate": row.current_valuation_rate,
+ "current_amount": flt(row.current_qty * row.current_valuation_rate),
+ }
+
+ if row.current_serial_no:
+ values_to_update["current_serial_no"] = row.current_serial_no
+
+ row.db_set(values_to_update)
if (
add_new_sle
@@ -880,6 +898,7 @@ def get_stock_balance_for(
batch_no: Optional[str] = None,
with_valuation_rate: bool = True,
inventory_dimensions_dict=None,
+ voucher_no=None,
):
frappe.has_permission("Stock Reconciliation", "write", throw=True)
@@ -910,6 +929,7 @@ def get_stock_balance_for(
with_serial_no=has_serial_no,
inventory_dimensions_dict=inventory_dimensions_dict,
batch_no=batch_no,
+ voucher_no=voucher_no,
)
if has_serial_no:
diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
index 05c60175f51..19c04afe909 100644
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -1055,6 +1055,74 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
self.assertEqual(sr.items[0].current_qty, se2.items[0].qty)
self.assertEqual(len(sr.items[0].current_serial_no.split("\n")), sr.items[0].current_qty)
+ def test_backdated_purchase_receipt_with_stock_reco(self):
+ item_code = self.make_item(
+ properties={
+ "is_stock_item": 1,
+ "has_serial_no": 1,
+ "serial_no_series": "TEST-SERIAL-.###",
+ }
+ ).name
+
+ warehouse = "_Test Warehouse - _TC"
+
+ # Step - 1: Create a Backdated Purchase Receipt
+
+ pr1 = make_purchase_receipt(
+ item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -3)
+ )
+ pr1.reload()
+
+ serial_nos = sorted(get_serial_nos(pr1.items[0].serial_no))[:5]
+
+ # Step - 2: Create a Stock Reconciliation
+ sr1 = create_stock_reconciliation(
+ item_code=item_code,
+ warehouse=warehouse,
+ qty=5,
+ serial_no="\n".join(serial_nos),
+ )
+
+ data = frappe.get_all(
+ "Stock Ledger Entry",
+ fields=["serial_no", "actual_qty", "stock_value_difference"],
+ filters={"voucher_no": sr1.name, "is_cancelled": 0},
+ order_by="creation",
+ )
+
+ for d in data:
+ if d.actual_qty < 0:
+ self.assertEqual(d.actual_qty, -10.0)
+ self.assertAlmostEqual(d.stock_value_difference, -1000.0)
+ else:
+ self.assertEqual(d.actual_qty, 5.0)
+ self.assertAlmostEqual(d.stock_value_difference, 500.0)
+
+ # Step - 3: Create a Purchase Receipt before the first Purchase Receipt
+ make_purchase_receipt(
+ item_code=item_code, warehouse=warehouse, qty=10, rate=200, posting_date=add_days(nowdate(), -5)
+ )
+
+ data = frappe.get_all(
+ "Stock Ledger Entry",
+ fields=["serial_no", "actual_qty", "stock_value_difference"],
+ filters={"voucher_no": sr1.name, "is_cancelled": 0},
+ order_by="creation",
+ )
+
+ for d in data:
+ if d.actual_qty < 0:
+ self.assertEqual(d.actual_qty, -20.0)
+ self.assertAlmostEqual(d.stock_value_difference, -3000.0)
+ else:
+ self.assertEqual(d.actual_qty, 5.0)
+ self.assertAlmostEqual(d.stock_value_difference, 500.0)
+
+ active_serial_no = frappe.get_all(
+ "Serial No", filters={"status": "Active", "item_code": item_code}
+ )
+ self.assertEqual(len(active_serial_no), 5)
+
def create_batch_item_with_batch(item_name, batch_id):
batch_item_doc = create_item(item_name, is_stock_item=1)
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index df1f544d7b1..ef1b0cda4ff 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -1,7 +1,6 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
-import copy
import json
from typing import Optional, Set, Tuple
@@ -27,10 +26,6 @@ class NegativeStockError(frappe.ValidationError):
pass
-class SerialNoExistsInFutureTransaction(frappe.ValidationError):
- pass
-
-
def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
"""Create SL entries from SL entry dicts
@@ -54,9 +49,6 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
future_sle_exists(args, sl_entries)
for sle in sl_entries:
- if sle.serial_no and not via_landed_cost_voucher:
- validate_serial_no(sle)
-
if cancel:
sle["actual_qty"] = -flt(sle.get("actual_qty"))
@@ -133,35 +125,6 @@ def get_args_for_future_sle(row):
)
-def validate_serial_no(sle):
- from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
-
- for sn in get_serial_nos(sle.serial_no):
- args = copy.deepcopy(sle)
- args.serial_no = sn
- args.warehouse = ""
-
- vouchers = []
- for row in get_stock_ledger_entries(args, ">"):
- voucher_type = frappe.bold(row.voucher_type)
- voucher_no = frappe.bold(get_link_to_form(row.voucher_type, row.voucher_no))
- vouchers.append(f"{voucher_type} {voucher_no}")
-
- if vouchers:
- serial_no = frappe.bold(sn)
- msg = (
- f"""The serial no {serial_no} has been used in the future transactions so you need to cancel them first.
- The list of the transactions are as below."""
- + "