mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-03 20:23:30 +02:00
perf: serial nos / batches reposting
(cherry picked from commit acb3ef78a7)
This commit is contained in:
committed by
Mergify
parent
f94f628884
commit
8a310efc97
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user