From f704eb758148004442558c7d0659ddc484bf5fe3 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 30 Mar 2023 11:32:39 +0530 Subject: [PATCH] fix: average batch wise valuation --- erpnext/stock/deprecated_serial_batch.py | 204 ++++++++++++++++++++++- erpnext/stock/serial_batch_bundle.py | 37 +++- 2 files changed, 229 insertions(+), 12 deletions(-) diff --git a/erpnext/stock/deprecated_serial_batch.py b/erpnext/stock/deprecated_serial_batch.py index b7c5d57d96..0992345537 100644 --- a/erpnext/stock/deprecated_serial_batch.py +++ b/erpnext/stock/deprecated_serial_batch.py @@ -1,3 +1,5 @@ +from collections import defaultdict + import frappe from frappe.query_builder.functions import CombineDatetime, Sum from frappe.utils import flt @@ -71,11 +73,14 @@ class DeprecatedBatchNoValuation: def calculate_avg_rate_from_deprecarated_ledgers(self): entries = self.get_sle_for_batches() for ledger in entries: - self.batch_avg_rate[ledger.batch_no] += flt(ledger.batch_value) / flt(ledger.batch_qty) + self.stock_value_differece[ledger.batch_no] += flt(ledger.batch_value) self.available_qty[ledger.batch_no] += flt(ledger.batch_qty) @deprecated def get_sle_for_batches(self): + if not self.batchwise_valuation_batches: + return [] + sle = frappe.qb.DocType("Stock Ledger Entry") timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime( @@ -87,10 +92,6 @@ class DeprecatedBatchNoValuation: == CombineDatetime(self.sle.posting_date, self.sle.posting_time) ) & (sle.creation < self.sle.creation) - batch_nos = self.batch_nos - if isinstance(self.batch_nos, dict): - batch_nos = list(self.batch_nos.keys()) - query = ( frappe.qb.from_(sle) .select( @@ -101,7 +102,8 @@ class DeprecatedBatchNoValuation: .where( (sle.item_code == self.sle.item_code) & (sle.warehouse == self.sle.warehouse) - & (sle.batch_no.isin(batch_nos)) + & (sle.batch_no.isin(self.batchwise_valuation_batches)) + & (sle.batch_no.isnotnull()) & (sle.is_cancelled == 0) ) .where(timestamp_condition) @@ -112,3 +114,193 @@ class DeprecatedBatchNoValuation: query = query.where(sle.name != self.sle.name) return query.run(as_dict=True) + + @deprecated + def calculate_avg_rate_for_non_batchwise_valuation(self): + if not self.non_batchwise_valuation_batches: + return + + avg_rate = self.get_avg_rate_for_non_batchwise_valuation_batches() + avilable_qty = self.get_available_qty_for_non_batchwise_valuation_batches() + + for batch_no in self.non_batchwise_valuation_batches: + self.stock_value_differece[batch_no] = avg_rate + self.available_qty[batch_no] = avilable_qty.get(batch_no, 0) + + @deprecated + def get_avg_rate_for_non_batchwise_valuation_batches(self): + stock_value, qty = self.get_balance_value_and_qty_from_sl_entries() + stock_value, qty = self.get_balance_value_and_qty_from_bundle(stock_value, qty) + + return stock_value / qty if qty else 0 + + @deprecated + def get_balance_value_and_qty_from_sl_entries(self): + stock_value_difference = 0.0 + available_qty = 0.0 + + sle = frappe.qb.DocType("Stock Ledger Entry") + batch = frappe.qb.DocType("Batch") + + timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime( + self.sle.posting_date, self.sle.posting_time + ) + if self.sle.creation: + timestamp_condition |= ( + CombineDatetime(sle.posting_date, sle.posting_time) + == CombineDatetime(self.sle.posting_date, self.sle.posting_time) + ) & (sle.creation < self.sle.creation) + + query = ( + frappe.qb.from_(sle) + .inner_join(batch) + .on(sle.batch_no == batch.name) + .select( + Sum(sle.stock_value_difference).as_("batch_value"), + Sum(sle.actual_qty).as_("batch_qty"), + ) + .where( + (sle.item_code == self.sle.item_code) + & (sle.warehouse == self.sle.warehouse) + & (sle.batch_no.isnotnull()) + & (batch.use_batchwise_valuation == 0) + & (sle.is_cancelled == 0) + ) + .where(timestamp_condition) + ) + + if self.sle.name: + query = query.where(sle.name != self.sle.name) + + for d in query.run(as_dict=True): + stock_value_difference += flt(d.batch_value) + available_qty += flt(d.batch_qty) + + return stock_value_difference, available_qty + + @deprecated + def get_balance_value_and_qty_from_bundle(self, stock_value, qty): + bundle = frappe.qb.DocType("Serial and Batch Bundle") + bundle_child = frappe.qb.DocType("Serial and Batch Entry") + batch = frappe.qb.DocType("Batch") + + timestamp_condition = CombineDatetime( + bundle.posting_date, bundle.posting_time + ) < CombineDatetime(self.sle.posting_date, self.sle.posting_time) + + if self.sle.creation: + timestamp_condition |= ( + CombineDatetime(bundle.posting_date, bundle.posting_time) + == CombineDatetime(self.sle.posting_date, self.sle.posting_time) + ) & (bundle.creation < self.sle.creation) + + query = ( + frappe.qb.from_(bundle) + .inner_join(bundle_child) + .on(bundle.name == bundle_child.parent) + .inner_join(batch) + .on(bundle_child.batch_no == batch.name) + .select( + Sum(bundle_child.stock_value_difference).as_("batch_value"), + Sum(bundle_child.qty).as_("batch_qty"), + ) + .where( + (bundle.item_code == self.sle.item_code) + & (bundle.warehouse == self.sle.warehouse) + & (bundle_child.batch_no.isnotnull()) + & (batch.use_batchwise_valuation == 0) + & (bundle.is_cancelled == 0) + ) + .where(timestamp_condition) + ) + + if self.sle.serial_and_batch_bundle: + query = query.where(bundle.name != self.sle.serial_and_batch_bundle) + + for d in query.run(as_dict=True): + stock_value += flt(d.batch_value) + qty += flt(d.batch_qty) + + return stock_value, qty + + @deprecated + def get_available_qty_for_non_batchwise_valuation_batches(self): + available_qty = defaultdict(float) + self.set_available_qty_for_non_batchwise_valuation_batches_from_sle(available_qty) + self.set_available_qty_for_non_batchwise_valuation_batches_from_bundle(available_qty) + + return available_qty + + @deprecated + def set_available_qty_for_non_batchwise_valuation_batches_from_sle(self, available_qty): + sle = frappe.qb.DocType("Stock Ledger Entry") + + timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime( + self.sle.posting_date, self.sle.posting_time + ) + if self.sle.creation: + timestamp_condition |= ( + CombineDatetime(sle.posting_date, sle.posting_time) + == CombineDatetime(self.sle.posting_date, self.sle.posting_time) + ) & (sle.creation < self.sle.creation) + + query = ( + frappe.qb.from_(sle) + .select( + sle.batch_no, + Sum(sle.actual_qty).as_("batch_qty"), + ) + .where( + (sle.item_code == self.sle.item_code) + & (sle.warehouse == self.sle.warehouse) + & (sle.batch_no.isin(self.non_batchwise_valuation_batches)) + & (sle.is_cancelled == 0) + ) + .where(timestamp_condition) + .groupby(sle.batch_no) + ) + + if self.sle.name: + query = query.where(sle.name != self.sle.name) + + for d in query.run(as_dict=True): + available_qty[d.batch_no] += flt(d.batch_qty) + + @deprecated + def set_available_qty_for_non_batchwise_valuation_batches_from_bundle(self, available_qty): + bundle = frappe.qb.DocType("Serial and Batch Bundle") + bundle_child = frappe.qb.DocType("Serial and Batch Entry") + + timestamp_condition = CombineDatetime( + bundle.posting_date, bundle.posting_time + ) < CombineDatetime(self.sle.posting_date, self.sle.posting_time) + + if self.sle.creation: + timestamp_condition |= ( + CombineDatetime(bundle.posting_date, bundle.posting_time) + == CombineDatetime(self.sle.posting_date, self.sle.posting_time) + ) & (bundle.creation < self.sle.creation) + + query = ( + frappe.qb.from_(bundle) + .inner_join(bundle_child) + .on(bundle.name == bundle_child.parent) + .select( + bundle_child.batch_no, + Sum(bundle_child.qty).as_("batch_qty"), + ) + .where( + (bundle.item_code == self.sle.item_code) + & (bundle.warehouse == self.sle.warehouse) + & (bundle_child.batch_no.isin(self.non_batchwise_valuation_batches)) + & (bundle.is_cancelled == 0) + ) + .where(timestamp_condition) + .groupby(bundle_child.batch_no) + ) + + if self.sle.serial_and_batch_bundle: + query = query.where(bundle.name != self.sle.serial_and_batch_bundle) + + for d in query.run(as_dict=True): + available_qty[d.batch_no] += flt(d.batch_qty) diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 7a6190ea77..a2b562c2ef 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -388,6 +388,7 @@ class BatchNoValuation(DeprecatedBatchNoValuation): setattr(self, key, value) self.batch_nos = self.get_batch_nos() + self.prepare_batches() self.calculate_avg_rate() self.calculate_valuation_rate() @@ -401,22 +402,22 @@ class BatchNoValuation(DeprecatedBatchNoValuation): self.batch_avg_rate = defaultdict(float) self.available_qty = defaultdict(float) + self.stock_value_differece = defaultdict(float) for ledger in entries: - self.batch_avg_rate[ledger.batch_no] += flt(ledger.incoming_rate) / flt(ledger.qty) + self.stock_value_differece[ledger.batch_no] += flt(ledger.incoming_rate) self.available_qty[ledger.batch_no] += flt(ledger.qty) self.calculate_avg_rate_from_deprecarated_ledgers() self.set_stock_value_difference() def get_batch_no_ledgers(self) -> List[dict]: + if not self.batchwise_valuation_batches: + return [] + parent = frappe.qb.DocType("Serial and Batch Bundle") child = frappe.qb.DocType("Serial and Batch Entry") - batch_nos = self.batch_nos - if isinstance(self.batch_nos, dict): - batch_nos = list(self.batch_nos.keys()) - timestamp_condition = "" if self.sle.posting_date and self.sle.posting_time: timestamp_condition = CombineDatetime( @@ -433,7 +434,7 @@ class BatchNoValuation(DeprecatedBatchNoValuation): Sum(child.qty).as_("qty"), ) .where( - (child.batch_no.isin(batch_nos)) + (child.batch_no.isin(self.batchwise_valuation_batches)) & (parent.warehouse == self.sle.warehouse) & (parent.item_code == self.sle.item_code) & (parent.docstatus == 1) @@ -451,6 +452,25 @@ class BatchNoValuation(DeprecatedBatchNoValuation): return query.run(as_dict=True) + def prepare_batches(self): + self.batches = self.batch_nos + if isinstance(self.batch_nos, dict): + self.batches = list(self.batch_nos.keys()) + + self.batchwise_valuation_batches = [] + self.non_batchwise_valuation_batches = [] + + batches = frappe.get_all( + "Batch", filters={"name": ("in", self.batches), "use_batchwise_valuation": 1}, fields=["name"] + ) + + for batch in batches: + self.batchwise_valuation_batches.append(batch.name) + + self.non_batchwise_valuation_batches = list( + set(self.batches) - set(self.batchwise_valuation_batches) + ) + def get_batch_nos(self) -> list: if self.sle.get("batch_nos"): return self.sle.batch_nos @@ -463,6 +483,11 @@ class BatchNoValuation(DeprecatedBatchNoValuation): self.stock_value_change = 0 for batch_no, ledger in self.batch_nos.items(): + self.batch_avg_rate[batch_no] = ( + self.stock_value_differece[batch_no] / self.available_qty[batch_no] + ) + + # New Stock Value Difference stock_value_change = self.batch_avg_rate[batch_no] * ledger.qty self.stock_value_change += stock_value_change frappe.db.set_value(