feat: get_valuation_rate batch wise
This function is used to show valuation rate on frontend and also as
fallback in case values aren't available. Add "batch_no" param to get
batch specific valuation rates.
Co-Authored-By: Alan Tom <2.alan.tom@gmail.com>
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index a181af7..b831557 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -249,6 +249,7 @@
"posting_time": self.get('posting_time'),
"qty": -1 * flt(d.get('stock_qty')),
"serial_no": d.get('serial_no'),
+ "batch_no": d.get("batch_no"),
"company": self.company,
"voucher_type": self.doctype,
"voucher_no": self.name,
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index df3c5f1..8c3aab4 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -420,6 +420,7 @@
"posting_time": sle.get('posting_time'),
"qty": sle.actual_qty,
"serial_no": sle.get('serial_no'),
+ "batch_no": sle.get("batch_no"),
"company": sle.company,
"voucher_type": sle.voucher_type,
"voucher_no": sle.voucher_no
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index 31b2209..e918cde 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -394,6 +394,7 @@
"posting_time": self.get('posting_time') or nowtime(),
"qty": qty if cint(self.get("is_return")) else (-1 * qty),
"serial_no": d.get('serial_no'),
+ "batch_no": d.get("batch_no"),
"company": self.company,
"voucher_type": self.doctype,
"voucher_no": self.name,
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 136e1ed..933ced0 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -719,6 +719,7 @@
'posting_time': posting_time,
'qty': item.qty * item.conversion_factor,
'serial_no': item.serial_no,
+ 'batch_no': item.batch_no,
'voucher_type': voucher_type,
'company': company,
'allow_zero_valuation_rate': item.allow_zero_valuation_rate
diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py
index e7d04db..73a48b3 100644
--- a/erpnext/stock/doctype/batch/test_batch.py
+++ b/erpnext/stock/doctype/batch/test_batch.py
@@ -8,7 +8,11 @@
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.stock.doctype.batch.batch import UnableToSelectBatchError, get_batch_no, get_batch_qty
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
+from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
+ create_stock_reconciliation,
+)
from erpnext.stock.get_item_details import get_item_details
+from erpnext.stock.stock_ledger import get_valuation_rate
from erpnext.tests.utils import ERPNextTestCase
@@ -345,6 +349,41 @@
self.assertEqual(sle.stock_queue, []) # queues don't apply on batched items
+ def test_moving_batch_valuation_rates(self):
+ item_code = "_TestBatchWiseVal"
+ warehouse = "_Test Warehouse - _TC"
+ self.make_batch_item(item_code)
+
+ def assertValuation(expected):
+ actual = get_valuation_rate(item_code, warehouse, "voucher_type", "voucher_no", batch_no=batch_no)
+ self.assertAlmostEqual(actual, expected)
+
+ se = make_stock_entry(item_code=item_code, qty=100, rate=10, target=warehouse)
+ batch_no = se.items[0].batch_no
+ assertValuation(10)
+
+ # consumption should never affect current valuation rate
+ make_stock_entry(item_code=item_code, qty=20, source=warehouse)
+ assertValuation(10)
+
+ make_stock_entry(item_code=item_code, qty=30, source=warehouse)
+ assertValuation(10)
+
+ # 50 * 10 = 500 current value, add more item with higher valuation
+ make_stock_entry(item_code=item_code, qty=50, rate=20, target=warehouse, batch_no=batch_no)
+ assertValuation(15)
+
+ # consuming again shouldn't do anything
+ make_stock_entry(item_code=item_code, qty=20, source=warehouse)
+ assertValuation(15)
+
+ # reset rate with stock reconiliation
+ create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=10, rate=25, batch_no=batch_no)
+ assertValuation(25)
+
+ make_stock_entry(item_code=item_code, qty=20, rate=20, target=warehouse, batch_no=batch_no)
+ assertValuation((20 * 20 + 10 * 25) / (10 + 20))
+
def create_batch(item_code, rate, create_item_price_for_batch):
pi = make_purchase_invoice(company="_Test Company",
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js
index c4b8131..5c9da3a 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -425,6 +425,7 @@
'posting_time' : frm.doc.posting_time,
'warehouse' : cstr(item.s_warehouse) || cstr(item.t_warehouse),
'serial_no' : item.serial_no,
+ 'batch_no' : item.batch_no,
'company' : frm.doc.company,
'qty' : item.s_warehouse ? -1*flt(item.transfer_qty) : flt(item.transfer_qty),
'voucher_type' : frm.doc.doctype,
@@ -457,6 +458,7 @@
'warehouse': cstr(child.s_warehouse) || cstr(child.t_warehouse),
'transfer_qty': child.transfer_qty,
'serial_no': child.serial_no,
+ 'batch_no': child.batch_no,
'qty': child.s_warehouse ? -1* child.transfer_qty : child.transfer_qty,
'posting_date': frm.doc.posting_date,
'posting_time': frm.doc.posting_time,
@@ -680,6 +682,7 @@
'warehouse' : cstr(d.s_warehouse) || cstr(d.t_warehouse),
'transfer_qty' : d.transfer_qty,
'serial_no' : d.serial_no,
+ 'batch_no' : d.batch_no,
'bom_no' : d.bom_no,
'expense_account' : d.expense_account,
'cost_center' : d.cost_center,
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 9ba007a..99cf4de 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -510,7 +510,7 @@
d.basic_rate = get_valuation_rate(d.item_code, d.t_warehouse,
self.doctype, self.name, d.allow_zero_valuation_rate,
currency=erpnext.get_company_currency(self.company), company=self.company,
- raise_error_if_no_rate=raise_error_if_no_rate)
+ raise_error_if_no_rate=raise_error_if_no_rate, batch_no=d.batch_no)
d.basic_rate = flt(d.basic_rate, d.precision("basic_rate"))
if d.is_process_loss:
@@ -541,6 +541,7 @@
"posting_time": self.posting_time,
"qty": item.s_warehouse and -1*flt(item.transfer_qty) or flt(item.transfer_qty),
"serial_no": item.serial_no,
+ "batch_no": item.batch_no,
"voucher_type": self.doctype,
"voucher_no": self.name,
"company": self.company,
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index c33cc12..53bfed8 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -634,7 +634,7 @@
if not allow_zero_rate:
self.wh_data.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse,
sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
- currency=erpnext.get_company_currency(sle.company), company=sle.company)
+ currency=erpnext.get_company_currency(sle.company), company=sle.company, batch_no=sle.batch_no)
def get_incoming_value_for_serial_nos(self, sle, serial_nos):
# get rate from serial nos within same company
@@ -702,7 +702,7 @@
if not allow_zero_valuation_rate:
self.wh_data.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse,
sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
- currency=erpnext.get_company_currency(sle.company), company=sle.company)
+ currency=erpnext.get_company_currency(sle.company), company=sle.company, batch_no=sle.batch_no)
def update_queue_values(self, sle):
incoming_rate = flt(sle.incoming_rate)
@@ -722,7 +722,7 @@
if not allow_zero_valuation_rate:
return get_valuation_rate(sle.item_code, sle.warehouse,
sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
- currency=erpnext.get_company_currency(sle.company), company=sle.company)
+ currency=erpnext.get_company_currency(sle.company), company=sle.company, batch_no=sle.batch_no)
else:
return 0.0
@@ -950,21 +950,38 @@
def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no,
- allow_zero_rate=False, currency=None, company=None, raise_error_if_no_rate=True):
+ allow_zero_rate=False, currency=None, company=None, raise_error_if_no_rate=True, batch_no=None):
if not company:
company = frappe.get_cached_value("Warehouse", warehouse, "company")
+ last_valuation_rate = None
+
+ # Get moving average rate of a specific batch number
+ if warehouse and batch_no and frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation"):
+ last_valuation_rate = frappe.db.sql("""
+ select sum(stock_value_difference) / sum(actual_qty)
+ from `tabStock Ledger Entry`
+ where
+ item_code = %s
+ AND warehouse = %s
+ AND batch_no = %s
+ AND is_cancelled = 0
+ AND NOT (voucher_no = %s AND voucher_type = %s)
+ """,
+ (item_code, warehouse, batch_no, voucher_no, voucher_type))
+
# Get valuation rate from last sle for the same item and warehouse
- last_valuation_rate = frappe.db.sql("""select valuation_rate
- from `tabStock Ledger Entry` force index (item_warehouse)
- where
- item_code = %s
- AND warehouse = %s
- AND valuation_rate >= 0
- AND is_cancelled = 0
- AND NOT (voucher_no = %s AND voucher_type = %s)
- order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, warehouse, voucher_no, voucher_type))
+ if not last_valuation_rate or last_valuation_rate[0][0] is None:
+ last_valuation_rate = frappe.db.sql("""select valuation_rate
+ from `tabStock Ledger Entry` force index (item_warehouse)
+ where
+ item_code = %s
+ AND warehouse = %s
+ AND valuation_rate >= 0
+ AND is_cancelled = 0
+ AND NOT (voucher_no = %s AND voucher_type = %s)
+ order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, warehouse, voucher_no, voucher_type))
if not last_valuation_rate:
# Get valuation rate from last sle for the item against any warehouse
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index 7263e39..3be252e 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -231,7 +231,7 @@
in_rate = get_valuation_rate(args.get('item_code'), args.get('warehouse'),
args.get('voucher_type'), voucher_no, args.get('allow_zero_valuation'),
currency=erpnext.get_company_currency(args.get('company')), company=args.get('company'),
- raise_error_if_no_rate=raise_error_if_no_rate)
+ raise_error_if_no_rate=raise_error_if_no_rate, batch_no=args.get("batch_no"))
return flt(in_rate)