From 97095c7d2446fcba7352192dd31cfb69c7667bcc Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 6 May 2025 17:02:03 +0530 Subject: [PATCH] fix: stock reco recalculate qty not works for opening stock reco --- erpnext/stock/doctype/batch/batch.py | 2 + .../stock_reconciliation.py | 88 +++++++++++++++++-- .../test_stock_reconciliation.py | 70 ++++++++++++++- erpnext/stock/serial_batch_bundle.py | 3 + erpnext/stock/stock_ledger.py | 2 +- 5 files changed, 157 insertions(+), 8 deletions(-) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 800d4f70c40..c7d9823d144 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -223,6 +223,7 @@ def get_batch_qty( ignore_voucher_nos=None, for_stock_levels=False, consider_negative_batches=False, + do_not_check_future_batches=False, ): """Returns batch actual qty if warehouse is passed, or returns dict of qty by warehouse if warehouse is None @@ -249,6 +250,7 @@ def get_batch_qty( "ignore_voucher_nos": ignore_voucher_nos, "for_stock_levels": for_stock_levels, "consider_negative_batches": consider_negative_batches, + "do_not_check_future_batches": do_not_check_future_batches, } ) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 0c28afe07e4..76a540f3e92 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -5,11 +5,11 @@ import frappe from frappe import _, bold, json, msgprint from frappe.query_builder.functions import CombineDatetime, Sum -from frappe.utils import add_to_date, cint, cstr, flt +from frappe.utils import add_to_date, cint, cstr, flt, get_datetime import erpnext from erpnext.accounts.utils import get_company_default -from erpnext.controllers.stock_controller import StockController +from erpnext.controllers.stock_controller import StockController, create_repost_item_valuation_entry from erpnext.stock.doctype.batch.batch import get_available_batches, get_batch_qty from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( @@ -186,9 +186,35 @@ class StockReconciliation(StockController): if not item.reconcile_all_serial_batch and item.serial_and_batch_bundle: bundle = self.get_bundle_for_specific_serial_batch(item) + if not bundle: + continue + item.current_serial_and_batch_bundle = bundle.name item.current_valuation_rate = abs(bundle.avg_rate) + if bundle.total_qty: + item.current_qty = abs(bundle.total_qty) + + if save: + if not item.current_qty: + frappe.throw( + _("Row # {0}: Please enter quantity for Item {1} as it is not zero.").format( + item.idx, item.item_code + ) + ) + + if self.docstatus == 1: + bundle.voucher_no = self.name + bundle.submit() + + item.db_set( + { + "current_serial_and_batch_bundle": item.current_serial_and_batch_bundle, + "current_qty": item.current_qty, + "current_valuation_rate": item.current_valuation_rate, + } + ) + if not item.valuation_rate: item.valuation_rate = item.current_valuation_rate continue @@ -333,20 +359,26 @@ class StockReconciliation(StockController): entry.batch_no, row.warehouse, row.item_code, + ignore_voucher_nos=[self.name], posting_date=self.posting_date, posting_time=self.posting_time, for_stock_levels=True, consider_negative_batches=True, + do_not_check_future_batches=True, ) + if not current_qty: + continue + total_current_qty += current_qty entry.qty = current_qty * -1 - reco_obj.save() + if total_current_qty: + reco_obj.save() - row.current_qty = total_current_qty + row.current_qty = total_current_qty - return reco_obj + return reco_obj def has_change_in_serial_batch(self, row) -> bool: bundles = {row.serial_and_batch_bundle: [], row.current_serial_and_batch_bundle: []} @@ -968,7 +1000,7 @@ class StockReconciliation(StockController): else: self._cancel() - def recalculate_current_qty(self, voucher_detail_no): + def recalculate_current_qty(self, voucher_detail_no, sle_creation, add_new_sle=False): from erpnext.stock.stock_ledger import get_valuation_rate for row in self.items: @@ -1036,6 +1068,49 @@ class StockReconciliation(StockController): } ) + if ( + add_new_sle + and not frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_detail_no": row.name, "actual_qty": ("<", 0), "is_cancelled": 0}, + "name", + ) + and not row.current_serial_and_batch_bundle + ): + self.set_current_serial_and_batch_bundle(voucher_detail_no, save=True) + row.reload() + + self.add_missing_stock_ledger_entry(row, voucher_detail_no, sle_creation) + + def add_missing_stock_ledger_entry(self, row, voucher_detail_no, sle_creation): + if row.current_qty == 0: + return + + new_sle = frappe.get_doc(self.get_sle_for_items(row)) + new_sle.actual_qty = row.current_qty * -1 + new_sle.valuation_rate = row.current_valuation_rate + new_sle.serial_and_batch_bundle = row.current_serial_and_batch_bundle + new_sle.submit() + + creation = add_to_date(sle_creation, seconds=-1) + new_sle.db_set("creation", creation) + + if not frappe.db.exists( + "Repost Item Valuation", + {"item": row.item_code, "warehouse": row.warehouse, "docstatus": 1, "status": "Queued"}, + ): + create_repost_item_valuation_entry( + { + "based_on": "Item and Warehouse", + "item_code": row.item_code, + "warehouse": row.warehouse, + "company": self.company, + "allow_negative_stock": 1, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + } + ) + def has_negative_stock_allowed(self): allow_negative_stock = cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock")) if allow_negative_stock: @@ -1109,6 +1184,7 @@ class StockReconciliation(StockController): ignore_voucher_nos=[doc.voucher_no], for_stock_levels=True, consider_negative_batches=True, + do_not_check_future_batches=True, ) or 0 ) * -1 diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 58606154291..663e9cad955 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -1069,7 +1069,7 @@ class TestStockReconciliation(IntegrationTestCase, StockTestMixin): sr.reload() self.assertTrue(sr.items[0].serial_and_batch_bundle) - self.assertFalse(sr.items[0].current_serial_and_batch_bundle) + self.assertTrue(sr.items[0].current_serial_and_batch_bundle) def test_not_reconcile_all_batch(self): from erpnext.stock.doctype.batch.batch import get_batch_qty @@ -1446,6 +1446,74 @@ class TestStockReconciliation(IntegrationTestCase, StockTestMixin): self.assertEqual(sr.difference_amount, 100 * -1) self.assertTrue(sr.items[0].qty == 0) + def test_stock_reco_recalculate_qty_for_backdated_entry(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + item_code = self.make_item( + "Test Batch Item Stock Reco Recalculate Qty", + { + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TEST-BATCH-RRQ-.###", + }, + ).name + + warehouse = "_Test Warehouse - _TC" + + sr = create_stock_reconciliation( + item_code=item_code, + warehouse=warehouse, + qty=10, + rate=100, + use_serial_batch_fields=1, + ) + + sr.reload() + self.assertEqual(sr.items[0].current_qty, 0) + self.assertEqual(sr.items[0].current_valuation_rate, 0) + + batch_no = get_batch_from_bundle(sr.items[0].serial_and_batch_bundle) + stock_ledgers = frappe.get_all( + "Stock Ledger Entry", + filters={"voucher_no": sr.name, "is_cancelled": 0}, + pluck="name", + ) + + self.assertTrue(len(stock_ledgers) == 1) + + make_stock_entry( + item_code=item_code, + target=warehouse, + qty=10, + basic_rate=100, + use_serial_batch_fields=1, + batch_no=batch_no, + ) + + # Make backdated stock reconciliation entry + create_stock_reconciliation( + item_code=item_code, + warehouse=warehouse, + qty=10, + rate=100, + use_serial_batch_fields=1, + batch_no=batch_no, + posting_date=add_days(nowdate(), -1), + ) + + stock_ledgers = frappe.get_all( + "Stock Ledger Entry", + filters={"voucher_no": sr.name, "is_cancelled": 0}, + pluck="name", + ) + + sr.reload() + self.assertEqual(sr.items[0].current_qty, 10) + self.assertEqual(sr.items[0].current_valuation_rate, 100) + + self.assertTrue(len(stock_ledgers) == 2) + def create_batch_item_with_batch(item_name, batch_id): batch_item_doc = create_item(item_name, is_stock_item=1) diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index e127960d6bb..bff764228f5 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -743,6 +743,9 @@ class BatchNoValuation(DeprecatedBatchNoValuation): if not self.sle.actual_qty: self.sle.actual_qty = self.get_actual_qty() + if not self.sle.actual_qty: + return 0.0 + return abs(flt(self.stock_value_change) / flt(self.sle.actual_qty)) def get_actual_qty(self): diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index ab83b54ce51..cd452770512 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -966,7 +966,7 @@ class update_entries_after: def reset_actual_qty_for_stock_reco(self, sle): doc = frappe.get_cached_doc("Stock Reconciliation", sle.voucher_no) - doc.recalculate_current_qty(sle.voucher_detail_no) + doc.recalculate_current_qty(sle.voucher_detail_no, sle.creation, sle.actual_qty > 0) if sle.actual_qty < 0: sle.actual_qty = (