refactor: serial no ledger and batchwise balance history report
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 6051c99..48fef18 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -2573,7 +2573,7 @@
"posting_date": si.posting_date,
"posting_time": si.posting_time,
"qty": -1 * flt(d.get("stock_qty")),
- "serial_no": d.serial_no,
+ "serial_and_batch_bundle": d.serial_and_batch_bundle,
"company": si.company,
"voucher_type": "Sales Invoice",
"voucher_no": si.name,
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index 80275de..71fee9f 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -323,8 +323,6 @@
def make_return_doc(doctype: str, source_name: str, target_doc=None):
from frappe.model.mapper import get_mapped_doc
- from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
-
company = frappe.db.get_value("Delivery Note", source_name, "company")
default_warehouse_for_sales_return = frappe.get_cached_value(
"Company", company, "default_warehouse_for_sales_return"
@@ -392,23 +390,51 @@
doc.run_method("calculate_taxes_and_totals")
def update_item(source_doc, target_doc, source_parent):
+ from erpnext.stock.serial_batch_bundle import SerialBatchCreation
+
target_doc.qty = -1 * source_doc.qty
- if source_doc.serial_no:
- returned_serial_nos = get_returned_serial_nos(source_doc, source_parent)
- serial_nos = list(set(get_serial_nos(source_doc.serial_no)) - set(returned_serial_nos))
- if serial_nos:
- target_doc.serial_no = "\n".join(serial_nos)
+ if source_doc.get("serial_and_batch_bundle"):
+ type_of_transaction = "Inward"
+ if (
+ frappe.db.get_value(
+ "Serial and Batch Bundle", source_doc.serial_and_batch_bundle, "type_of_transaction"
+ )
+ == "Inward"
+ ):
+ type_of_transaction = "Outward"
- if source_doc.get("rejected_serial_no"):
- returned_serial_nos = get_returned_serial_nos(
- source_doc, source_parent, serial_no_field="rejected_serial_no"
+ cls_obj = SerialBatchCreation(
+ {
+ "type_of_transaction": type_of_transaction,
+ "serial_and_batch_bundle": source_doc.serial_and_batch_bundle,
+ }
)
- rejected_serial_nos = list(
- set(get_serial_nos(source_doc.rejected_serial_no)) - set(returned_serial_nos)
+
+ cls_obj.duplicate_package()
+ if cls_obj.serial_and_batch_bundle:
+ target_doc.serial_and_batch_bundle = cls_obj.serial_and_batch_bundle
+
+ if source_doc.get("rejected_serial_and_batch_bundle"):
+ type_of_transaction = "Inward"
+ if (
+ frappe.db.get_value(
+ "Serial and Batch Bundle", source_doc.rejected_serial_and_batch_bundle, "type_of_transaction"
+ )
+ == "Inward"
+ ):
+ type_of_transaction = "Outward"
+
+ cls_obj = SerialBatchCreation(
+ {
+ "type_of_transaction": type_of_transaction,
+ "serial_and_batch_bundle": source_doc.rejected_serial_and_batch_bundle,
+ }
)
- if rejected_serial_nos:
- target_doc.rejected_serial_no = "\n".join(rejected_serial_nos)
+
+ cls_obj.duplicate_package()
+ if cls_obj.serial_and_batch_bundle:
+ target_doc.serial_and_batch_bundle = cls_obj.serial_and_batch_bundle
if doctype in ["Purchase Receipt", "Subcontracting Receipt"]:
returned_qty_map = get_returned_qty_map_for_row(
diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py
index b929883..814657d 100644
--- a/erpnext/controllers/subcontracting_controller.py
+++ b/erpnext/controllers/subcontracting_controller.py
@@ -294,13 +294,13 @@
for batch_no, qty in consumed_bundles.batch_nos.items():
self.available_materials[key]["batch_no"][batch_no] -= abs(qty)
- # Will be deperecated in v16
+ # Will be deprecated in v16
if row.serial_no:
self.available_materials[key]["serial_no"] = list(
set(self.available_materials[key]["serial_no"]) - set(get_serial_nos(row.serial_no))
)
- # Will be deperecated in v16
+ # Will be deprecated in v16
if row.batch_no:
self.available_materials[key]["batch_no"][row.batch_no] -= row.consumed_qty
@@ -814,8 +814,7 @@
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"qty": -1 * item.consumed_qty,
- "serial_no": item.serial_no,
- "batch_no": item.batch_no,
+ "serial_and_batch_bundle": item.serial_and_batch_bundle,
}
)
diff --git a/erpnext/stock/deprecated_serial_batch.py b/erpnext/stock/deprecated_serial_batch.py
index 14717c6..ac30f82 100644
--- a/erpnext/stock/deprecated_serial_batch.py
+++ b/erpnext/stock/deprecated_serial_batch.py
@@ -4,7 +4,7 @@
class DeprecatedSerialNoValuation:
- # Will be deperecated in v16
+ # Will be deprecated in v16
def calculate_stock_value_from_deprecarated_ledgers(self):
serial_nos = list(
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 c06f63f..311b35f 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
@@ -11,7 +11,7 @@
from frappe.utils import add_days, cint, flt, get_link_to_form, today
from pypika import Case
-from erpnext.stock.serial_batch_bundle import BatchNoBundleValuation, SerialNoBundleValuation
+from erpnext.stock.serial_batch_bundle import BatchNoValuation, SerialNoValuation
class SerialNoExistsInFutureTransactionError(frappe.ValidationError):
@@ -81,14 +81,14 @@
def set_incoming_rate_for_outward_transaction(self, row=None, save=False):
sle = self.get_sle_for_outward_transaction(row)
if self.has_serial_no:
- sn_obj = SerialNoBundleValuation(
+ sn_obj = SerialNoValuation(
sle=sle,
warehouse=self.item_code,
item_code=self.warehouse,
)
else:
- sn_obj = BatchNoBundleValuation(
+ sn_obj = BatchNoValuation(
sle=sle,
warehouse=self.item_code,
item_code=self.warehouse,
@@ -187,9 +187,12 @@
self.set_incoming_rate(save=True, row=row)
self.calculate_qty_and_amount(save=True)
self.validate_quantity(row)
- self.set_warranty_expiry_date(row)
+ self.set_warranty_expiry_date()
def set_warranty_expiry_date(self):
+ if self.type_of_transaction != "Outward":
+ return
+
if not (self.docstatus == 1 and self.voucher_type == "Delivery Note" and self.has_serial_no):
return
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js
index e0c32e4..6ffe5b3 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -493,8 +493,7 @@
'item_code': child.item_code,
'warehouse': cstr(child.s_warehouse) || cstr(child.t_warehouse),
'transfer_qty': child.transfer_qty,
- 'serial_no': child.serial_no,
- 'batch_no': child.batch_no,
+ 'serial_and_batch_bundle': child.serial_and_batch_bundle,
'qty': child.s_warehouse ? -1* child.transfer_qty : child.transfer_qty,
'posting_date': frm.doc.posting_date,
'posting_time': frm.doc.posting_time,
diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
index a902655..7b3d7f4 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
@@ -92,6 +92,16 @@
as_dict=1,
)
+ values_to_be_change = {}
+ if self.has_batch_no != item_detail.has_batch_no:
+ values_to_be_change["has_batch_no"] = item_detail.has_batch_no
+
+ if self.has_serial_no != item_detail.has_serial_no:
+ values_to_be_change["has_serial_no"] = item_detail.has_serial_no
+
+ if values_to_be_change:
+ self.db_set(values_to_be_change)
+
if not item_detail:
frappe.throw(_("Item {0} not found").format(self.item_code))
diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
index 621b9df..66bef50 100644
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -157,7 +157,9 @@
item_code=serial_item_code, warehouse=serial_warehouse, qty=5, rate=200
)
- serial_nos = get_serial_nos(sr.items[0].serial_no)
+ serial_nos = frappe.get_doc(
+ "Serial and Batch Bundle", sr.items[0].serial_and_batch_bundle
+ ).get_serial_nos()
self.assertEqual(len(serial_nos), 5)
args = {
@@ -165,7 +167,7 @@
"warehouse": serial_warehouse,
"posting_date": nowdate(),
"posting_time": nowtime(),
- "serial_no": sr.items[0].serial_no,
+ "serial_and_batch_bundle": sr.items[0].serial_and_batch_bundle,
}
valuation_rate = get_incoming_rate(args)
@@ -177,7 +179,10 @@
item_code=serial_item_code, warehouse=serial_warehouse, qty=5, rate=300
)
- serial_nos1 = get_serial_nos(sr.items[0].serial_no)
+ serial_nos1 = frappe.get_doc(
+ "Serial and Batch Bundle", sr.items[0].serial_and_batch_bundle
+ ).get_serial_nos()
+
self.assertEqual(len(serial_nos1), 5)
args = {
@@ -185,7 +190,7 @@
"warehouse": serial_warehouse,
"posting_date": nowdate(),
"posting_time": nowtime(),
- "serial_no": sr.items[0].serial_no,
+ "serial_and_batch_bundle": sr.items[0].serial_and_batch_bundle,
}
valuation_rate = get_incoming_rate(args)
@@ -257,7 +262,7 @@
sr.save()
sr.submit()
- batch_no = sr.items[0].batch_no
+ batch_no = sr.items[0].serial_and_batch_bundle
self.assertTrue(batch_no)
to_delete_records.append(sr.name)
diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py
index 0d57938..2c46082 100644
--- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py
+++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py
@@ -67,8 +67,16 @@
return columns
-# get all details
def get_stock_ledger_entries(filters):
+ # Will be deprecated in v16
+ entries = get_stock_ledger_entries_for_batch_no(filters)
+
+ entries += get_stock_ledger_entries_for_batch_bundle(filters)
+ return entries
+
+
+# get all details
+def get_stock_ledger_entries_for_batch_no(filters):
if not filters.get("from_date"):
frappe.throw(_("'From Date' is required"))
if not filters.get("to_date"):
@@ -99,7 +107,43 @@
if filters.get(field):
query = query.where(sle[field] == filters.get(field))
- return query.run(as_dict=True)
+ return query.run(as_dict=True) or []
+
+
+def get_stock_ledger_entries_for_batch_bundle(filters):
+ sle = frappe.qb.DocType("Stock Ledger Entry")
+ batch_package = frappe.qb.DocType("Serial and Batch Entry")
+
+ query = (
+ frappe.qb.from_(sle)
+ .inner_join(batch_package)
+ .on(batch_package.parent == sle.serial_and_batch_bundle)
+ .select(
+ sle.item_code,
+ sle.warehouse,
+ batch_package.batch_no,
+ sle.posting_date,
+ fn.Sum(batch_package.qty).as_("actual_qty"),
+ )
+ .where(
+ (sle.docstatus < 2)
+ & (sle.is_cancelled == 0)
+ & (sle.has_batch_no == 1)
+ & (sle.posting_date <= filters["to_date"])
+ )
+ .groupby(sle.voucher_no, sle.batch_no, sle.item_code, sle.warehouse)
+ .orderby(sle.item_code, sle.warehouse)
+ )
+
+ query = apply_warehouse_filter(query, sle, filters)
+ for field in ["item_code", "batch_no", "company"]:
+ if filters.get(field):
+ if field == "batch_no":
+ query = query.where(batch_package[field] == filters.get(field))
+ else:
+ query = query.where(sle[field] == filters.get(field))
+
+ return query.run(as_dict=True) or []
def get_item_warehouse_batch_map(filters, float_precision):
diff --git a/erpnext/stock/report/serial_no_ledger/serial_no_ledger.js b/erpnext/stock/report/serial_no_ledger/serial_no_ledger.js
index 616312e..976e515 100644
--- a/erpnext/stock/report/serial_no_ledger/serial_no_ledger.js
+++ b/erpnext/stock/report/serial_no_ledger/serial_no_ledger.js
@@ -19,13 +19,6 @@
}
},
{
- 'label': __('Serial No'),
- 'fieldtype': 'Link',
- 'fieldname': 'serial_no',
- 'options': 'Serial No',
- 'reqd': 1
- },
- {
'label': __('Warehouse'),
'fieldtype': 'Link',
'fieldname': 'warehouse',
@@ -43,10 +36,35 @@
}
},
{
+ 'label': __('Serial No'),
+ 'fieldtype': 'Link',
+ 'fieldname': 'serial_no',
+ 'options': 'Serial No',
+ get_query: function() {
+ let item_code = frappe.query_report.get_filter_value('item_code');
+ let warehouse = frappe.query_report.get_filter_value('warehouse');
+
+ let query_filters = {'item_code': item_code};
+ if (warehouse) {
+ query_filters['warehouse'] = warehouse;
+ }
+
+ return {
+ filters: query_filters
+ }
+ }
+ },
+ {
'label': __('As On Date'),
'fieldtype': 'Date',
'fieldname': 'posting_date',
'default': frappe.datetime.get_today()
},
+ {
+ 'label': __('Posting Time'),
+ 'fieldtype': 'Time',
+ 'fieldname': 'posting_time',
+ 'default': frappe.datetime.get_time()
+ },
]
};
diff --git a/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py b/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py
index e439f51..99f1a94 100644
--- a/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py
+++ b/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py
@@ -1,7 +1,7 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
-
+import frappe
from frappe import _
from erpnext.stock.stock_ledger import get_stock_ledger_entries
@@ -45,10 +45,71 @@
"options": "Warehouse",
"width": 220,
},
+ {
+ "label": _("Serial No"),
+ "fieldtype": "Link",
+ "fieldname": "serial_no",
+ "options": "Serial No",
+ "width": 220,
+ },
]
return columns
def get_data(filters):
- return get_stock_ledger_entries(filters, "<=", order="asc") or []
+ stock_ledgers = get_stock_ledger_entries(filters, "<=", order="asc", check_serial_no=False)
+
+ if not stock_ledgers:
+ return []
+
+ data = []
+ serial_bundle_ids = [
+ d.serial_and_batch_bundle for d in stock_ledgers if d.serial_and_batch_bundle
+ ]
+
+ bundle_wise_serial_nos = get_serial_nos(filters, serial_bundle_ids)
+
+ for row in stock_ledgers:
+ args = frappe._dict(
+ {
+ "posting_date": row.posting_date,
+ "posting_time": row.posting_time,
+ "voucher_type": row.voucher_type,
+ "voucher_no": row.voucher_no,
+ "company": row.company,
+ "warehouse": row.warehouse,
+ }
+ )
+
+ serial_nos = bundle_wise_serial_nos.get(row.serial_and_batch_bundle, [])
+
+ for index, serial_no in enumerate(serial_nos):
+ if index == 0:
+ args.serial_no = serial_no
+ data.append(args)
+ else:
+ data.append(
+ {
+ "serial_no": serial_no,
+ }
+ )
+
+ return data
+
+
+def get_serial_nos(filters, serial_bundle_ids):
+ bundle_wise_serial_nos = {}
+ bundle_filters = {"parent": ["in", serial_bundle_ids]}
+ if filters.get("serial_no"):
+ bundle_filters["serial_no"] = filters.get("serial_no")
+
+ for d in frappe.get_all(
+ "Serial and Batch Entry",
+ fields=["serial_no", "parent"],
+ filters=bundle_filters,
+ order_by="idx asc",
+ ):
+ bundle_wise_serial_nos.setdefault(d.parent, []).append(d.serial_no)
+
+ return bundle_wise_serial_nos
diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py
index f2de819..1266133 100644
--- a/erpnext/stock/serial_batch_bundle.py
+++ b/erpnext/stock/serial_batch_bundle.py
@@ -341,7 +341,7 @@
return [d.serial_no for d in entries]
-class SerialNoBundleValuation(DeprecatedSerialNoValuation):
+class SerialNoValuation(DeprecatedSerialNoValuation):
def __init__(self, **kwargs):
for key, value in kwargs.items():
setattr(self, key, value)
@@ -470,7 +470,7 @@
return False
-class BatchNoBundleValuation(DeprecatedBatchNoValuation):
+class BatchNoValuation(DeprecatedBatchNoValuation):
def __init__(self, **kwargs):
for key, value in kwargs.items():
setattr(self, key, value)
@@ -567,11 +567,11 @@
def get_empty_batches_based_work_order(work_order, item_code):
- batches = get_batches_from_work_order(work_order)
+ batches = get_batches_from_work_order(work_order, item_code)
if not batches:
return batches
- entries = get_batches_from_stock_entries(work_order)
+ entries = get_batches_from_stock_entries(work_order, item_code)
if not entries:
return batches
@@ -589,15 +589,18 @@
return batches
-def get_batches_from_work_order(work_order):
+def get_batches_from_work_order(work_order, item_code):
return frappe._dict(
frappe.get_all(
- "Batch", fields=["name", "qty_to_produce"], filters={"reference_name": work_order}, as_list=1
+ "Batch",
+ fields=["name", "qty_to_produce"],
+ filters={"reference_name": work_order, "item": item_code},
+ as_list=1,
)
)
-def get_batches_from_stock_entries(work_order):
+def get_batches_from_stock_entries(work_order, item_code):
entries = frappe.get_all(
"Stock Entry",
filters={"work_order": work_order, "docstatus": 1, "purpose": "Manufacture"},
@@ -610,6 +613,7 @@
filters={
"parent": ("in", [d.name for d in entries]),
"is_finished_item": 1,
+ "item_code": item_code,
},
)
@@ -623,3 +627,21 @@
for d in entries:
batches[d.batch_no] -= d.qty
+
+
+class SerialBatchCreation:
+ def __init__(self, args):
+ for key, value in args.items():
+ setattr(self, key, value)
+
+ def duplicate_package(self):
+ if not self.serial_and_batch_bundle:
+ return
+
+ id = self.serial_and_batch_bundle
+ package = frappe.get_doc("Serial and Batch Bundle", id)
+ new_package = frappe.copy_doc(package)
+ new_package.type_of_transaction = self.type_of_transaction
+ new_package.save()
+
+ self.serial_and_batch_bundle = new_package.name
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index dfb7786..e616ed0 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -27,7 +27,7 @@
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
get_sre_reserved_qty_for_item_and_warehouse as get_reserved_stock,
)
-from erpnext.stock.serial_batch_bundle import BatchNoBundleValuation, SerialNoBundleValuation
+from erpnext.stock.serial_batch_bundle import BatchNoValuation, SerialNoValuation
from erpnext.stock.utils import (
get_incoming_outgoing_rate_for_cancel,
get_or_make_bin,
@@ -693,7 +693,7 @@
if sle.serial_and_batch_bundle:
if frappe.get_cached_value("Item", sle.item_code, "has_serial_no"):
- SerialNoBundleValuation(
+ SerialNoValuation(
sle=sle,
sle_self=self,
wh_data=self.wh_data,
@@ -701,7 +701,7 @@
item_code=sle.item_code,
)
else:
- BatchNoBundleValuation(
+ BatchNoValuation(
sle=sle,
sle_self=self,
wh_data=self.wh_data,
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index 18e0b90..8d1ec54 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -12,7 +12,7 @@
import erpnext
from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses
-from erpnext.stock.serial_batch_bundle import BatchNoBundleValuation, SerialNoBundleValuation
+from erpnext.stock.serial_batch_bundle import BatchNoValuation, SerialNoValuation
from erpnext.stock.valuation import FIFOValuation, LIFOValuation
BarcodeScanResult = Dict[str, Optional[str]]
@@ -264,7 +264,7 @@
if item_details.has_serial_no and args.get("serial_and_batch_bundle"):
args.actual_qty = args.qty
- sn_obj = SerialNoBundleValuation(
+ sn_obj = SerialNoValuation(
sle=args,
warehouse=args.get("warehouse"),
item_code=args.get("item_code"),
@@ -274,7 +274,7 @@
elif item_details.has_batch_no and args.get("serial_and_batch_bundle"):
args.actual_qty = args.qty
- batch_obj = BatchNoBundleValuation(
+ batch_obj = BatchNoValuation(
sle=args,
warehouse=args.get("warehouse"),
item_code=args.get("item_code"),