perf: serial nos / batches reposting

(cherry picked from commit acb3ef78a7)
This commit is contained in:
Rohit Waghchaure
2025-10-06 00:52:56 +05:30
committed by Mergify
parent f94f628884
commit 8a310efc97
3 changed files with 150 additions and 14 deletions

View File

@@ -7,6 +7,7 @@ from frappe.query_builder.functions import CombineDatetime, Sum
from frappe.utils import flt, nowtime from frappe.utils import flt, nowtime
from frappe.utils.deprecations import deprecated from frappe.utils.deprecations import deprecated
from pypika import Order from pypika import Order
from pypika.functions import Coalesce
class DeprecatedSerialNoValuation: class DeprecatedSerialNoValuation:
@@ -197,9 +198,15 @@ class DeprecatedBatchNoValuation:
@deprecated @deprecated
def set_balance_value_for_non_batchwise_valuation_batches(self): def set_balance_value_for_non_batchwise_valuation_batches(self):
self.last_sle = self.get_last_sle_for_non_batch() if hasattr(self, "prev_sle"):
self.last_sle = self.prev_sle
else:
self.last_sle = self.get_last_sle_for_non_batch()
if self.last_sle and self.last_sle.stock_queue: if self.last_sle and self.last_sle.stock_queue:
self.stock_queue = json.loads(self.last_sle.stock_queue or "[]") or [] self.stock_queue = self.last_sle.stock_queue
if isinstance(self.stock_queue, str):
self.stock_queue = json.loads(self.stock_queue) or []
self.set_balance_value_from_sl_entries() self.set_balance_value_from_sl_entries()
self.set_balance_value_from_bundle() self.set_balance_value_from_bundle()
@@ -293,10 +300,7 @@ class DeprecatedBatchNoValuation:
query = query.where(sle.name != self.sle.name) query = query.where(sle.name != self.sle.name)
if self.sle.serial_and_batch_bundle: if self.sle.serial_and_batch_bundle:
query = query.where( query = query.where(Coalesce(sle.serial_and_batch_bundle, "") != self.sle.serial_and_batch_bundle)
(sle.serial_and_batch_bundle != self.sle.serial_and_batch_bundle)
| (sle.serial_and_batch_bundle.isnull())
)
data = query.run(as_dict=True) data = query.run(as_dict=True)

View File

@@ -1323,9 +1323,18 @@ class TestStockEntry(FrappeTestCase):
posting_date="2021-07-02", # Illegal SE posting_date="2021-07-02", # Illegal SE
purpose="Material Transfer", purpose="Material Transfer",
), ),
dict(
item_code=item_code,
qty=2,
from_warehouse=warehouse_names[0],
to_warehouse=warehouse_names[1],
batch_no=batch_no,
posting_date="2021-07-02", # Illegal SE
purpose="Material Transfer",
),
] ]
self.assertRaises(NegativeStockError, create_stock_entries, sequence_of_entries) self.assertRaises(frappe.ValidationError, create_stock_entries, sequence_of_entries)
@change_settings("Stock Settings", {"allow_negative_stock": 0}) @change_settings("Stock Settings", {"allow_negative_stock": 0})
def test_future_negative_sle_batch(self): def test_future_negative_sle_batch(self):

View File

@@ -1010,13 +1010,12 @@ class update_entries_after:
if not frappe.db.exists("Serial and Batch Bundle", sle.serial_and_batch_bundle): if not frappe.db.exists("Serial and Batch Bundle", sle.serial_and_batch_bundle):
return return
if self.args.get("sle_id") and sle.actual_qty < 0: if sle.actual_qty < 0 and (
doc = frappe.db.get_value( sle.voucher_type in ["Stock Reconciliation", "Asset Capitalization"]
"Serial and Batch Bundle", or not frappe.db.get_value(sle.voucher_type, sle.voucher_no, "is_return")
sle.serial_and_batch_bundle, ):
["total_amount", "total_qty"], doc = frappe._dict({})
as_dict=1, self.update_serial_batch_no_valuation(sle, doc, prev_sle=self.wh_data)
)
else: else:
doc = frappe.get_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle) doc = frappe.get_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle)
doc.set_incoming_rate( doc.set_incoming_rate(
@@ -1040,6 +1039,88 @@ class update_entries_after:
self.wh_data.qty_after_transaction, self.flt_precision self.wh_data.qty_after_transaction, self.flt_precision
) )
def update_serial_batch_no_valuation(self, sle, doc, prev_sle=None):
from erpnext.stock.serial_batch_bundle import BatchNoValuation, SerialNoValuation
sabb_data = get_serial_from_sabb(sle.serial_and_batch_bundle)
if not sabb_data:
doc.update({"total_amount": 0.0, "total_qty": 0.0, "avg_rate": 0.0})
return
serial_nos = [d.serial_no for d in sabb_data if d.serial_no]
if serial_nos:
sle["serial_nos"] = get_serial_nos_data(",".join(serial_nos))
sn_obj = SerialNoValuation(
sle=sle,
item_code=self.item_code,
warehouse=sle.warehouse,
)
else:
sle["batch_nos"] = {row.batch_no: row for row in sabb_data if row.batch_no}
sn_obj = BatchNoValuation(
sle=sle,
item_code=self.item_code,
warehouse=sle.warehouse,
prev_sle=prev_sle,
)
tot_amt = 0.0
total_qty = 0.0
avg_rate = 0.0
for d in sabb_data:
incoming_rate = get_incoming_rate_for_serial_and_batch(self.item_code, d, sn_obj)
if flt(incoming_rate, self.currency_precision) == flt(
d.valuation_rate, self.currency_precision
) and not getattr(d, "stock_queue", None):
continue
amount = incoming_rate * flt(d.qty)
tot_amt += flt(amount)
total_qty += flt(d.qty)
values_to_update = {
"incoming_rate": incoming_rate,
"stock_value_difference": amount,
}
if d.stock_queue:
values_to_update["stock_queue"] = d.stock_queue
frappe.db.set_value(
"Serial and Batch Entry",
d.name,
values_to_update,
update_modified=False,
)
if total_qty:
avg_rate = tot_amt / total_qty
doc.update(
{
"total_amount": tot_amt,
"total_qty": total_qty,
"avg_rate": avg_rate,
}
)
frappe.db.set_value(
"Serial and Batch Bundle",
sle.serial_and_batch_bundle,
{
"total_qty": total_qty,
"avg_rate": avg_rate,
"total_amount": tot_amt,
},
update_modified=False,
)
for key in ("serial_nos", "batch_nos"):
if key in sle:
del sle[key]
def get_outgoing_rate_for_batched_item(self, sle): def get_outgoing_rate_for_batched_item(self, sle):
if self.wh_data.qty_after_transaction == 0: if self.wh_data.qty_after_transaction == 0:
return 0 return 0
@@ -2297,3 +2378,45 @@ def is_transfer_stock_entry(voucher_no):
purpose = frappe.get_cached_value("Stock Entry", voucher_no, "purpose") purpose = frappe.get_cached_value("Stock Entry", voucher_no, "purpose")
return purpose in ["Material Transfer", "Material Transfer for Manufacture", "Send to Subcontractor"] return purpose in ["Material Transfer", "Material Transfer for Manufacture", "Send to Subcontractor"]
@frappe.request_cache
def get_serial_from_sabb(serial_and_batch_bundle):
return frappe.get_all(
"Serial and Batch Entry",
filters={"parent": serial_and_batch_bundle},
fields=["serial_no", "batch_no", "name", "qty", "incoming_rate"],
order_by="idx",
)
def get_incoming_rate_for_serial_and_batch(item_code, row, sn_obj):
if row.serial_no:
return abs(sn_obj.serial_no_incoming_rate.get(row.serial_no, 0.0))
else:
stock_queue = []
if hasattr(sn_obj, "stock_queue") and sn_obj.stock_queue:
stock_queue = parse_json(sn_obj.stock_queue)
val_method = get_valuation_method(item_code)
actual_qty = row.qty
if stock_queue and val_method == "FIFO" and row.batch_no in sn_obj.non_batchwise_valuation_batches:
if actual_qty < 0:
stock_queue = FIFOValuation(stock_queue)
_prev_qty, prev_stock_value = stock_queue.get_total_stock_and_value()
stock_queue.remove_stock(qty=abs(actual_qty))
_qty, stock_value = stock_queue.get_total_stock_and_value()
stock_value_difference = stock_value - prev_stock_value
incoming_rate = abs(flt(stock_value_difference) / abs(flt(actual_qty)))
stock_queue = stock_queue.state
else:
incoming_rate = abs(flt(sn_obj.batch_avg_rate.get(row.batch_no)))
stock_queue.append([row.qty, incoming_rate])
row.stock_queue = json.dumps(stock_queue)
else:
incoming_rate = abs(flt(sn_obj.batch_avg_rate.get(row.batch_no)))
return incoming_rate