perf: serial and batch bundle valuation (reposting) (#40255)
perf: serial and batch bundle valuation
diff --git a/erpnext/stock/deprecated_serial_batch.py b/erpnext/stock/deprecated_serial_batch.py
index ab38c15..7be1418 100644
--- a/erpnext/stock/deprecated_serial_batch.py
+++ b/erpnext/stock/deprecated_serial_batch.py
@@ -13,7 +13,9 @@
):
return
- serial_nos = self.get_serial_nos()
+ serial_nos = self.get_filterd_serial_nos()
+ if not serial_nos:
+ return
actual_qty = flt(self.sle.actual_qty)
@@ -25,8 +27,21 @@
self.stock_value_change += stock_value_change
+ def get_filterd_serial_nos(self):
+ serial_nos = []
+ non_filtered_serial_nos = self.get_serial_nos()
+
+ # If the serial no inwarded using the Serial and Batch Bundle, then the serial no should not be considered
+ for serial_no in non_filtered_serial_nos:
+ if serial_no and serial_no not in self.serial_no_incoming_rate:
+ serial_nos.append(serial_no)
+
+ return serial_nos
+
@deprecated
def get_incoming_value_for_serial_nos(self, serial_nos):
+ from erpnext.stock.utils import get_combine_datetime
+
# get rate from serial nos within same company
incoming_values = 0.0
for serial_no in serial_nos:
@@ -42,18 +57,19 @@
| (table.serial_no.like("%\n" + serial_no + "\n%"))
)
& (table.company == self.sle.company)
+ & (table.warehouse == self.sle.warehouse)
& (table.serial_and_batch_bundle.isnull())
+ & (table.actual_qty > 0)
& (table.is_cancelled == 0)
+ & table.posting_datetime
+ <= get_combine_datetime(self.sle.posting_date, self.sle.posting_time)
)
.orderby(table.posting_datetime, order=Order.desc)
+ .limit(1)
).run(as_dict=1)
for sle in stock_ledgers:
- self.serial_no_incoming_rate[serial_no] += (
- flt(sle.incoming_rate)
- if sle.actual_qty > 0
- else (sle.stock_value_difference / sle.actual_qty) * -1
- )
+ self.serial_no_incoming_rate[serial_no] += flt(sle.incoming_rate)
incoming_values += self.serial_no_incoming_rate[serial_no]
return incoming_values
diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
index 257f263..4058aa8 100644
--- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
+++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
@@ -415,23 +415,6 @@
create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, charges=charges)
new_purchase_rate = serial_no_rate + charges
- sn_obj = SerialNoValuation(
- sle=frappe._dict(
- {
- "posting_date": today(),
- "posting_time": nowtime(),
- "item_code": "_Test Serialized Item",
- "warehouse": "Stores - TCP1",
- "serial_nos": [serial_no],
- }
- )
- )
-
- new_serial_no_rate = sn_obj.get_incoming_rate_of_serial_no(serial_no)
-
- # Since the serial no is already delivered the rate must be zero
- self.assertFalse(new_serial_no_rate)
-
stock_value_difference = frappe.db.get_value(
"Stock Ledger Entry",
filters={
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 33f0dce..d01dfef 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
@@ -332,13 +332,8 @@
rate = frappe.db.get_value(child_table, self.voucher_detail_no, valuation_field)
for d in self.entries:
- if not rate or (
- flt(rate, precision) == flt(d.incoming_rate, precision) and d.stock_value_difference
- ):
- continue
-
d.incoming_rate = flt(rate, precision)
- if self.has_batch_no:
+ if d.qty:
d.stock_value_difference = flt(d.qty) * flt(d.incoming_rate)
if save:
diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py
index 24dd9d1..1fcc439 100644
--- a/erpnext/stock/serial_batch_bundle.py
+++ b/erpnext/stock/serial_batch_bundle.py
@@ -4,8 +4,9 @@
import frappe
from frappe import _, bold
from frappe.model.naming import make_autoname
-from frappe.query_builder.functions import CombineDatetime, Sum
+from frappe.query_builder.functions import CombineDatetime, Sum, Timestamp
from frappe.utils import cint, cstr, flt, get_link_to_form, now, nowtime, today
+from pypika import Order
from erpnext.stock.deprecated_serial_batch import (
DeprecatedBatchNoValuation,
@@ -424,19 +425,21 @@
)
else:
- entries = self.get_serial_no_ledgers()
-
self.serial_no_incoming_rate = defaultdict(float)
self.stock_value_change = 0.0
- for ledger in entries:
- self.stock_value_change += ledger.incoming_rate
- self.serial_no_incoming_rate[ledger.serial_no] += ledger.incoming_rate
+ serial_nos = self.get_serial_nos()
+ for serial_no in serial_nos:
+ incoming_rate = self.get_incoming_rate_from_bundle(serial_no)
+ if not incoming_rate:
+ continue
+
+ self.stock_value_change += incoming_rate
+ self.serial_no_incoming_rate[serial_no] += incoming_rate
self.calculate_stock_value_from_deprecarated_ledgers()
- def get_serial_no_ledgers(self):
- serial_nos = self.get_serial_nos()
+ def get_incoming_rate_from_bundle(self, serial_no) -> float:
bundle = frappe.qb.DocType("Serial and Batch Bundle")
bundle_child = frappe.qb.DocType("Serial and Batch Entry")
@@ -444,20 +447,18 @@
frappe.qb.from_(bundle)
.inner_join(bundle_child)
.on(bundle.name == bundle_child.parent)
- .select(
- bundle.name,
- bundle_child.serial_no,
- (bundle_child.incoming_rate * bundle_child.qty).as_("incoming_rate"),
- )
+ .select((bundle_child.incoming_rate * bundle_child.qty).as_("incoming_rate"))
.where(
(bundle.is_cancelled == 0)
& (bundle.docstatus == 1)
- & (bundle_child.serial_no.isin(serial_nos))
- & (bundle.type_of_transaction.isin(["Inward", "Outward"]))
+ & (bundle_child.serial_no == serial_no)
+ & (bundle.type_of_transaction == "Inward")
+ & (bundle_child.qty > 0)
& (bundle.item_code == self.sle.item_code)
& (bundle_child.warehouse == self.sle.warehouse)
)
- .orderby(bundle.posting_date, bundle.posting_time, bundle.creation)
+ .orderby(Timestamp(bundle.posting_date, bundle.posting_time), order=Order.desc)
+ .limit(1)
)
# Important to exclude the current voucher to calculate correct the stock value difference
@@ -474,7 +475,8 @@
query = query.where(timestamp_condition)
- return query.run(as_dict=True)
+ incoming_rate = query.run()
+ return flt(incoming_rate[0][0]) if incoming_rate else 0.0
def get_serial_nos(self):
if self.sle.get("serial_nos"):
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 2ae6c19..2b62baf 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -952,7 +952,12 @@
get_rate_for_return, # don't move this import to top
)
- if self.valuation_method == "Moving Average":
+ if (
+ self.valuation_method == "Moving Average"
+ and not sle.get("serial_no")
+ and not sle.get("batch_no")
+ and not sle.get("serial_and_batch_bundle")
+ ):
rate = get_incoming_rate(
{
"item_code": sle.item_code,
@@ -979,6 +984,18 @@
voucher_detail_no=sle.voucher_detail_no,
sle=sle,
)
+
+ if (
+ sle.get("serial_and_batch_bundle")
+ and rate > 0
+ and sle.voucher_type in ["Delivery Note", "Sales Invoice"]
+ ):
+ frappe.db.set_value(
+ sle.voucher_type + " Item",
+ sle.voucher_detail_no,
+ "incoming_rate",
+ rate,
+ )
elif (
sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
and sle.voucher_detail_no