fix: stock reco recalculate qty not works for opening stock reco

(cherry picked from commit 97095c7d24)
This commit is contained in:
Rohit Waghchaure
2025-05-06 17:02:03 +05:30
committed by Mergify
parent fb56db1166
commit 2bd30e3c46
5 changed files with 157 additions and 8 deletions

View File

@@ -223,6 +223,7 @@ def get_batch_qty(
ignore_voucher_nos=None, ignore_voucher_nos=None,
for_stock_levels=False, for_stock_levels=False,
consider_negative_batches=False, consider_negative_batches=False,
do_not_check_future_batches=False,
): ):
"""Returns batch actual qty if warehouse is passed, """Returns batch actual qty if warehouse is passed,
or returns dict of qty by warehouse if warehouse is None 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, "ignore_voucher_nos": ignore_voucher_nos,
"for_stock_levels": for_stock_levels, "for_stock_levels": for_stock_levels,
"consider_negative_batches": consider_negative_batches, "consider_negative_batches": consider_negative_batches,
"do_not_check_future_batches": do_not_check_future_batches,
} }
) )

View File

@@ -5,11 +5,11 @@
import frappe import frappe
from frappe import _, bold, json, msgprint from frappe import _, bold, json, msgprint
from frappe.query_builder.functions import CombineDatetime, Sum 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 import erpnext
from erpnext.accounts.utils import get_company_default 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.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.inventory_dimension.inventory_dimension import get_inventory_dimensions
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( 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: if not item.reconcile_all_serial_batch and item.serial_and_batch_bundle:
bundle = self.get_bundle_for_specific_serial_batch(item) bundle = self.get_bundle_for_specific_serial_batch(item)
if not bundle:
continue
item.current_serial_and_batch_bundle = bundle.name item.current_serial_and_batch_bundle = bundle.name
item.current_valuation_rate = abs(bundle.avg_rate) 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: if not item.valuation_rate:
item.valuation_rate = item.current_valuation_rate item.valuation_rate = item.current_valuation_rate
continue continue
@@ -333,20 +359,26 @@ class StockReconciliation(StockController):
entry.batch_no, entry.batch_no,
row.warehouse, row.warehouse,
row.item_code, row.item_code,
ignore_voucher_nos=[self.name],
posting_date=self.posting_date, posting_date=self.posting_date,
posting_time=self.posting_time, posting_time=self.posting_time,
for_stock_levels=True, for_stock_levels=True,
consider_negative_batches=True, consider_negative_batches=True,
do_not_check_future_batches=True,
) )
if not current_qty:
continue
total_current_qty += current_qty total_current_qty += current_qty
entry.qty = current_qty * -1 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: def has_change_in_serial_batch(self, row) -> bool:
bundles = {row.serial_and_batch_bundle: [], row.current_serial_and_batch_bundle: []} bundles = {row.serial_and_batch_bundle: [], row.current_serial_and_batch_bundle: []}
@@ -968,7 +1000,7 @@ class StockReconciliation(StockController):
else: else:
self._cancel() 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 from erpnext.stock.stock_ledger import get_valuation_rate
for row in self.items: 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): def has_negative_stock_allowed(self):
allow_negative_stock = cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock")) allow_negative_stock = cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
if allow_negative_stock: if allow_negative_stock:
@@ -1109,6 +1184,7 @@ class StockReconciliation(StockController):
ignore_voucher_nos=[doc.voucher_no], ignore_voucher_nos=[doc.voucher_no],
for_stock_levels=True, for_stock_levels=True,
consider_negative_batches=True, consider_negative_batches=True,
do_not_check_future_batches=True,
) )
or 0 or 0
) * -1 ) * -1

View File

@@ -1069,7 +1069,7 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
sr.reload() sr.reload()
self.assertTrue(sr.items[0].serial_and_batch_bundle) 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): def test_not_reconcile_all_batch(self):
from erpnext.stock.doctype.batch.batch import get_batch_qty from erpnext.stock.doctype.batch.batch import get_batch_qty
@@ -1446,6 +1446,74 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
self.assertEqual(sr.difference_amount, 100 * -1) self.assertEqual(sr.difference_amount, 100 * -1)
self.assertTrue(sr.items[0].qty == 0) 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): def create_batch_item_with_batch(item_name, batch_id):
batch_item_doc = create_item(item_name, is_stock_item=1) batch_item_doc = create_item(item_name, is_stock_item=1)

View File

@@ -743,6 +743,9 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
if not self.sle.actual_qty: if not self.sle.actual_qty:
self.sle.actual_qty = self.get_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)) return abs(flt(self.stock_value_change) / flt(self.sle.actual_qty))
def get_actual_qty(self): def get_actual_qty(self):

View File

@@ -962,7 +962,7 @@ class update_entries_after:
def reset_actual_qty_for_stock_reco(self, sle): def reset_actual_qty_for_stock_reco(self, sle):
doc = frappe.get_cached_doc("Stock Reconciliation", sle.voucher_no) 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: if sle.actual_qty < 0:
sle.actual_qty = ( sle.actual_qty = (