mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-11 21:51:48 +00:00
fix: re-check future sle before queuing repost on submit (#57664)
* test: cover both repost branches and the no-repost case * fix: queue repost for entries backdated by a concurrent submit --------- Co-authored-by: nareshkannasln <nareshkannashanmugam@gmail.com>
This commit is contained in:
@@ -702,6 +702,11 @@ def is_reposting_pending():
|
||||
)
|
||||
|
||||
|
||||
def invalidate_future_sle_cache(voucher_type, voucher_no):
|
||||
if hasattr(frappe.local, "future_sle"):
|
||||
frappe.local.future_sle.pop((voucher_type, voucher_no), None)
|
||||
|
||||
|
||||
def future_sle_exists(args, sl_entries=None):
|
||||
from erpnext.stock.utils import get_combine_datetime
|
||||
|
||||
|
||||
@@ -40,3 +40,140 @@ class TestStockControllerConversions(ERPNextTestSuite):
|
||||
sl_entries = [frappe._dict(item_code=item, warehouse="_Test Warehouse - _TC")]
|
||||
|
||||
self.assertTrue(future_sle_exists(args, sl_entries))
|
||||
|
||||
def _make_opening_entry(self, item, warehouse):
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
|
||||
opening = make_stock_entry(
|
||||
item_code=item,
|
||||
target=warehouse,
|
||||
qty=100,
|
||||
basic_rate=100,
|
||||
posting_date=add_days(today(), -5),
|
||||
posting_time="01:00:00",
|
||||
)
|
||||
self.addCleanup(self._cancel_and_delete, "Stock Entry", opening.name)
|
||||
|
||||
return opening
|
||||
|
||||
def _later_sle(self, item, warehouse, opening):
|
||||
sle = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Stock Ledger Entry",
|
||||
"item_code": item,
|
||||
"warehouse": warehouse,
|
||||
"posting_date": today(),
|
||||
"posting_time": "12:00:00",
|
||||
"voucher_type": "Stock Entry",
|
||||
"voucher_no": opening.name,
|
||||
"actual_qty": 7,
|
||||
"incoming_rate": 100,
|
||||
"qty_after_transaction": 107,
|
||||
"valuation_rate": 100,
|
||||
"stock_value": 10700,
|
||||
"company": opening.company,
|
||||
"stock_uom": "Nos",
|
||||
}
|
||||
)
|
||||
sle.flags.ignore_permissions = True
|
||||
sle.flags.ignore_links = True
|
||||
|
||||
return sle
|
||||
|
||||
def _submit_entry(self, item, warehouse, inject=None):
|
||||
from erpnext.stock import stock_ledger
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
|
||||
original_make_entry = stock_ledger.make_entry
|
||||
injected = []
|
||||
|
||||
def make_entry_with_injection(*args, **kwargs):
|
||||
if inject is not None and not injected:
|
||||
injected.append(True)
|
||||
inject.submit()
|
||||
return original_make_entry(*args, **kwargs)
|
||||
|
||||
stock_ledger.make_entry = make_entry_with_injection
|
||||
try:
|
||||
entry = make_stock_entry(
|
||||
item_code=item,
|
||||
target=warehouse,
|
||||
qty=5,
|
||||
basic_rate=500,
|
||||
posting_date=today(),
|
||||
posting_time="06:00:00",
|
||||
)
|
||||
finally:
|
||||
stock_ledger.make_entry = original_make_entry
|
||||
|
||||
self.addCleanup(self._cancel_and_delete, "Stock Entry", entry.name)
|
||||
if inject is not None:
|
||||
self.assertTrue(injected, "the later SL Entry was not written during the submit")
|
||||
|
||||
return entry
|
||||
|
||||
def _reposts_queued_for(self, item, warehouse, voucher_no):
|
||||
names = set(
|
||||
frappe.get_all(
|
||||
"Repost Item Valuation",
|
||||
filters={"docstatus": 1, "item_code": item, "warehouse": warehouse},
|
||||
pluck="name",
|
||||
)
|
||||
) | set(
|
||||
frappe.get_all(
|
||||
"Repost Item Valuation",
|
||||
filters={"docstatus": 1, "voucher_no": voucher_no},
|
||||
pluck="name",
|
||||
)
|
||||
)
|
||||
for name in names:
|
||||
self.addCleanup(frappe.delete_doc, "Repost Item Valuation", name, force=1)
|
||||
|
||||
return names
|
||||
|
||||
def test_repost_queued_for_entry_backdated_while_its_sl_entries_were_written(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
item = make_item("_Test Concurrent Backdated Item", {"is_stock_item": 1}).name
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
opening = self._make_opening_entry(item, warehouse)
|
||||
backdated = self._submit_entry(item, warehouse, inject=self._later_sle(item, warehouse, opening))
|
||||
|
||||
self.assertTrue(
|
||||
self._reposts_queued_for(item, warehouse, backdated.name),
|
||||
"No Repost Item Valuation was queued for an entry that a later SL Entry made backdated",
|
||||
)
|
||||
|
||||
def test_repost_queued_against_voucher_when_item_based_reposting_is_off(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
item = make_item("_Test Voucher Based Repost Item", {"is_stock_item": 1}).name
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
with self.change_settings("Stock Reposting Settings", item_based_reposting=0):
|
||||
opening = self._make_opening_entry(item, warehouse)
|
||||
backdated = self._submit_entry(item, warehouse, inject=self._later_sle(item, warehouse, opening))
|
||||
|
||||
self.assertTrue(
|
||||
frappe.get_all(
|
||||
"Repost Item Valuation",
|
||||
filters={"docstatus": 1, "voucher_no": backdated.name},
|
||||
pluck="name",
|
||||
),
|
||||
"No voucher based Repost Item Valuation was queued",
|
||||
)
|
||||
|
||||
def test_no_repost_queued_when_nothing_was_written_after_the_entry(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
item = make_item("_Test Unconcurrent Item", {"is_stock_item": 1}).name
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
self._make_opening_entry(item, warehouse)
|
||||
entry = self._submit_entry(item, warehouse)
|
||||
|
||||
self.assertFalse(
|
||||
self._reposts_queued_for(item, warehouse, entry.name),
|
||||
"A Repost Item Valuation was queued for an entry with nothing posted after it",
|
||||
)
|
||||
|
||||
@@ -137,7 +137,7 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
|
||||
such cases certain validations need to be ignored (like negative
|
||||
stock)
|
||||
"""
|
||||
from erpnext.controllers.stock_controller import future_sle_exists
|
||||
from erpnext.controllers.stock_controller import future_sle_exists, invalidate_future_sle_cache
|
||||
|
||||
if sl_entries:
|
||||
# Sorted so two vouchers touching the same pairs can't take the gates in opposite order.
|
||||
@@ -195,6 +195,8 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
|
||||
_("Item {0} ignored since it is not a stock item").format(args.get("item_code"))
|
||||
)
|
||||
|
||||
invalidate_future_sle_cache(sl_entries[0].get("voucher_type"), sl_entries[0].get("voucher_no"))
|
||||
|
||||
|
||||
def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_voucher=False, cancelled=False):
|
||||
if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
|
||||
|
||||
Reference in New Issue
Block a user