From e37ceb5f69aa998f045964a536cd4c2d1ce800c8 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 3 Jul 2026 15:57:21 +0530 Subject: [PATCH 1/3] test: cover Stock Closing Entry duplicate date-range validation --- .../test_stock_closing_entry.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) 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..09670f0ff8c 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,33 @@ 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_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() + self.assertTrue(later.name) From ae0cfbd3e156a9ba087549e9fda65b9a757d7436 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 3 Jul 2026 17:06:51 +0530 Subject: [PATCH 2/3] test: assert the saved closing entry exists rather than a truthy name --- .../doctype/stock_closing_entry/test_stock_closing_entry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 09670f0ff8c..2f47551ff74 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 @@ -87,5 +87,5 @@ class TestStockClosingEntryDuplicate(ERPNextTestSuite): 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() - self.assertTrue(later.name) + later.insert() # would raise if validate_duplicate wrongly flagged it as overlapping + self.assertTrue(frappe.db.exists("Stock Closing Entry", later.name)) From 10c6cda6db4bbfb0e8a9b276f41498a42caeb88d Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Sat, 4 Jul 2026 17:47:22 +0530 Subject: [PATCH 3/3] fix: detect contained/enclosing date ranges in Stock Closing Entry duplicate check --- .../stock_closing_entry/stock_closing_entry.py | 9 ++++----- .../stock_closing_entry/test_stock_closing_entry.py | 12 ++++++++++++ 2 files changed, 16 insertions(+), 5 deletions(-) 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 2f47551ff74..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 @@ -84,6 +84,18 @@ class TestStockClosingEntryDuplicate(ERPNextTestSuite): 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")