Merge pull request #58902 from frappe/mergify/bp/version-16-hotfix/pr-58792

fix: calculate holiday list totals for half days (backport #58792)
This commit is contained in:
Sudharsanan Ashok
2026-09-09 13:25:46 +05:30
committed by GitHub
6 changed files with 143 additions and 8 deletions

View File

@@ -506,3 +506,4 @@ erpnext.patches.v16_0.rename_secondary_item_type_field
erpnext.patches.v16_0.append_fieldname_to_pos_search_fields
erpnext.patches.v16_0.set_secondary_item_valuation_type
erpnext.patches.v16_0.add_transaction_roles_to_sms_settings
erpnext.patches.v16_0.recalculate_holiday_list_totals

View File

@@ -0,0 +1,18 @@
import frappe
from frappe.query_builder import Case
from frappe.query_builder.functions import Coalesce, Sum
def execute():
holiday_list = frappe.qb.DocType("Holiday List")
holiday = frappe.qb.DocType("Holiday")
total_holidays = (
frappe.qb.from_(holiday)
.select(Sum(Case().when(holiday.is_half_day == 1, 0.5).else_(1)))
.where(
(holiday.parent == holiday_list.name)
& (holiday.parenttype == "Holiday List")
& (holiday.parentfield == "holidays")
)
)
frappe.qb.update(holiday_list).set(holiday_list.total_holidays, Coalesce(total_holidays, 0)).run()

View File

@@ -1,11 +1,18 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
function update_total_holidays(frm) {
let total_holidays = 0;
for (const holiday of frm.doc.holidays || []) {
total_holidays += holiday.is_half_day ? 0.5 : 1;
}
frm.doc.total_holidays = total_holidays;
frm.refresh_field("total_holidays");
}
frappe.ui.form.on("Holiday List", {
refresh: function (frm) {
if (frm.doc.holidays) {
frm.set_value("total_holidays", frm.doc.holidays.length);
}
update_total_holidays(frm);
frm.call("get_supported_countries").then((r) => {
frm.subdivisions_by_country = r.message.subdivisions_by_country;
@@ -43,6 +50,18 @@ frappe.ui.form.on("Holiday List", {
},
});
frappe.ui.form.on("Holiday", {
holidays_add: function (frm) {
update_total_holidays(frm);
},
holidays_remove: function (frm) {
update_total_holidays(frm);
},
is_half_day: function (frm) {
update_total_holidays(frm);
},
});
frappe.tour["Holiday List"] = [
{
fieldname: "holiday_list_name",

View File

@@ -58,9 +58,10 @@
},
{
"fieldname": "total_holidays",
"fieldtype": "Int",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Total Holidays",
"precision": "1",
"read_only": 1
},
{

View File

@@ -8,7 +8,7 @@ from datetime import date
import frappe
from frappe import _, throw
from frappe.model.document import Document
from frappe.utils import formatdate, getdate, today
from frappe.utils import cint, formatdate, getdate, today
class OverlapError(frappe.ValidationError):
@@ -34,7 +34,7 @@ class HolidayList(Document):
is_half_day: DF.Check
subdivision: DF.Autocomplete | None
to_date: DF.Date
total_holidays: DF.Int
total_holidays: DF.Float
weekly_off: DF.Literal[
"", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
]
@@ -42,10 +42,13 @@ class HolidayList(Document):
def validate(self):
self.validate_days()
self.total_holidays = len(self.holidays)
self.update_total_holidays()
self.validate_duplicate_date()
self.sort_holidays()
def update_total_holidays(self):
self.total_holidays = sum(0.5 if cint(holiday.is_half_day) else 1 for holiday in self.holidays)
@frappe.whitelist()
def get_weekly_off_dates(self):
if not self.weekly_off:
@@ -67,6 +70,8 @@ class HolidayList(Document):
},
)
self.update_total_holidays()
@frappe.whitelist()
def get_supported_countries(self):
from holidays.utils import list_supported_countries
@@ -108,6 +113,8 @@ class HolidayList(Document):
"holidays", {"description": holiday_name, "holiday_date": holiday_date, "weekly_off": 0}
)
self.update_total_holidays()
def sort_holidays(self):
self.holidays.sort(key=lambda x: getdate(x.holiday_date))
for i in range(len(self.holidays)):
@@ -153,6 +160,7 @@ class HolidayList(Document):
@frappe.whitelist()
def clear_table(self):
self.set("holidays", [])
self.update_total_holidays()
def validate_duplicate_date(self):
unique_dates = []

View File

@@ -5,7 +5,7 @@ from contextlib import contextmanager
from datetime import date, timedelta
import frappe
from frappe.utils import getdate
from frappe.utils import get_datetime, getdate
from erpnext.setup.doctype.holiday_list.holiday_list import local_country_name
from erpnext.tests.utils import ERPNextTestSuite
@@ -46,6 +46,94 @@ class TestHolidayList(ERPNextTestSuite):
self.assertIn(date(2023, 2, 26), holidays)
self.assertNotIn(date(2023, 3, 5), holidays)
def test_total_holidays_includes_half_days(self):
holiday_list = make_holiday_list(
"test_half_day_holiday_list",
from_date="2023-01-01",
to_date="2023-01-03",
holiday_dates=[
{"holiday_date": "2023-01-01", "description": "Full-day holiday"},
{
"holiday_date": "2023-01-02",
"description": "Half-day holiday",
"is_half_day": 1,
},
],
)
self.assertEqual(holiday_list.total_holidays, 1.5)
self.assertEqual(frappe.db.get_value("Holiday List", holiday_list.name, "total_holidays"), 1.5)
def test_weekly_off_updates_total_without_saving(self):
holiday_list = frappe.new_doc("Holiday List")
holiday_list.from_date = "2023-01-01"
holiday_list.to_date = "2023-01-14"
holiday_list.weekly_off = "Saturday"
holiday_list.is_half_day = 1
holiday_list.append("holidays", {"holiday_date": "2023-01-01", "description": "Full day"})
holiday_list.get_weekly_off_dates()
self.assertEqual(len(holiday_list.holidays), 3)
self.assertEqual(holiday_list.total_holidays, 2)
holiday_list.get_weekly_off_dates()
self.assertEqual(len(holiday_list.holidays), 3)
self.assertEqual(holiday_list.total_holidays, 2)
holiday_list.clear_table()
self.assertEqual(holiday_list.holidays, [])
self.assertEqual(holiday_list.total_holidays, 0)
def test_local_holidays_updates_total_without_saving(self):
holiday_list = frappe.new_doc("Holiday List")
holiday_list.from_date = "2023-01-01"
holiday_list.to_date = "2023-01-02"
holiday_list.country = "DE"
holiday_list.append(
"holidays", {"holiday_date": "2023-01-02", "description": "Half day", "is_half_day": 1}
)
holiday_list.get_local_holidays()
self.assertEqual(len(holiday_list.holidays), 2)
self.assertEqual(holiday_list.total_holidays, 1.5)
holiday_list.get_local_holidays()
self.assertEqual(len(holiday_list.holidays), 2)
self.assertEqual(holiday_list.total_holidays, 1.5)
def test_recalculate_existing_holiday_list_totals(self):
from erpnext.patches.v16_0.recalculate_holiday_list_totals import execute
cases = (("mixed", [0, 1], 1.5), ("half", [1, 1, 1], 1.5), ("full", [0, 0], 2), ("empty", [], 0))
holiday_lists = []
for name, half_days, expected in cases:
holiday_list = make_holiday_list(
f"test_backfill_holidays_{name}",
from_date="2023-01-01",
to_date="2023-01-03",
holiday_dates=[
{
"holiday_date": date(2023, 1, idx),
"description": "Test holiday",
"is_half_day": is_half_day,
}
for idx, is_half_day in enumerate(half_days, start=1)
],
)
# Simulate totals persisted by the old controller, including a stale empty list.
holiday_list.db_set("total_holidays", len(half_days) or 1, update_modified=False)
holiday_lists.append((holiday_list, expected))
for _ in range(2):
execute()
for holiday_list, expected in holiday_lists:
with self.subTest(holiday_list=holiday_list.name):
total, modified = frappe.db.get_value(
"Holiday List", holiday_list.name, ["total_holidays", "modified"]
)
self.assertEqual(total, expected)
self.assertEqual(modified, get_datetime(holiday_list.modified))
def test_local_holidays(self):
holiday_list = frappe.new_doc("Holiday List")
holiday_list.from_date = "2022-01-01"