diff --git a/erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py b/erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py index 00a3b0204c4..1f8450c87c4 100644 --- a/erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py +++ b/erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py @@ -59,11 +59,10 @@ class StockClosingEntry(Document): .where( (table.docstatus == 1) & (table.company == self.company) - & ( - (table.from_date.between(self.from_date, self.to_date)) - | (table.to_date.between(self.from_date, self.to_date)) - | ((self.from_date >= table.from_date) & (table.from_date >= self.to_date)) - ) + # two date ranges overlap when each starts on or before the other ends; + # this also catches one range being fully contained within the other + & (table.from_date <= self.to_date) + & (table.to_date >= self.from_date) ) ) diff --git a/erpnext/stock/doctype/stock_closing_entry/test_stock_closing_entry.py b/erpnext/stock/doctype/stock_closing_entry/test_stock_closing_entry.py index 3485e47030f..df5c22b6be5 100644 --- a/erpnext/stock/doctype/stock_closing_entry/test_stock_closing_entry.py +++ b/erpnext/stock/doctype/stock_closing_entry/test_stock_closing_entry.py @@ -1,6 +1,8 @@ # Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt +from unittest.mock import patch + import frappe from frappe.utils import add_days, today @@ -57,3 +59,45 @@ class TestStockClosingEntry(ERPNextTestSuite): ).submit() self.last_closing_entry = entry.name return entry + + +class TestStockClosingEntryDuplicate(ERPNextTestSuite): + """validate_duplicate blocks a second submitted closing entry whose date range + overlaps an existing one for the same scope (company + warehouse/item filters).""" + + def make_closing(self, from_date, to_date, **fields): + doc = frappe.new_doc("Stock Closing Entry") + doc.company = COMPANY + doc.from_date = from_date + doc.to_date = to_date + doc.update(fields) + return doc + + def submit_closing(self, doc): + # the closing-balance build is enqueued on submit; skip it here + with patch("erpnext.stock.doctype.stock_closing_entry.stock_closing_entry.enqueue"): + doc.submit() + return doc + + def test_overlapping_range_is_rejected(self): + self.submit_closing(self.make_closing("2026-01-01", "2026-03-31")) + overlap = self.make_closing("2026-02-01", "2026-04-30") + self.assertRaises(frappe.ValidationError, overlap.insert) + + def test_fully_contained_range_is_rejected(self): + # a range entirely inside an existing entry's range is still a duplicate + self.submit_closing(self.make_closing("2026-01-01", "2026-12-31")) + contained = self.make_closing("2026-03-01", "2026-03-31") + self.assertRaises(frappe.ValidationError, contained.insert) + + def test_enclosing_range_is_rejected(self): + # and so is a range that fully encloses an existing entry's range + self.submit_closing(self.make_closing("2026-03-01", "2026-03-31")) + enclosing = self.make_closing("2026-01-01", "2026-12-31") + self.assertRaises(frappe.ValidationError, enclosing.insert) + + def test_non_overlapping_range_is_allowed(self): + self.submit_closing(self.make_closing("2026-01-01", "2026-03-31")) + later = self.make_closing("2026-04-01", "2026-06-30") + later.insert() # would raise if validate_duplicate wrongly flagged it as overlapping + self.assertTrue(frappe.db.exists("Stock Closing Entry", later.name))