mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-13 06:31:48 +00:00
* test: cover both repost branches and the no-repost case
* fix: queue repost for entries backdated by a concurrent submit
---------
(cherry picked from commit 399ff463cc)
Co-authored-by: nareshkannasln <nareshkannashanmugam@gmail.com>
This commit is contained in:
@@ -2417,6 +2417,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):
|
def future_sle_exists(args, sl_entries=None):
|
||||||
from erpnext.stock.utils import get_combine_datetime
|
from erpnext.stock.utils import get_combine_datetime
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe.utils import add_days, today
|
||||||
|
|
||||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||||
from erpnext.controllers.stock_controller import (
|
from erpnext.controllers.stock_controller import (
|
||||||
@@ -75,3 +76,174 @@ class TestLedgerPreviewPermission(ERPNextTestSuite):
|
|||||||
|
|
||||||
stock_ledger_result = show_stock_ledger_preview(company, "Purchase Receipt", pr.name)
|
stock_ledger_result = show_stock_ledger_preview(company, "Purchase Receipt", pr.name)
|
||||||
self.assertTrue(stock_ledger_result.get("sl_data"))
|
self.assertTrue(stock_ledger_result.get("sl_data"))
|
||||||
|
|
||||||
|
|
||||||
|
class TestStockControllerConversions(ERPNextTestSuite):
|
||||||
|
@staticmethod
|
||||||
|
def _cancel_and_delete(doctype, name):
|
||||||
|
if not frappe.db.exists(doctype, name):
|
||||||
|
return
|
||||||
|
doc = frappe.get_doc(doctype, name)
|
||||||
|
if doc.docstatus == 1:
|
||||||
|
doc.cancel()
|
||||||
|
frappe.delete_doc(doctype, name, force=1)
|
||||||
|
|
||||||
|
def test_future_sle_exists_detects_later_entries(self):
|
||||||
|
# A later SLE for the same item+warehouse must be reported as a future entry, which
|
||||||
|
# exercises the GROUP BY query in future_sle_exists on both engines.
|
||||||
|
from erpnext.controllers.stock_controller import future_sle_exists
|
||||||
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
|
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||||
|
|
||||||
|
item = make_item("_Test Future SLE Item", {"is_stock_item": 1}).name
|
||||||
|
se = make_stock_entry(item_code=item, target="_Test Warehouse - _TC", qty=10, basic_rate=100)
|
||||||
|
self.addCleanup(self._cancel_and_delete, "Stock Entry", se.name)
|
||||||
|
|
||||||
|
# Pretend a different voucher posts a day earlier for the same item/warehouse: the existing
|
||||||
|
# (later) SLE must be reported as a future entry.
|
||||||
|
args = frappe._dict(
|
||||||
|
voucher_type="Stock Entry",
|
||||||
|
voucher_no="_TEST-NONEXISTENT-SE",
|
||||||
|
posting_date=add_days(today(), -1),
|
||||||
|
posting_time="00:00:00",
|
||||||
|
)
|
||||||
|
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",
|
||||||
|
)
|
||||||
|
|||||||
@@ -92,7 +92,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
|
such cases certain validations need to be ignored (like negative
|
||||||
stock)
|
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:
|
if sl_entries:
|
||||||
validate_stock_frozen_by_closing_entry(sl_entries)
|
validate_stock_frozen_by_closing_entry(sl_entries)
|
||||||
@@ -144,6 +144,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"))
|
_("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):
|
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":
|
if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
|
||||||
|
|||||||
Reference in New Issue
Block a user