test: test case to check use serial / batch fields feature
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 9eed6a4..74c835c 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -21,6 +21,9 @@
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import (
get_evaluated_inventory_dimension,
)
+from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
+ get_type_of_transaction,
+)
from erpnext.stock.stock_ledger import get_items_to_be_repost
@@ -150,6 +153,13 @@
if row.use_serial_batch_fields and (
not row.serial_and_batch_bundle and not row.get("rejected_serial_and_batch_bundle")
):
+ if self.doctype == "Stock Reconciliation":
+ qty = row.qty
+ type_of_transaction = "Inward"
+ else:
+ qty = row.stock_qty
+ type_of_transaction = get_type_of_transaction(self, row)
+
sn_doc = SerialBatchCreation(
{
"item_code": row.item_code,
@@ -159,14 +169,15 @@
"voucher_type": self.doctype,
"voucher_no": self.name,
"voucher_detail_no": row.name,
- "qty": row.stock_qty,
- "type_of_transaction": "Inward" if row.stock_qty > 0 else "Outward",
+ "qty": qty,
+ "type_of_transaction": type_of_transaction,
"company": self.company,
"is_rejected": 1 if row.get("rejected_warehouse") else 0,
"serial_nos": get_serial_nos(row.serial_no) if row.serial_no else None,
- "batches": frappe._dict({row.batch_no: row.stock_qty}) if row.batch_no else None,
+ "batches": frappe._dict({row.batch_no: qty}) if row.batch_no else None,
"batch_no": row.batch_no,
"use_serial_batch_fields": row.use_serial_batch_fields,
+ "do_not_submit": True,
}
).make_serial_and_batch_bundle()
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index b19361c..459e7e7 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -1566,7 +1566,7 @@
dn.return_against = args.return_against
bundle_id = None
- if args.get("batch_no") or args.get("serial_no"):
+ if not args.use_serial_batch_fields and (args.get("batch_no") or args.get("serial_no")):
type_of_transaction = args.type_of_transaction or "Outward"
if dn.is_return:
@@ -1608,6 +1608,9 @@
"expense_account": args.expense_account or "Cost of Goods Sold - _TC",
"cost_center": args.cost_center or "_Test Cost Center - _TC",
"target_warehouse": args.target_warehouse,
+ "use_serial_batch_fields": args.use_serial_batch_fields,
+ "serial_no": args.serial_no if args.use_serial_batch_fields else None,
+ "batch_no": args.batch_no if args.use_serial_batch_fields else None,
},
)
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index 65c08c1..2d20922 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -2230,6 +2230,93 @@
pr_doc.reload()
self.assertFalse(pr_doc.items[0].from_warehouse)
+ def test_use_serial_batch_fields_for_serial_nos(self):
+ from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
+ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
+ create_stock_reconciliation,
+ )
+
+ item_code = make_item(
+ "_Test Use Serial Fields Item Serial Item",
+ properties={"has_serial_no": 1, "serial_no_series": "SNU-TSFISI-.#####"},
+ ).name
+
+ serial_nos = [
+ "SNU-TSFISI-000011",
+ "SNU-TSFISI-000012",
+ "SNU-TSFISI-000013",
+ "SNU-TSFISI-000014",
+ "SNU-TSFISI-000015",
+ ]
+
+ pr = make_purchase_receipt(
+ item_code=item_code,
+ qty=5,
+ serial_no="\n".join(serial_nos),
+ use_serial_batch_fields=1,
+ rate=100,
+ )
+
+ self.assertEqual(pr.items[0].use_serial_batch_fields, 1)
+ self.assertFalse(pr.items[0].serial_no)
+ self.assertTrue(pr.items[0].serial_and_batch_bundle)
+
+ sbb_doc = frappe.get_doc("Serial and Batch Bundle", pr.items[0].serial_and_batch_bundle)
+
+ for row in sbb_doc.entries:
+ self.assertTrue(row.serial_no in serial_nos)
+
+ serial_nos.remove("SNU-TSFISI-000015")
+
+ sr = create_stock_reconciliation(
+ item_code=item_code,
+ serial_no="\n".join(serial_nos),
+ qty=4,
+ warehouse=pr.items[0].warehouse,
+ use_serial_batch_fields=1,
+ do_not_submit=True,
+ )
+ sr.reload()
+
+ serial_nos = get_serial_nos(sr.items[0].current_serial_no)
+ self.assertEqual(len(serial_nos), 5)
+ self.assertEqual(sr.items[0].current_qty, 5)
+
+ new_serial_nos = get_serial_nos(sr.items[0].serial_no)
+ self.assertEqual(len(new_serial_nos), 4)
+ self.assertEqual(sr.items[0].qty, 4)
+ self.assertEqual(sr.items[0].use_serial_batch_fields, 1)
+ self.assertFalse(sr.items[0].current_serial_and_batch_bundle)
+ self.assertFalse(sr.items[0].serial_and_batch_bundle)
+ self.assertTrue(sr.items[0].current_serial_no)
+ sr.submit()
+
+ sr.reload()
+ self.assertTrue(sr.items[0].current_serial_and_batch_bundle)
+ self.assertTrue(sr.items[0].serial_and_batch_bundle)
+
+ serial_no_status = frappe.db.get_value("Serial No", "SNU-TSFISI-000015", "status")
+
+ self.assertTrue(serial_no_status != "Active")
+
+ dn = create_delivery_note(
+ item_code=item_code,
+ qty=4,
+ serial_no="\n".join(new_serial_nos),
+ use_serial_batch_fields=1,
+ )
+
+ self.assertTrue(dn.items[0].serial_and_batch_bundle)
+ self.assertEqual(dn.items[0].qty, 4)
+ doc = frappe.get_doc("Serial and Batch Bundle", dn.items[0].serial_and_batch_bundle)
+ for row in doc.entries:
+ self.assertTrue(row.serial_no in new_serial_nos)
+
+ for sn in new_serial_nos:
+ serial_no_status = frappe.db.get_value("Serial No", sn, "status")
+ self.assertTrue(serial_no_status != "Active")
+
def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
@@ -2399,7 +2486,7 @@
uom = args.uom or frappe.db.get_value("Item", item_code, "stock_uom") or "_Test UOM"
bundle_id = None
- if args.get("batch_no") or args.get("serial_no"):
+ if not args.use_serial_batch_fields and (args.get("batch_no") or args.get("serial_no")):
batches = {}
if args.get("batch_no"):
batches = frappe._dict({args.batch_no: qty})
@@ -2441,6 +2528,9 @@
"cost_center": args.cost_center
or frappe.get_cached_value("Company", pr.company, "cost_center"),
"asset_location": args.location or "Test Location",
+ "use_serial_batch_fields": args.use_serial_batch_fields or 0,
+ "serial_no": args.serial_no if args.use_serial_batch_fields else "",
+ "batch_no": args.batch_no if args.use_serial_batch_fields else "",
},
)
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 ea33c54..eb4df29 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
@@ -1117,7 +1117,7 @@
if isinstance(data, list):
return data
- return [s.strip() for s in cstr(data).strip().upper().replace(",", "\n").split("\n") if s.strip()]
+ return [s.strip() for s in cstr(data).strip().replace(",", "\n").split("\n") if s.strip()]
@frappe.whitelist()
@@ -1256,7 +1256,7 @@
def get_type_of_transaction(parent_doc, child_row):
- type_of_transaction = child_row.type_of_transaction
+ type_of_transaction = child_row.get("type_of_transaction")
if parent_doc.get("doctype") == "Stock Entry":
type_of_transaction = "Outward" if child_row.s_warehouse else "Inward"
@@ -1384,6 +1384,8 @@
filters = {"item_code": kwargs.item_code}
+ # ignore_warehouse is used for backdated stock transactions
+ # There might be chances that the serial no not exists in the warehouse during backdated stock transactions
if not kwargs.get("ignore_warehouse"):
filters["warehouse"] = ("is", "set")
if kwargs.warehouse:
diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py
index 122664c..5f4f393 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.py
+++ b/erpnext/stock/doctype/serial_no/serial_no.py
@@ -151,9 +151,7 @@
if isinstance(serial_no, list):
return serial_no
- return [
- s.strip() for s in cstr(serial_no).strip().upper().replace(",", "\n").split("\n") if s.strip()
- ]
+ return [s.strip() for s in cstr(serial_no).strip().replace(",", "\n").split("\n") if s.strip()]
def clean_serial_no_string(serial_no: str) -> str:
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
index 8e9dcb0..ba7f9c5 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
@@ -198,6 +198,7 @@
frappe.model.set_value(cdt, cdn, "current_amount", r.message.rate * r.message.qty);
frappe.model.set_value(cdt, cdn, "amount", row.qty * row.valuation_rate);
frappe.model.set_value(cdt, cdn, "current_serial_no", r.message.serial_nos);
+ frappe.model.set_value(cdt, cdn, "use_serial_batch_fields", r.message.use_serial_batch_fields);
if (frm.doc.purpose == "Stock Reconciliation" && !frm.doc.scan_mode) {
frappe.model.set_value(cdt, cdn, "serial_no", r.message.serial_nos);
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index cc8a7c5..ce08615 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -99,6 +99,7 @@
)
def on_submit(self):
+ self.make_bundle_for_current_qty()
self.make_bundle_using_old_serial_batch_fields()
self.update_stock_ledger()
self.make_gl_entries()
@@ -117,9 +118,52 @@
self.repost_future_sle_and_gle()
self.delete_auto_created_batches()
+ def make_bundle_for_current_qty(self):
+ from erpnext.stock.serial_batch_bundle import SerialBatchCreation
+
+ for row in self.items:
+ if not row.use_serial_batch_fields:
+ continue
+
+ if row.current_serial_and_batch_bundle:
+ continue
+
+ if row.current_qty and (row.current_serial_no or row.batch_no):
+ sn_doc = SerialBatchCreation(
+ {
+ "item_code": row.item_code,
+ "warehouse": row.warehouse,
+ "posting_date": self.posting_date,
+ "posting_time": self.posting_time,
+ "voucher_type": self.doctype,
+ "voucher_no": self.name,
+ "voucher_detail_no": row.name,
+ "qty": row.qty,
+ "type_of_transaction": "Outward",
+ "company": self.company,
+ "is_rejected": 0,
+ "serial_nos": get_serial_nos(row.current_serial_no) if row.current_serial_no else None,
+ "batches": frappe._dict({row.batch_no: row.qty}) if row.batch_no else None,
+ "batch_no": row.batch_no,
+ "do_not_submit": True,
+ }
+ ).make_serial_and_batch_bundle()
+
+ row.current_serial_and_batch_bundle = sn_doc.name
+ row.db_set(
+ {
+ "current_serial_and_batch_bundle": sn_doc.name,
+ "current_serial_no": "",
+ "batch_no": "",
+ }
+ )
+
def set_current_serial_and_batch_bundle(self, voucher_detail_no=None, save=False) -> None:
"""Set Serial and Batch Bundle for each item"""
for item in self.items:
+ if not save and item.use_serial_batch_fields:
+ continue
+
if voucher_detail_no and voucher_detail_no != item.name:
continue
@@ -230,6 +274,9 @@
def set_new_serial_and_batch_bundle(self):
for item in self.items:
+ if item.use_serial_batch_fields:
+ continue
+
if not item.qty:
continue
@@ -292,8 +339,10 @@
inventory_dimensions_dict=inventory_dimensions_dict,
)
- if (item.qty is None or item.qty == item_dict.get("qty")) and (
- item.valuation_rate is None or item.valuation_rate == item_dict.get("rate")
+ if (
+ (item.qty is None or item.qty == item_dict.get("qty"))
+ and (item.valuation_rate is None or item.valuation_rate == item_dict.get("rate"))
+ and (not item.serial_no or (item.serial_no == item_dict.get("serial_nos")))
):
return False
else:
@@ -304,6 +353,11 @@
if item.valuation_rate is None:
item.valuation_rate = item_dict.get("rate")
+ if item_dict.get("serial_nos"):
+ item.current_serial_no = item_dict.get("serial_nos")
+ if self.purpose == "Stock Reconciliation" and not item.serial_no and item.qty:
+ item.serial_no = item.current_serial_no
+
item.current_qty = item_dict.get("qty")
item.current_valuation_rate = item_dict.get("rate")
self.calculate_difference_amount(item, item_dict)
@@ -1136,9 +1190,16 @@
has_serial_no = bool(item_dict.get("has_serial_no"))
has_batch_no = bool(item_dict.get("has_batch_no"))
+ use_serial_batch_fields = frappe.db.get_single_value("Stock Settings", "use_serial_batch_fields")
+
if not batch_no and has_batch_no:
# Not enough information to fetch data
- return {"qty": 0, "rate": 0, "serial_nos": None}
+ return {
+ "qty": 0,
+ "rate": 0,
+ "serial_nos": None,
+ "use_serial_batch_fields": use_serial_batch_fields,
+ }
# TODO: fetch only selected batch's values
data = get_stock_balance(
@@ -1161,7 +1222,12 @@
get_batch_qty(batch_no, warehouse, posting_date=posting_date, posting_time=posting_time) or 0
)
- return {"qty": qty, "rate": rate, "serial_nos": serial_nos}
+ return {
+ "qty": qty,
+ "rate": rate,
+ "serial_nos": serial_nos,
+ "use_serial_batch_fields": use_serial_batch_fields,
+ }
@frappe.whitelist()
diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
index 0bbfed4..479a74a 100644
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -1094,7 +1094,7 @@
)
bundle_id = None
- if args.batch_no or args.serial_no:
+ if not args.use_serial_batch_fields and (args.batch_no or args.serial_no):
batches = frappe._dict({})
if args.batch_no:
batches[args.batch_no] = args.qty
@@ -1125,7 +1125,10 @@
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"qty": args.qty,
"valuation_rate": args.rate,
+ "serial_no": args.serial_no if args.use_serial_batch_fields else None,
+ "batch_no": args.batch_no if args.use_serial_batch_fields else None,
"serial_and_batch_bundle": bundle_id,
+ "use_serial_batch_fields": args.use_serial_batch_fields,
},
)
diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json
index 344486c..c698283 100644
--- a/erpnext/stock/doctype/stock_settings/stock_settings.json
+++ b/erpnext/stock/doctype/stock_settings/stock_settings.json
@@ -423,7 +423,7 @@
"label": "Auto Reserve Stock for Sales Order on Purchase"
},
{
- "default": "0",
+ "default": "1",
"fieldname": "use_serial_batch_fields",
"fieldtype": "Check",
"label": "Use Serial / Batch Fields"
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index 76af5d7..9eac172 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -11,6 +11,9 @@
from frappe.utils import cstr, flt, get_link_to_form, nowdate, nowtime
import erpnext
+from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
+ get_available_serial_nos,
+)
from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses
from erpnext.stock.serial_batch_bundle import BatchNoValuation, SerialNoValuation
from erpnext.stock.valuation import FIFOValuation, LIFOValuation
@@ -125,7 +128,21 @@
if with_valuation_rate:
if with_serial_no:
- serial_nos = get_serial_nos_data_after_transactions(args)
+ serial_no_details = get_available_serial_nos(
+ frappe._dict(
+ {
+ "item_code": item_code,
+ "warehouse": warehouse,
+ "posting_date": posting_date,
+ "posting_time": posting_time,
+ "ignore_warehouse": 1,
+ }
+ )
+ )
+
+ serial_nos = ""
+ if serial_no_details:
+ serial_nos = "\n".join(d.serial_no for d in serial_no_details)
return (
(last_entry.qty_after_transaction, last_entry.valuation_rate, serial_nos)
@@ -140,38 +157,6 @@
return last_entry.qty_after_transaction if last_entry else 0.0
-def get_serial_nos_data_after_transactions(args):
-
- serial_nos = set()
- args = frappe._dict(args)
- sle = frappe.qb.DocType("Stock Ledger Entry")
-
- stock_ledger_entries = (
- frappe.qb.from_(sle)
- .select("serial_no", "actual_qty")
- .where(
- (sle.item_code == args.item_code)
- & (sle.warehouse == args.warehouse)
- & (
- CombineDatetime(sle.posting_date, sle.posting_time)
- < CombineDatetime(args.posting_date, args.posting_time)
- )
- & (sle.is_cancelled == 0)
- )
- .orderby(sle.posting_date, sle.posting_time, sle.creation)
- .run(as_dict=1)
- )
-
- for stock_ledger_entry in stock_ledger_entries:
- changed_serial_no = get_serial_nos_data(stock_ledger_entry.serial_no)
- if stock_ledger_entry.actual_qty > 0:
- serial_nos.update(changed_serial_no)
- else:
- serial_nos.difference_update(changed_serial_no)
-
- return "\n".join(serial_nos)
-
-
def get_serial_nos_data(serial_nos):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos