From 8f2a7f848318d2ae253dd0b79858e5b2d485cc34 Mon Sep 17 00:00:00 2001 From: Patrick Eissler <77415730+PatrickDEissler@users.noreply.github.com> Date: Mon, 2 Feb 2026 10:29:56 +0100 Subject: [PATCH 01/26] feat(Absence Plan): pre-note leaves --- .../doctype/absence_plan/__init__.py | 0 .../doctype/absence_plan/absence_plan.js | 108 ++++++++ .../doctype/absence_plan/absence_plan.json | 251 ++++++++++++++++++ .../doctype/absence_plan/absence_plan.py | 106 ++++++++ .../doctype/absence_plan/absence_plan_list.js | 16 ++ .../doctype/absence_plan/test_absence_plan.py | 9 + .../doctype/absence_plan_date/__init__.py | 0 .../absence_plan_date/absence_plan_date.json | 51 ++++ .../absence_plan_date/absence_plan_date.py | 8 + 9 files changed, 549 insertions(+) create mode 100644 time_capture/time_capture/doctype/absence_plan/__init__.py create mode 100644 time_capture/time_capture/doctype/absence_plan/absence_plan.js create mode 100644 time_capture/time_capture/doctype/absence_plan/absence_plan.json create mode 100644 time_capture/time_capture/doctype/absence_plan/absence_plan.py create mode 100644 time_capture/time_capture/doctype/absence_plan/absence_plan_list.js create mode 100644 time_capture/time_capture/doctype/absence_plan/test_absence_plan.py create mode 100644 time_capture/time_capture/doctype/absence_plan_date/__init__.py create mode 100644 time_capture/time_capture/doctype/absence_plan_date/absence_plan_date.json create mode 100644 time_capture/time_capture/doctype/absence_plan_date/absence_plan_date.py diff --git a/time_capture/time_capture/doctype/absence_plan/__init__.py b/time_capture/time_capture/doctype/absence_plan/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/time_capture/time_capture/doctype/absence_plan/absence_plan.js b/time_capture/time_capture/doctype/absence_plan/absence_plan.js new file mode 100644 index 0000000..490a77b --- /dev/null +++ b/time_capture/time_capture/doctype/absence_plan/absence_plan.js @@ -0,0 +1,108 @@ +// Copyright (c) 2025, ALYF GmbH and contributors +// For license information, please see license.txt + +frappe.ui.form.on("Absence Plan", { + setup: function (frm) { + frm.set_query("leave_approver", function () { + return { + query: "hrms.hr.doctype.department_approver.department_approver.get_approvers", + filters: { + employee: frm.doc.employee, + doctype: "Leave Application", + }, + }; + }); + }, + + employee: function (frm) { + frm.trigger("set_leave_approver"); + }, + + bulk_insert_btn: function (frm) { + open_bulk_insert_dialog(frm); + }, + + set_leave_approver: function (frm) { + if (frm.doc.employee) { + return frappe.call({ + method: "hrms.hr.doctype.leave_application.leave_application.get_leave_approver", + args: { + employee: frm.doc.employee, + }, + callback: function (r) { + if (r && r.message) { + frm.set_value("leave_approver", r.message); + } + }, + }); + } + }, +}); + +function open_bulk_insert_dialog(frm) { + const d = new frappe.ui.Dialog({ + title: __("Bulk Insert"), + fields: [ + { + fieldtype: "Select", + fieldname: "mode", + label: __("Mode"), + options: "Weekly Off\nTimespan", + default: "Weekly Off", + reqd: 1, + }, + { + fieldtype: "Date", + fieldname: "from_date", + label: __("From Date"), + reqd: 1, + }, + { + fieldtype: "Date", + fieldname: "to_date", + label: __("To Date"), + reqd: 1, + }, + { + fieldtype: "Select", + fieldname: "weekday", + label: __("Weekday"), + options: "Monday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday", + depends_on: "eval:doc.mode === 'Weekly Off'", + mandatory_depends_on: "eval:doc.mode === 'Weekly Off'", + }, + { + fieldtype: "Small Text", + fieldname: "reason", + label: __("Reason"), + depends_on: "eval:doc.mode === 'Timespan'", + }, + ], + primary_action_label: __("Add"), + primary_action: function (values) { + frappe.call({ + method: "time_capture.time_capture.doctype.absence_plan.absence_plan.bulk_insert_dates", + args: { + doc: frm.doc, + mode: values.mode, + weekday: values.weekday, + from_date: values.from_date, + to_date: values.to_date, + reason: values.reason, + }, + callback: function (r) { + if (r && r.message && r.message.length) { + r.message.forEach(function (item) { + const row = frm.add_child("dates"); + row.date = item.date; + row.reason = item.reason || ""; + }); + frm.refresh_field("dates"); + } + d.hide(); + }, + }); + }, + }); + d.show(); +} diff --git a/time_capture/time_capture/doctype/absence_plan/absence_plan.json b/time_capture/time_capture/doctype/absence_plan/absence_plan.json new file mode 100644 index 0000000..98d34cd --- /dev/null +++ b/time_capture/time_capture/doctype/absence_plan/absence_plan.json @@ -0,0 +1,251 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2025-02-02 09:00:00", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "employee", + "employee_name", + "column_break_ihkc", + "leave_type", + "section_break_ueuf", + "from_date", + "column_break_iiup", + "to_date", + "dates_section", + "bulk_insert_btn", + "dates", + "approval_section", + "leave_approver", + "column_break_lfyt", + "status", + "amended_from" + ], + "fields": [ + { + "fieldname": "employee", + "fieldtype": "Link", + "in_filter": 1, + "in_list_view": 1, + "label": "Employee", + "options": "Employee", + "reqd": 1 + }, + { + "fetch_from": "employee.employee_name", + "fieldname": "employee_name", + "fieldtype": "Data", + "label": "Employee Name" + }, + { + "fieldname": "column_break_ihkc", + "fieldtype": "Column Break" + }, + { + "fieldname": "leave_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Leave Type", + "options": "Leave Type", + "reqd": 1 + }, + { + "fieldname": "dates_section", + "fieldtype": "Section Break", + "label": "Dates" + }, + { + "fieldname": "bulk_insert_btn", + "fieldtype": "Button", + "label": "Bulk Insert" + }, + { + "fieldname": "dates", + "fieldtype": "Table", + "label": "Dates", + "options": "Absence Plan Date" + }, + { + "fieldname": "approval_section", + "fieldtype": "Section Break", + "label": "Approval" + }, + { + "fieldname": "leave_approver", + "fieldtype": "Link", + "label": "Leave Approver", + "options": "User", + "reqd": 1 + }, + { + "fieldname": "column_break_lfyt", + "fieldtype": "Column Break" + }, + { + "allow_on_submit": 1, + "fieldname": "status", + "fieldtype": "Select", + "in_filter": 1, + "in_standard_filter": 1, + "label": "Status", + "no_copy": 1, + "options": "Open\nApproved\nRejected\nCancelled", + "permlevel": 1 + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Absence Plan", + "print_hide": 1, + "read_only": 1, + "search_index": 1 + }, + { + "fieldname": "section_break_ueuf", + "fieldtype": "Section Break" + }, + { + "fieldname": "from_date", + "fieldtype": "Date", + "label": "From Date", + "read_only": 1 + }, + { + "fieldname": "column_break_iiup", + "fieldtype": "Column Break" + }, + { + "fieldname": "to_date", + "fieldtype": "Date", + "label": "To Date", + "read_only": 1 + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [], + "modified": "2026-02-02 10:25:00.000000", + "modified_by": "Administrator", + "module": "Time Capture", + "name": "Absence Plan", + "owner": "Administrator", + "permissions": [ + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Employee", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "permlevel": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR User", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Leave Approver", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "permlevel": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Leave Approver", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "permlevel": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR User", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "permlevel": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR Manager", + "share": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "title_field": "employee_name" +} diff --git a/time_capture/time_capture/doctype/absence_plan/absence_plan.py b/time_capture/time_capture/doctype/absence_plan/absence_plan.py new file mode 100644 index 0000000..b97a284 --- /dev/null +++ b/time_capture/time_capture/doctype/absence_plan/absence_plan.py @@ -0,0 +1,106 @@ +# Copyright (c) 2025, ALYF GmbH and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.model.document import Document +from frappe.utils import getdate +from frappe.utils.data import add_to_date +from hrms.hr.utils import share_doc_with_approver + + +class AbsencePlan(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + from time_capture.time_capture.doctype.absence_plan_date.absence_plan_date import ( + AbsencePlanDate, + ) + + amended_from: DF.Link | None + employee: DF.Link + employee_name: DF.Data | None + from_date: DF.Date | None + leave_approver: DF.Link + leave_type: DF.Link + status: DF.Literal["Open", "Approved", "Rejected", "Cancelled"] + dates: DF.Table[AbsencePlanDate] + to_date: DF.Date | None + # end: auto-generated types + + def before_validate(self): + self.remove_duplicate_dates() + if self.dates: + self.from_date = min([row.date for row in self.dates]) + self.to_date = max([row.date for row in self.dates]) + + def on_update(self): + share_doc_with_approver(self, self.leave_approver) + + def remove_duplicate_dates(self): + """Keep only the first occurrence of each date in dates.""" + seen = set() + new_rows = [] + for row in self.dates or []: + key = str(row.date) if row.date else None + if key is None: + new_rows.append(row) + elif key not in seen: + seen.add(key) + new_rows.append(row) + self.dates = new_rows + + def on_submit(self): + if self.status in ["Open", "Cancelled"]: + frappe.throw(_("Only Absence Plans with status 'Approved' and 'Rejected' can be submitted")) + + def before_cancel(self): + self.status = "Cancelled" + + +@frappe.whitelist() +def bulk_insert_dates(mode, from_date, to_date, weekday=None, reason=None): + """Return list of {date, reason} to add. Called from Bulk Insert dialog.""" + frappe.has_permission("Absence Plan", "write", throw=True) + if mode == "Weekly Off": + if not weekday: + frappe.throw(_("Please select at least one weekday")) + return _get_weekly_off_dates(from_date, to_date, weekday) + elif mode == "Timespan": + return _get_timespan_days(from_date, to_date, reason) + + frappe.throw(_("Invalid mode")) + + +def _get_weekly_off_dates(from_date, to_date, weekday): + weekdays = { + "Monday": 0, + "Tuesday": 1, + "Wednesday": 2, + "Thursday": 3, + "Friday": 4, + "Saturday": 5, + "Sunday": 6, + } + weekday_index = weekdays[weekday] + dates = [] + dt = getdate(from_date) + while dt <= getdate(to_date): + if dt.weekday() == weekday_index: + dates.append({"date": str(dt), "reason": weekday}) + dt = add_to_date(dt, days=1) + return dates + + +def _get_timespan_days(from_date, to_date, reason=None): + dates = [] + dt = getdate(from_date) + while dt <= getdate(to_date): + dates.append({"date": str(dt), "reason": reason}) + dt = add_to_date(dt, days=1) + return dates diff --git a/time_capture/time_capture/doctype/absence_plan/absence_plan_list.js b/time_capture/time_capture/doctype/absence_plan/absence_plan_list.js new file mode 100644 index 0000000..6d05d98 --- /dev/null +++ b/time_capture/time_capture/doctype/absence_plan/absence_plan_list.js @@ -0,0 +1,16 @@ +frappe.listview_settings["Absence Plan"] = { + has_indicator_for_draft: 1, + get_indicator: function (doc) { + const status_color = { + Approved: "green", + Rejected: "red", + Open: "orange", + Draft: "red", + Cancelled: "red", + Submitted: "blue", + }; + const status = + !doc.docstatus && ["Approved", "Rejected"].includes(doc.status) ? "Draft" : doc.status; + return [__(status), status_color[status], "status,=," + doc.status]; + }, +}; diff --git a/time_capture/time_capture/doctype/absence_plan/test_absence_plan.py b/time_capture/time_capture/doctype/absence_plan/test_absence_plan.py new file mode 100644 index 0000000..b587bb4 --- /dev/null +++ b/time_capture/time_capture/doctype/absence_plan/test_absence_plan.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, ALYF GmbH and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestAbsencePlan(FrappeTestCase): + pass diff --git a/time_capture/time_capture/doctype/absence_plan_date/__init__.py b/time_capture/time_capture/doctype/absence_plan_date/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/time_capture/time_capture/doctype/absence_plan_date/absence_plan_date.json b/time_capture/time_capture/doctype/absence_plan_date/absence_plan_date.json new file mode 100644 index 0000000..ee8da12 --- /dev/null +++ b/time_capture/time_capture/doctype/absence_plan_date/absence_plan_date.json @@ -0,0 +1,51 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2025-02-02 09:00:00.000000", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "date", + "weekday", + "reason" + ], + "fields": [ + { + "fieldname": "date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Date", + "reqd": 1 + }, + { + "fieldname": "weekday", + "fieldtype": "Select", + "label": "Weekday", + "options": "Monday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday", + "read_only": 1, + "no_copy": 1 + }, + { + "fieldname": "reason", + "fieldtype": "Text Editor", + "in_list_view": 1, + "label": "Reason", + "description": "Examples: School or weekly off, etc." + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2026-02-02 10:25:00.000000", + "modified_by": "Administrator", + "module": "Time Capture", + "name": "Absence Plan Date", + "owner": "Administrator", + "permissions": [], + "row_format": "Dynamic", + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} diff --git a/time_capture/time_capture/doctype/absence_plan_date/absence_plan_date.py b/time_capture/time_capture/doctype/absence_plan_date/absence_plan_date.py new file mode 100644 index 0000000..1c537b7 --- /dev/null +++ b/time_capture/time_capture/doctype/absence_plan_date/absence_plan_date.py @@ -0,0 +1,8 @@ +# Copyright (c) 2025, ALYF GmbH and contributors +# For license information, please see license.txt + +from frappe.model.document import Document + + +class AbsencePlanDate(Document): + pass From c6001d768c2b47aa0916eb7d2681fd48126fd656 Mon Sep 17 00:00:00 2001 From: Patrick Eissler <77415730+PatrickDEissler@users.noreply.github.com> Date: Mon, 2 Feb 2026 10:39:59 +0100 Subject: [PATCH 02/26] fix: validate sequential order of dates --- time_capture/time_capture/doctype/absence_plan/absence_plan.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/time_capture/time_capture/doctype/absence_plan/absence_plan.py b/time_capture/time_capture/doctype/absence_plan/absence_plan.py index b97a284..b05ba1c 100644 --- a/time_capture/time_capture/doctype/absence_plan/absence_plan.py +++ b/time_capture/time_capture/doctype/absence_plan/absence_plan.py @@ -67,6 +67,9 @@ def before_cancel(self): def bulk_insert_dates(mode, from_date, to_date, weekday=None, reason=None): """Return list of {date, reason} to add. Called from Bulk Insert dialog.""" frappe.has_permission("Absence Plan", "write", throw=True) + if getdate(to_date) < getdate(from_date): + frappe.throw(_("To Date cannot be before From Date.")) + if mode == "Weekly Off": if not weekday: frappe.throw(_("Please select at least one weekday")) From 3ebea282e32b02acd519cd6690be9feae6935261 Mon Sep 17 00:00:00 2001 From: Patrick Eissler <77415730+PatrickDEissler@users.noreply.github.com> Date: Mon, 2 Feb 2026 10:41:50 +0100 Subject: [PATCH 03/26] perf: minimize usage of getdate method --- .../doctype/absence_plan/absence_plan.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/time_capture/time_capture/doctype/absence_plan/absence_plan.py b/time_capture/time_capture/doctype/absence_plan/absence_plan.py index b05ba1c..905a53a 100644 --- a/time_capture/time_capture/doctype/absence_plan/absence_plan.py +++ b/time_capture/time_capture/doctype/absence_plan/absence_plan.py @@ -67,7 +67,9 @@ def before_cancel(self): def bulk_insert_dates(mode, from_date, to_date, weekday=None, reason=None): """Return list of {date, reason} to add. Called from Bulk Insert dialog.""" frappe.has_permission("Absence Plan", "write", throw=True) - if getdate(to_date) < getdate(from_date): + to_date = getdate(to_date) + from_date = getdate(from_date) + if to_date < from_date: frappe.throw(_("To Date cannot be before From Date.")) if mode == "Weekly Off": @@ -92,8 +94,8 @@ def _get_weekly_off_dates(from_date, to_date, weekday): } weekday_index = weekdays[weekday] dates = [] - dt = getdate(from_date) - while dt <= getdate(to_date): + dt = from_date + while dt <= to_date: if dt.weekday() == weekday_index: dates.append({"date": str(dt), "reason": weekday}) dt = add_to_date(dt, days=1) @@ -102,8 +104,8 @@ def _get_weekly_off_dates(from_date, to_date, weekday): def _get_timespan_days(from_date, to_date, reason=None): dates = [] - dt = getdate(from_date) - while dt <= getdate(to_date): + dt = from_date + while dt <= to_date: dates.append({"date": str(dt), "reason": reason}) dt = add_to_date(dt, days=1) return dates From af5ecada2078f3941c9661c605610ac95399c8e5 Mon Sep 17 00:00:00 2001 From: Patrick Eissler <77415730+PatrickDEissler@users.noreply.github.com> Date: Mon, 2 Feb 2026 10:44:02 +0100 Subject: [PATCH 04/26] fix: avoid past dates --- .../time_capture/doctype/absence_plan/absence_plan.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/time_capture/time_capture/doctype/absence_plan/absence_plan.py b/time_capture/time_capture/doctype/absence_plan/absence_plan.py index 905a53a..2651e38 100644 --- a/time_capture/time_capture/doctype/absence_plan/absence_plan.py +++ b/time_capture/time_capture/doctype/absence_plan/absence_plan.py @@ -67,10 +67,12 @@ def before_cancel(self): def bulk_insert_dates(mode, from_date, to_date, weekday=None, reason=None): """Return list of {date, reason} to add. Called from Bulk Insert dialog.""" frappe.has_permission("Absence Plan", "write", throw=True) - to_date = getdate(to_date) from_date = getdate(from_date) + to_date = getdate(to_date) if to_date < from_date: frappe.throw(_("To Date cannot be before From Date.")) + if from_date < getdate(): + frappe.throw(_("Dates cannot be in the past.")) if mode == "Weekly Off": if not weekday: From 5190661cbbb1a003060ec0764e1e5a1290dba6a4 Mon Sep 17 00:00:00 2001 From: Patrick Eissler <77415730+PatrickDEissler@users.noreply.github.com> Date: Mon, 2 Feb 2026 10:47:45 +0100 Subject: [PATCH 05/26] feat: order dates --- .../time_capture/doctype/absence_plan/absence_plan.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/time_capture/time_capture/doctype/absence_plan/absence_plan.py b/time_capture/time_capture/doctype/absence_plan/absence_plan.py index 2651e38..3ac0e12 100644 --- a/time_capture/time_capture/doctype/absence_plan/absence_plan.py +++ b/time_capture/time_capture/doctype/absence_plan/absence_plan.py @@ -38,10 +38,14 @@ def before_validate(self): if self.dates: self.from_date = min([row.date for row in self.dates]) self.to_date = max([row.date for row in self.dates]) + self.order_dates() def on_update(self): share_doc_with_approver(self, self.leave_approver) + def before_update_after_submit(self): + self.order_dates() + def remove_duplicate_dates(self): """Keep only the first occurrence of each date in dates.""" seen = set() @@ -55,6 +59,9 @@ def remove_duplicate_dates(self): new_rows.append(row) self.dates = new_rows + def order_dates(self): + self.dates = sorted(self.dates, key=lambda x: x.date) + def on_submit(self): if self.status in ["Open", "Cancelled"]: frappe.throw(_("Only Absence Plans with status 'Approved' and 'Rejected' can be submitted")) From e576e5ec53a5fd390defaaf23ae9479a741d16ff Mon Sep 17 00:00:00 2001 From: Patrick Eissler <77415730+PatrickDEissler@users.noreply.github.com> Date: Mon, 2 Feb 2026 12:21:13 +0100 Subject: [PATCH 06/26] fix: avoid overlapping absence_plans --- .../doctype/absence_plan/absence_plan.py | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/time_capture/time_capture/doctype/absence_plan/absence_plan.py b/time_capture/time_capture/doctype/absence_plan/absence_plan.py index 3ac0e12..6532c5a 100644 --- a/time_capture/time_capture/doctype/absence_plan/absence_plan.py +++ b/time_capture/time_capture/doctype/absence_plan/absence_plan.py @@ -4,7 +4,7 @@ import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import getdate +from frappe.utils import get_link_to_form, getdate from frappe.utils.data import add_to_date from hrms.hr.utils import share_doc_with_approver @@ -40,11 +40,15 @@ def before_validate(self): self.to_date = max([row.date for row in self.dates]) self.order_dates() + def validate(self): + self.avoid_overlapping_absence_plans() + def on_update(self): share_doc_with_approver(self, self.leave_approver) def before_update_after_submit(self): self.order_dates() + self.avoid_overlapping_absence_plans() def remove_duplicate_dates(self): """Keep only the first occurrence of each date in dates.""" @@ -62,6 +66,27 @@ def remove_duplicate_dates(self): def order_dates(self): self.dates = sorted(self.dates, key=lambda x: x.date) + def avoid_overlapping_absence_plans(self): + overlapping_absence_plans = frappe.db.get_all( + "Absence Plan", + {"docstatus": ["!=", 2], "employee": self.employee, "name": ["!=", self.name]}, + or_filters={ + "from_date": ["between", [self.from_date, self.to_date]], + "to_date": ["between", [self.from_date, self.to_date]], + }, + ) + if overlapping_absence_plans: + frappe.throw( + _( + "There is already an Absence Plan for this employee for the selected date range. See here: {0}" + ).format( + get_link_to_form( + "Absence Plan", overlapping_absence_plans[0].name, _("Absence Plans (List)") + ) + ) + ) + return + def on_submit(self): if self.status in ["Open", "Cancelled"]: frappe.throw(_("Only Absence Plans with status 'Approved' and 'Rejected' can be submitted")) From 6ee470f10dda72b5829c06890e943d2db5da6308 Mon Sep 17 00:00:00 2001 From: Patrick Eissler <77415730+PatrickDEissler@users.noreply.github.com> Date: Mon, 2 Feb 2026 12:47:08 +0100 Subject: [PATCH 07/26] feat: add notification workflow --- .../doctype/absence_plan/absence_plan.py | 54 +++++++++++++++++-- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/time_capture/time_capture/doctype/absence_plan/absence_plan.py b/time_capture/time_capture/doctype/absence_plan/absence_plan.py index 6532c5a..1fcbba0 100644 --- a/time_capture/time_capture/doctype/absence_plan/absence_plan.py +++ b/time_capture/time_capture/doctype/absence_plan/absence_plan.py @@ -33,6 +33,9 @@ class AbsencePlan(Document): to_date: DF.Date | None # end: auto-generated types + def after_insert(self): + self.notify_leave_approver() + def before_validate(self): self.remove_duplicate_dates() if self.dates: @@ -50,6 +53,14 @@ def before_update_after_submit(self): self.order_dates() self.avoid_overlapping_absence_plans() + def on_submit(self): + if self.status in ["Open", "Cancelled"]: + frappe.throw(_("Only Absence Plans with status 'Approved' and 'Rejected' can be submitted")) + self.notify_employee() + + def before_cancel(self): + self.status = "Cancelled" + def remove_duplicate_dates(self): """Keep only the first occurrence of each date in dates.""" seen = set() @@ -87,12 +98,17 @@ def avoid_overlapping_absence_plans(self): ) return - def on_submit(self): - if self.status in ["Open", "Cancelled"]: - frappe.throw(_("Only Absence Plans with status 'Approved' and 'Rejected' can be submitted")) + def notify_leave_approver(self): + message = _("{0} raised a new Absence Plan for approval: {1}").format( + self.employee_name or self.employee, self.name + ) + _create_pwa_notification(self.leave_approver, self.employee, "Leave Approver", message, self.name) - def before_cancel(self): - self.status = "Cancelled" + def notify_employee(self): + message = _("Your Absence Plan {0} has been {1}.").format( + self.name, _("Approved" if self.status == "Approved" else "Rejected") + ) + _create_pwa_notification(self.leave_approver, self.employee, "Employee", message, self.name) @frappe.whitelist() @@ -143,3 +159,31 @@ def _get_timespan_days(from_date, to_date, reason=None): dates.append({"date": str(dt), "reason": reason}) dt = add_to_date(dt, days=1) return dates + + +def _create_pwa_notification( + leave_approver: str, employee: str, send_to: str, message: str, absence_plan_name: str +): + employee_user = frappe.db.get_value("Employee", employee, "user_id", cache=True) + if send_to == "Leave Approver": + from_user = leave_approver + to_user = employee_user + elif send_to == "Employee": + from_user = employee_user + to_user = leave_approver + else: + frappe.throw(_("Send to has to be either 'Leave Approver' or 'Employee'.")) + + if not employee_user or not leave_approver: + return + + if from_user == to_user: + return + + n = frappe.new_doc("PWA Notification") + n.from_user = from_user + n.to_user = to_user + n.message = message + n.reference_document_type = "Absence Plan" + n.reference_document_name = absence_plan_name + n.insert(ignore_permissions=True) From 5851ccf1004e058baf7dad09cc56ebd2fc088b1c Mon Sep 17 00:00:00 2001 From: Patrick Eissler <77415730+PatrickDEissler@users.noreply.github.com> Date: Mon, 2 Feb 2026 17:23:46 +0100 Subject: [PATCH 08/26] feat: add notification workflow --- time_capture/locale/de.po | 103 +++++++++++++++- time_capture/locale/main.pot | 103 +++++++++++++++- .../doctype/absence_plan/absence_plan.json | 13 ++- .../doctype/absence_plan/absence_plan.py | 110 ++++++++++++------ 4 files changed, 285 insertions(+), 44 deletions(-) diff --git a/time_capture/locale/de.po b/time_capture/locale/de.po index 02da69c..28f2b4f 100644 --- a/time_capture/locale/de.po +++ b/time_capture/locale/de.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: Time Capture VERSION\n" "Report-Msgid-Bugs-To: patrick@alyf.de\n" -"POT-Creation-Date: 2026-01-22 17:02+0053\n" -"PO-Revision-Date: 2025-11-07 14:07+0053\n" +"POT-Creation-Date: 2026-02-02 17:16+0053\n" +"PO-Revision-Date: 2025-02-02 17:08+0053\n" "Last-Translator: patrick@alyf.de\n" "Language-Team: patrick@alyf.de\n" "MIME-Version: 1.0\n" @@ -31,11 +31,45 @@ msgstr "Verwaltung" msgid "Freelancer Time Capture" msgstr "Freelancer Zeiterfassung" +#. Name of a DocType +#: time_capture/time_capture/doctype/absence_plan/absence_plan.json +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:92 +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:123 +msgid "Absence Plan" +msgstr "Abwesenheitsplan" + +#. Name of a DocType +#: time_capture/time_capture/doctype/absence_plan_date/absence_plan_date.json +msgid "Absence Plan Date" +msgstr "Abwesenheitsplan-Datum" + +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:126 +msgid "Absence Plan {0} - {1}" +msgstr "Abwesenheitsplan {0} - {1}" + +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:95 +msgid "Absence Plan {0} for approval" +msgstr "Abwesenheitsplan {0} zum Freigeben" + +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:162 +msgid "Absence Plans (List)" +msgstr "Abwesenheitspläne (Liste)" + #. Label of a Duration field in DocType 'Mandatory Break' #: time_capture/time_capture/doctype/mandatory_break/mandatory_break.json msgid "Additional Break" msgstr "Zusätzliche Pause" +#. Label of a Section Break field in DocType 'Absence Plan' +#: time_capture/time_capture/doctype/absence_plan/absence_plan.json +msgid "Approval" +msgstr "Freigabe" + +#. Option for the 'Status' (Select) field in DocType 'Absence Plan' +#: time_capture/time_capture/doctype/absence_plan/absence_plan.json +msgid "Approved" +msgstr "Genehmigt" + #: time_capture/time_capture/report/working_time/working_time.js:24 msgid "April" msgstr "April" @@ -58,6 +92,12 @@ msgstr "August" msgid "Break" msgstr "Pause" +#. Label of a Button field in DocType 'Absence Plan' +#: time_capture/time_capture/doctype/absence_plan/absence_plan.js:44 +#: time_capture/time_capture/doctype/absence_plan/absence_plan.json +msgid "Bulk Insert" +msgstr "Massen einfügen" + #: time_capture/custom_fields.py:60 time_capture/public/js/employee.js:29 msgid "Change/Add Expected Hours" msgstr "Erwartete Stunden ändern/hinzufügen" @@ -103,6 +143,10 @@ msgstr "Freelancer-Benutzer wird erstellt" msgid "Current Balance" msgstr "Aktuelles Saldo" +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:178 +msgid "Dates cannot be in the past." +msgstr "Daten dürfen nicht in der Vergangenheit liegen." + #: time_capture/time_capture/report/working_time/working_time.js:32 msgid "December" msgstr "Dezember" @@ -126,6 +170,11 @@ msgstr "Entwürfe" msgid "Duplicate Entry Found" msgstr "Doppelter Eintrag gefunden" +#. Label of a Data field in DocType 'Absence Plan' +#: time_capture/time_capture/doctype/absence_plan/absence_plan.json +msgid "Employee Email" +msgstr "Mitarbeiter-E-Mail" + #. Name of a DocType #: time_capture/time_capture/doctype/employee_expected_working_hours/employee_expected_working_hours.json msgid "Employee Expected Working Hours" @@ -140,6 +189,10 @@ msgstr "Mitarbeiterstatus" msgid "Employee {0} has Attendances outside the Holiday List period." msgstr "Mitarbeiter {0} hat Anwesenheiten außerhalb der Periode der Feiertagsliste." +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:129 +msgid "Employee {0} has been notified via Email." +msgstr "Mitarbeiter {0} wurde per E-Mail benachrichtigt." + #. Label of a Check field in DocType 'Time Capture Settings' #: time_capture/time_capture/doctype/time_capture_settings/time_capture_settings.json msgid "Enable Reminders" @@ -170,6 +223,12 @@ msgstr "Alle X Tage" msgid "Example: If you set this field to '3', the system sends Notifications every 3 days." msgstr "Beispiel: Wenn Sie dieses Feld auf '3' setzen, sendet das System alle 3 Tage Benachrichtigungen." +#. Description of the 'Reason' (Text Editor) field in DocType 'Absence Plan +#. Date' +#: time_capture/time_capture/doctype/absence_plan_date/absence_plan_date.json +msgid "Examples: School or weekly off, etc." +msgstr "Beispiele: Schule, wöchentlicher freier Tag usw." + #. Label of a Float field in DocType 'Employee Expected Working Hours' #: time_capture/time_capture/doctype/employee_expected_working_hours/employee_expected_working_hours.json msgid "Expected Daily Working Hours" @@ -292,6 +351,10 @@ msgstr "Angegebene Pause" msgid "Indicated Break is not in the proper format." msgstr "Angegebene Pause ist nicht im korrekten Format." +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:187 +msgid "Invalid mode" +msgstr "Ungültiger Modus" + #. Label of a Check field in DocType 'Time Capture' #: time_capture/time_capture/doctype/time_capture/time_capture.json msgid "Is Of Legal Age" @@ -318,6 +381,12 @@ msgstr "Letzte manuelle Saldo-Korrektur" msgid "Last Notification (Sent On)" msgstr "Letzte Benachrichtigung (gesendet am)" +#. Label of a Link field in DocType 'Absence Plan' +#. Name of a role +#: time_capture/time_capture/doctype/absence_plan/absence_plan.json +msgid "Leave Approver" +msgstr "Urlaubsgenehmiger" + #: time_capture/custom_fields.py:65 msgid "Leave Policy" msgstr "Urlaubsrichtlinie" @@ -330,7 +399,9 @@ msgstr "Urlaubsrichtlinie wurde geändert. Hinweis: Dies aktualisiert keine best msgid "Leave Summary" msgstr "Urlaubsübersicht" +#. Label of a Link field in DocType 'Absence Plan' #: time_capture/public/js/utils.js:143 +#: time_capture/time_capture/doctype/absence_plan/absence_plan.json msgid "Leave Type" msgstr "Urlaubstyp" @@ -339,6 +410,10 @@ msgstr "Urlaubstyp" msgid "Leave and Working Time Summaries" msgstr "Urlaubs- und Arbeitszeitübersichten" +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:98 +msgid "Leave approver {0} has been notified via Email." +msgstr "Urlaubsgenehmiger {0} wurde per E-Mail benachrichtigt." + #. Label of a Section Break field in DocType 'Time Capture Settings' #: time_capture/time_capture/doctype/time_capture_settings/time_capture_settings.json msgid "Leaves" @@ -375,6 +450,10 @@ msgstr "Mai" msgid "Minimum Draft Age (Days)" msgstr "Mindestalter für Entwürfe (Tage)" +#: time_capture/time_capture/doctype/absence_plan/absence_plan.js:49 +msgid "Mode" +msgstr "Modus" + #: time_capture/custom_fields.py:41 msgid "No Supervisor Required" msgstr "Kein Vorgesetzter Benötigt" @@ -435,6 +514,10 @@ msgstr "November" msgid "October" msgstr "Oktober" +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:62 +msgid "Only Absence Plans with status 'Approved' and 'Rejected' can be submitted" +msgstr "Nur Abwesenheitspläne mit Status 'Genehmigt' und 'Abgelehnt' können gebucht werden." + #: time_capture/public/js/employee.js:11 msgid "Only System Manager are allowed to edit Expected Working Hours." msgstr "Nur System Manager dürfen Erwartete Arbeitsstunden anpassen." @@ -486,6 +569,10 @@ msgstr "Bitte überprüfen und buchen Sie die ausstehenden Zeiterfassungen." msgid "Please save the Employee first." msgstr "Bitte speichern Sie den Mitarbeiter zuerst." +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:182 +msgid "Please select at least one weekday" +msgstr "Bitte wählen Sie mindestens einen Wochentag." + #: time_capture/time_capture/doctype/create_freelancer/create_freelancer.js:18 msgid "Please wait..." msgstr "Bitte warten..." @@ -579,6 +666,10 @@ msgstr "Der Vorgesetzte kann nicht gleich dem Mitarbeiter sein." msgid "The supervisor has no User linked in his/her Employee record." msgstr "Der Vorgesetzte hat keinen Nutzer in seinem/ihren Mitarbeiter-Eintrag." +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:158 +msgid "There is already an Absence Plan for this employee for the selected date range. See here: {0}" +msgstr "Es existiert bereits ein Abwesenheitsplan für diesen Mitarbeiter im gewählten Zeitraum. Siehe hier: {0}" + #: time_capture/scripts/employee.py:87 msgid "This Employee has no supervisor (Reports To), but has Create User Permission checkbox enabled. Please, make sure that this intended." msgstr "Dieser Mitarbeiter hat keinen Vorgesetzten, aber er hat Benutzerberechtigung Erstellen aktiviert. Bitte prüfen Sie, ob das gewollt ist." @@ -673,6 +764,10 @@ msgstr "Arbeitszeitübersicht" msgid "Working time must be completely booked on projects and tasks." msgstr "Die Arbeitszeit muss vollständig auf Projekte und Aufgaben gebucht werden." +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:113 +msgid "Your Absence Plan {0} has been {1}." +msgstr "Ihr Abwesenheitsplan {0} wurde {1}" + #: time_capture/scripts/summary_utils.py:109 msgid "{0} (on {1})" msgstr "{0} (am {1})" @@ -685,3 +780,7 @@ msgstr "{0} Attendances erfolgreich aktualisiert." msgid "{0} already exists for this date and person." msgstr "{0} existiert bereits für dieses Datum und diese Person." +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:80 +msgid "{0} raised a new Absence Plan for approval: {1}" +msgstr "{0} hat einen neuen Abwesenheitsplan zum Freigeben erstellt: {1}" + diff --git a/time_capture/locale/main.pot b/time_capture/locale/main.pot index 0e2f2f5..6002e23 100644 --- a/time_capture/locale/main.pot +++ b/time_capture/locale/main.pot @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: Time Capture VERSION\n" "Report-Msgid-Bugs-To: patrick@alyf.de\n" -"POT-Creation-Date: 2026-01-22 17:02+0053\n" -"PO-Revision-Date: 2026-01-22 17:02+0053\n" +"POT-Creation-Date: 2026-02-02 17:16+0053\n" +"PO-Revision-Date: 2026-02-02 17:16+0053\n" "Last-Translator: patrick@alyf.de\n" "Language-Team: patrick@alyf.de\n" "MIME-Version: 1.0\n" @@ -31,11 +31,45 @@ msgstr "" msgid "Freelancer Time Capture" msgstr "" +#. Name of a DocType +#: time_capture/time_capture/doctype/absence_plan/absence_plan.json +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:92 +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:123 +msgid "Absence Plan" +msgstr "" + +#. Name of a DocType +#: time_capture/time_capture/doctype/absence_plan_date/absence_plan_date.json +msgid "Absence Plan Date" +msgstr "" + +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:126 +msgid "Absence Plan {0} - {1}" +msgstr "" + +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:95 +msgid "Absence Plan {0} for approval" +msgstr "" + +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:162 +msgid "Absence Plans (List)" +msgstr "" + #. Label of a Duration field in DocType 'Mandatory Break' #: time_capture/time_capture/doctype/mandatory_break/mandatory_break.json msgid "Additional Break" msgstr "" +#. Label of a Section Break field in DocType 'Absence Plan' +#: time_capture/time_capture/doctype/absence_plan/absence_plan.json +msgid "Approval" +msgstr "" + +#. Option for the 'Status' (Select) field in DocType 'Absence Plan' +#: time_capture/time_capture/doctype/absence_plan/absence_plan.json +msgid "Approved" +msgstr "" + #: time_capture/time_capture/report/working_time/working_time.js:24 msgid "April" msgstr "" @@ -58,6 +92,12 @@ msgstr "" msgid "Break" msgstr "" +#. Label of a Button field in DocType 'Absence Plan' +#: time_capture/time_capture/doctype/absence_plan/absence_plan.js:44 +#: time_capture/time_capture/doctype/absence_plan/absence_plan.json +msgid "Bulk Insert" +msgstr "" + #: time_capture/custom_fields.py:60 time_capture/public/js/employee.js:29 msgid "Change/Add Expected Hours" msgstr "" @@ -103,6 +143,10 @@ msgstr "" msgid "Current Balance" msgstr "" +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:178 +msgid "Dates cannot be in the past." +msgstr "" + #: time_capture/time_capture/report/working_time/working_time.js:32 msgid "December" msgstr "" @@ -126,6 +170,11 @@ msgstr "" msgid "Duplicate Entry Found" msgstr "" +#. Label of a Data field in DocType 'Absence Plan' +#: time_capture/time_capture/doctype/absence_plan/absence_plan.json +msgid "Employee Email" +msgstr "" + #. Name of a DocType #: time_capture/time_capture/doctype/employee_expected_working_hours/employee_expected_working_hours.json msgid "Employee Expected Working Hours" @@ -140,6 +189,10 @@ msgstr "" msgid "Employee {0} has Attendances outside the Holiday List period." msgstr "" +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:129 +msgid "Employee {0} has been notified via Email." +msgstr "" + #. Label of a Check field in DocType 'Time Capture Settings' #: time_capture/time_capture/doctype/time_capture_settings/time_capture_settings.json msgid "Enable Reminders" @@ -170,6 +223,12 @@ msgstr "" msgid "Example: If you set this field to '3', the system sends Notifications every 3 days." msgstr "" +#. Description of the 'Reason' (Text Editor) field in DocType 'Absence Plan +#. Date' +#: time_capture/time_capture/doctype/absence_plan_date/absence_plan_date.json +msgid "Examples: School or weekly off, etc." +msgstr "" + #. Label of a Float field in DocType 'Employee Expected Working Hours' #: time_capture/time_capture/doctype/employee_expected_working_hours/employee_expected_working_hours.json msgid "Expected Daily Working Hours" @@ -292,6 +351,10 @@ msgstr "" msgid "Indicated Break is not in the proper format." msgstr "" +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:187 +msgid "Invalid mode" +msgstr "" + #. Label of a Check field in DocType 'Time Capture' #: time_capture/time_capture/doctype/time_capture/time_capture.json msgid "Is Of Legal Age" @@ -318,6 +381,12 @@ msgstr "" msgid "Last Notification (Sent On)" msgstr "" +#. Label of a Link field in DocType 'Absence Plan' +#. Name of a role +#: time_capture/time_capture/doctype/absence_plan/absence_plan.json +msgid "Leave Approver" +msgstr "" + #: time_capture/custom_fields.py:65 msgid "Leave Policy" msgstr "" @@ -330,7 +399,9 @@ msgstr "" msgid "Leave Summary" msgstr "" +#. Label of a Link field in DocType 'Absence Plan' #: time_capture/public/js/utils.js:143 +#: time_capture/time_capture/doctype/absence_plan/absence_plan.json msgid "Leave Type" msgstr "" @@ -339,6 +410,10 @@ msgstr "" msgid "Leave and Working Time Summaries" msgstr "" +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:98 +msgid "Leave approver {0} has been notified via Email." +msgstr "" + #. Label of a Section Break field in DocType 'Time Capture Settings' #: time_capture/time_capture/doctype/time_capture_settings/time_capture_settings.json msgid "Leaves" @@ -375,6 +450,10 @@ msgstr "" msgid "Minimum Draft Age (Days)" msgstr "" +#: time_capture/time_capture/doctype/absence_plan/absence_plan.js:49 +msgid "Mode" +msgstr "" + #: time_capture/custom_fields.py:41 msgid "No Supervisor Required" msgstr "" @@ -435,6 +514,10 @@ msgstr "" msgid "October" msgstr "" +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:62 +msgid "Only Absence Plans with status 'Approved' and 'Rejected' can be submitted" +msgstr "" + #: time_capture/public/js/employee.js:11 msgid "Only System Manager are allowed to edit Expected Working Hours." msgstr "" @@ -486,6 +569,10 @@ msgstr "" msgid "Please save the Employee first." msgstr "" +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:182 +msgid "Please select at least one weekday" +msgstr "" + #: time_capture/time_capture/doctype/create_freelancer/create_freelancer.js:18 msgid "Please wait..." msgstr "" @@ -579,6 +666,10 @@ msgstr "" msgid "The supervisor has no User linked in his/her Employee record." msgstr "" +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:158 +msgid "There is already an Absence Plan for this employee for the selected date range. See here: {0}" +msgstr "" + #: time_capture/scripts/employee.py:87 msgid "This Employee has no supervisor (Reports To), but has Create User Permission checkbox enabled. Please, make sure that this intended." msgstr "" @@ -673,6 +764,10 @@ msgstr "" msgid "Working time must be completely booked on projects and tasks." msgstr "" +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:113 +msgid "Your Absence Plan {0} has been {1}." +msgstr "" + #: time_capture/scripts/summary_utils.py:109 msgid "{0} (on {1})" msgstr "" @@ -685,3 +780,7 @@ msgstr "" msgid "{0} already exists for this date and person." msgstr "" +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:80 +msgid "{0} raised a new Absence Plan for approval: {1}" +msgstr "" + diff --git a/time_capture/time_capture/doctype/absence_plan/absence_plan.json b/time_capture/time_capture/doctype/absence_plan/absence_plan.json index 98d34cd..94ee719 100644 --- a/time_capture/time_capture/doctype/absence_plan/absence_plan.json +++ b/time_capture/time_capture/doctype/absence_plan/absence_plan.json @@ -7,6 +7,7 @@ "field_order": [ "employee", "employee_name", + "employee_email", "column_break_ihkc", "leave_type", "section_break_ueuf", @@ -30,6 +31,7 @@ "in_list_view": 1, "label": "Employee", "options": "Employee", + "read_only_depends_on": "eval: !doc.__islocal;", "reqd": 1 }, { @@ -122,13 +124,20 @@ "fieldtype": "Date", "label": "To Date", "read_only": 1 + }, + { + "fieldname": "employee_email", + "fieldtype": "Data", + "hidden": 1, + "label": "Employee Email", + "options": "Email" } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2026-02-02 10:25:00.000000", + "modified": "2026-02-02 16:52:02.277175", "modified_by": "Administrator", "module": "Time Capture", "name": "Absence Plan", @@ -248,4 +257,4 @@ "sort_order": "DESC", "states": [], "title_field": "employee_name" -} +} \ No newline at end of file diff --git a/time_capture/time_capture/doctype/absence_plan/absence_plan.py b/time_capture/time_capture/doctype/absence_plan/absence_plan.py index 1fcbba0..26eef6f 100644 --- a/time_capture/time_capture/doctype/absence_plan/absence_plan.py +++ b/time_capture/time_capture/doctype/absence_plan/absence_plan.py @@ -7,6 +7,7 @@ from frappe.utils import get_link_to_form, getdate from frappe.utils.data import add_to_date from hrms.hr.utils import share_doc_with_approver +from hrms.utils import get_employee_email class AbsencePlan(Document): @@ -33,6 +34,9 @@ class AbsencePlan(Document): to_date: DF.Date | None # end: auto-generated types + def before_insert(self): + self.employee_email = get_employee_email(self.employee) + def after_insert(self): self.notify_leave_approver() @@ -61,6 +65,69 @@ def on_submit(self): def before_cancel(self): self.status = "Cancelled" + def notify_leave_approver(self): + """ + Notify leave approver about new Absence Plan + """ + # Preperation + if not self.leave_approver: + return + from_user = self.employee_email + if from_user == self.leave_approver: + return + + lang = _get_user_lang(self.leave_approver) + message = _("{0} raised a new Absence Plan for approval: {1}", lang=lang).format( + self.employee_name or self.employee, self.name + ) + + # Create Notification + n = frappe.new_doc("PWA Notification") + n.from_user, n.to_user = from_user, self.leave_approver + n.message = message + n.reference_document_type, n.reference_document_name = self.doctype, self.name + n.insert(ignore_permissions=True) + + # Send Email + link = get_link_to_form(self.doctype, self.name, _("Absence Plan", lang=lang)) + frappe.sendmail( + recipients=[self.leave_approver], + subject=_("Absence Plan {0} for approval", lang=lang).format(self.name), + message=message + "

" + link, + ) + frappe.msgprint(_("Leave approver {0} has been notified via Email.").format(self.leave_approver)) + + def notify_employee(self): + """ + Notify employee about Absence Plan approval status + """ + # Preperation + to_user = self.employee_email + if not to_user: + return + from_user = self.leave_approver + if from_user == to_user: + return + + lang = _get_user_lang(to_user) + message = _("Your Absence Plan {0} has been {1}.", lang=lang).format(self.name, self.status) + + # Create Notification + n = frappe.new_doc("PWA Notification") + n.from_user, n.to_user = from_user, to_user + n.message = message + n.reference_document_type, n.reference_document_name = self.doctype, self.name + n.insert(ignore_permissions=True) + + # Send Email + link = get_link_to_form(self.doctype, self.name, _("Absence Plan", lang=lang)) + frappe.sendmail( + recipients=[to_user], + subject=_("Absence Plan {0} - {1}", lang=lang).format(self.name, self.status), + message=message + "

" + link, + ) + frappe.msgprint(_("Employee {0} has been notified via Email.").format(to_user)) + def remove_duplicate_dates(self): """Keep only the first occurrence of each date in dates.""" seen = set() @@ -98,18 +165,6 @@ def avoid_overlapping_absence_plans(self): ) return - def notify_leave_approver(self): - message = _("{0} raised a new Absence Plan for approval: {1}").format( - self.employee_name or self.employee, self.name - ) - _create_pwa_notification(self.leave_approver, self.employee, "Leave Approver", message, self.name) - - def notify_employee(self): - message = _("Your Absence Plan {0} has been {1}.").format( - self.name, _("Approved" if self.status == "Approved" else "Rejected") - ) - _create_pwa_notification(self.leave_approver, self.employee, "Employee", message, self.name) - @frappe.whitelist() def bulk_insert_dates(mode, from_date, to_date, weekday=None, reason=None): @@ -161,29 +216,8 @@ def _get_timespan_days(from_date, to_date, reason=None): return dates -def _create_pwa_notification( - leave_approver: str, employee: str, send_to: str, message: str, absence_plan_name: str -): - employee_user = frappe.db.get_value("Employee", employee, "user_id", cache=True) - if send_to == "Leave Approver": - from_user = leave_approver - to_user = employee_user - elif send_to == "Employee": - from_user = employee_user - to_user = leave_approver - else: - frappe.throw(_("Send to has to be either 'Leave Approver' or 'Employee'.")) - - if not employee_user or not leave_approver: - return - - if from_user == to_user: - return - - n = frappe.new_doc("PWA Notification") - n.from_user = from_user - n.to_user = to_user - n.message = message - n.reference_document_type = "Absence Plan" - n.reference_document_name = absence_plan_name - n.insert(ignore_permissions=True) +def _get_user_lang(user): + user_lang = frappe.db.get_value("User", user, "language", cache=True) + if user_lang: + return user_lang + return frappe.local.lang or "en" From 4520c64de47955bf20fc90099379a0f0144c0ce9 Mon Sep 17 00:00:00 2001 From: Patrick Eissler <77415730+PatrickDEissler@users.noreply.github.com> Date: Mon, 2 Feb 2026 18:12:53 +0100 Subject: [PATCH 09/26] feat: allow several plans, without update after submit --- .../doctype/absence_plan/absence_plan.js | 2 +- .../doctype/absence_plan/absence_plan.json | 8 +++-- .../doctype/absence_plan/absence_plan.py | 35 +++---------------- 3 files changed, 11 insertions(+), 34 deletions(-) diff --git a/time_capture/time_capture/doctype/absence_plan/absence_plan.js b/time_capture/time_capture/doctype/absence_plan/absence_plan.js index 490a77b..db5fab9 100644 --- a/time_capture/time_capture/doctype/absence_plan/absence_plan.js +++ b/time_capture/time_capture/doctype/absence_plan/absence_plan.js @@ -83,7 +83,6 @@ function open_bulk_insert_dialog(frm) { frappe.call({ method: "time_capture.time_capture.doctype.absence_plan.absence_plan.bulk_insert_dates", args: { - doc: frm.doc, mode: values.mode, weekday: values.weekday, from_date: values.from_date, @@ -98,6 +97,7 @@ function open_bulk_insert_dialog(frm) { row.reason = item.reason || ""; }); frm.refresh_field("dates"); + frm.save(); } d.hide(); }, diff --git a/time_capture/time_capture/doctype/absence_plan/absence_plan.json b/time_capture/time_capture/doctype/absence_plan/absence_plan.json index 94ee719..abf6a41 100644 --- a/time_capture/time_capture/doctype/absence_plan/absence_plan.json +++ b/time_capture/time_capture/doctype/absence_plan/absence_plan.json @@ -60,13 +60,15 @@ { "fieldname": "bulk_insert_btn", "fieldtype": "Button", - "label": "Bulk Insert" + "label": "Bulk Insert", + "depends_on": "eval: doc.docstatus == 0" }, { "fieldname": "dates", "fieldtype": "Table", "label": "Dates", - "options": "Absence Plan Date" + "options": "Absence Plan Date", + "read_only_depends_on": "" }, { "fieldname": "approval_section", @@ -137,7 +139,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2026-02-02 16:52:02.277175", + "modified": "2026-02-02 17:59:00.281409", "modified_by": "Administrator", "module": "Time Capture", "name": "Absence Plan", diff --git a/time_capture/time_capture/doctype/absence_plan/absence_plan.py b/time_capture/time_capture/doctype/absence_plan/absence_plan.py index 26eef6f..5b44f78 100644 --- a/time_capture/time_capture/doctype/absence_plan/absence_plan.py +++ b/time_capture/time_capture/doctype/absence_plan/absence_plan.py @@ -1,6 +1,8 @@ # Copyright (c) 2025, ALYF GmbH and contributors # For license information, please see license.txt +import json + import frappe from frappe import _ from frappe.model.document import Document @@ -47,16 +49,9 @@ def before_validate(self): self.to_date = max([row.date for row in self.dates]) self.order_dates() - def validate(self): - self.avoid_overlapping_absence_plans() - def on_update(self): share_doc_with_approver(self, self.leave_approver) - def before_update_after_submit(self): - self.order_dates() - self.avoid_overlapping_absence_plans() - def on_submit(self): if self.status in ["Open", "Cancelled"]: frappe.throw(_("Only Absence Plans with status 'Approved' and 'Rejected' can be submitted")) @@ -144,32 +139,12 @@ def remove_duplicate_dates(self): def order_dates(self): self.dates = sorted(self.dates, key=lambda x: x.date) - def avoid_overlapping_absence_plans(self): - overlapping_absence_plans = frappe.db.get_all( - "Absence Plan", - {"docstatus": ["!=", 2], "employee": self.employee, "name": ["!=", self.name]}, - or_filters={ - "from_date": ["between", [self.from_date, self.to_date]], - "to_date": ["between", [self.from_date, self.to_date]], - }, - ) - if overlapping_absence_plans: - frappe.throw( - _( - "There is already an Absence Plan for this employee for the selected date range. See here: {0}" - ).format( - get_link_to_form( - "Absence Plan", overlapping_absence_plans[0].name, _("Absence Plans (List)") - ) - ) - ) - return - @frappe.whitelist() -def bulk_insert_dates(mode, from_date, to_date, weekday=None, reason=None): +def bulk_insert_dates( + mode: str, from_date: str, to_date: str, weekday: str | None = None, reason: str | None = None +): """Return list of {date, reason} to add. Called from Bulk Insert dialog.""" - frappe.has_permission("Absence Plan", "write", throw=True) from_date = getdate(from_date) to_date = getdate(to_date) if to_date < from_date: From c166f8b54f7e6335e5d48d2cf0fc9eebe476f5dd Mon Sep 17 00:00:00 2001 From: Patrick Eissler <77415730+PatrickDEissler@users.noreply.github.com> Date: Mon, 2 Feb 2026 18:20:36 +0100 Subject: [PATCH 10/26] fix: add depends_on statements --- .../doctype/absence_plan/absence_plan.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/time_capture/time_capture/doctype/absence_plan/absence_plan.json b/time_capture/time_capture/doctype/absence_plan/absence_plan.json index abf6a41..ab0e501 100644 --- a/time_capture/time_capture/doctype/absence_plan/absence_plan.json +++ b/time_capture/time_capture/doctype/absence_plan/absence_plan.json @@ -58,17 +58,16 @@ "label": "Dates" }, { + "depends_on": "eval: doc.docstatus == 0", "fieldname": "bulk_insert_btn", "fieldtype": "Button", - "label": "Bulk Insert", - "depends_on": "eval: doc.docstatus == 0" + "label": "Bulk Insert" }, { "fieldname": "dates", "fieldtype": "Table", "label": "Dates", - "options": "Absence Plan Date", - "read_only_depends_on": "" + "options": "Absence Plan Date" }, { "fieldname": "approval_section", @@ -95,7 +94,8 @@ "label": "Status", "no_copy": 1, "options": "Open\nApproved\nRejected\nCancelled", - "permlevel": 1 + "permlevel": 1, + "read_only_depends_on": "eval: doc.docstatus == 1" }, { "fieldname": "amended_from", @@ -139,7 +139,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2026-02-02 17:59:00.281409", + "modified": "2026-02-02 18:18:31.040154", "modified_by": "Administrator", "module": "Time Capture", "name": "Absence Plan", From 667a2a79aeffff340c1d8f375edcca82db8e5156 Mon Sep 17 00:00:00 2001 From: Patrick Eissler <77415730+PatrickDEissler@users.noreply.github.com> Date: Tue, 3 Feb 2026 09:38:23 +0100 Subject: [PATCH 11/26] feat: consider Absence Plan when creating Time Captures --- time_capture/custom_fields.py | 18 ++++++++ time_capture/patches.txt | 2 +- time_capture/scripts/attendance.py | 15 +++---- .../doctype/time_capture/time_capture.py | 45 +++++++++++++++++++ 4 files changed, 70 insertions(+), 10 deletions(-) diff --git a/time_capture/custom_fields.py b/time_capture/custom_fields.py index 66ca71c..d35b4c5 100644 --- a/time_capture/custom_fields.py +++ b/time_capture/custom_fields.py @@ -24,6 +24,24 @@ def get_custom_fields(): "precision": "2", "allow_on_submit": 1, }, + { + "fieldname": "custom_absence_plan", + "fieldtype": "Link", + "insert_after": "leave_application", + "label": _("Absence Plan"), + "no_copy": 1, + "options": "Absence Plan", + "read_only": 1, + }, + { + "fieldname": "custom_leave_type_absence_plan", + "fieldtype": "Link", + "insert_after": "custom_absence_plan", + "label": _("Leave Type (Absence Plan)"), + "no_copy": 1, + "options": "Leave Type", + "read_only": 1, + }, { "label": _("Time Capture"), "fieldname": "custom_time_capture", diff --git a/time_capture/patches.txt b/time_capture/patches.txt index fd3ee43..b633e73 100644 --- a/time_capture/patches.txt +++ b/time_capture/patches.txt @@ -3,7 +3,7 @@ # Read docs to understand patches: https://frappeframework.com/docs/v14/user/en/database-migrations [post_model_sync] -execute:from time_capture.install import _make_custom_fields;_make_custom_fields() # 2026-01-22 #2 +execute:from time_capture.install import _make_custom_fields;_make_custom_fields() # 2026-02-03 #1 execute:from time_capture.install import _make_property_setters;_make_property_setters() # 2026-01-22 #2 time_capture.patches.move_expected_working_time_to_child_table time_capture.patches.create_freelancer_role #7 diff --git a/time_capture/scripts/attendance.py b/time_capture/scripts/attendance.py index ad503d1..4528ee6 100644 --- a/time_capture/scripts/attendance.py +++ b/time_capture/scripts/attendance.py @@ -9,7 +9,7 @@ def on_change(doc, event): set_attendance_metrics(doc) if ( - doc.leave_type + (doc.leave_type or doc.custom_leave_type_absence_plan) and frappe.utils.getdate(doc.attendance_date) <= frappe.utils.getdate() and doc.docstatus != 2 ): @@ -52,9 +52,8 @@ def get_attendance_metrics(doc, get_working_hours=False): Returns: tuple: (status, actual_working_hours, expected_working_hours, flexitime) """ - if doc.leave_type and doc.leave_type == frappe.db.get_single_value( - "Time Capture Settings", "sick_leave_type" - ): + leave_type = doc.leave_type or doc.custom_leave_type_absence_plan + if leave_type and leave_type == frappe.db.get_single_value("Time Capture Settings", "sick_leave_type"): # Special Case: Sick Leaves are not considered for working hours or expected working hours. return "On Leave", 0.0, 0.0, 0.0 @@ -70,14 +69,12 @@ def get_attendance_metrics(doc, get_working_hours=False): working_hours = doc.working_hours or 0.0 # 2: Get Status - status = _get_attendance_status( - is_half_day, expected_working_hours_full_day, working_hours, doc.leave_type - ) + status = _get_attendance_status(is_half_day, expected_working_hours_full_day, working_hours, leave_type) # 3: Get Expected Working Hours - if doc.leave_type and doc.leave_application: + if leave_type: expected_working_hours = _get_expected_working_hours_for_leave_days( - expected_working_hours_full_day, doc.leave_type, is_half_day + expected_working_hours_full_day, leave_type, is_half_day ) else: expected_working_hours = expected_working_hours_full_day diff --git a/time_capture/time_capture/doctype/time_capture/time_capture.py b/time_capture/time_capture/doctype/time_capture/time_capture.py index d6fac80..3150d5b 100644 --- a/time_capture/time_capture/doctype/time_capture/time_capture.py +++ b/time_capture/time_capture/doctype/time_capture/time_capture.py @@ -308,6 +308,9 @@ def _create_time_capture(employee, date): if is_holiday(employee.holiday_list, date): return + if _create_attendance_for_absence_plan(employee.name, date): + return + time_capture = frappe.new_doc("Time Capture") time_capture.update( { @@ -320,6 +323,48 @@ def _create_time_capture(employee, date): time_capture.insert() +def _create_attendance_for_absence_plan(employee_name, date): + """ + Check if the employee has an approved Absence Plan that includes the given date. + Returns the leave_type from the matching plan, or None if none found. + Caller is responsible for attendance creation etc. + """ + # Get Absence Plan and Leave Type from Absence Plan + AP = frappe.qb.DocType("Absence Plan") + APD = frappe.qb.DocType("Absence Plan Date") + absence_plans = ( + frappe.qb.from_(AP) + .inner_join(APD) + .on(AP.name == APD.parent) + .select(AP.name, AP.leave_type) + .where( + (AP.employee == employee_name) + & (APD.date == date) + & (AP.status == "Approved") + & (AP.docstatus == 1) + ) + ).run(as_dict=True) + + if not absence_plans: + return False + + # Create Attendance if there is a leave type (= absence is planned) + absence_plan = absence_plans[0] + attendance = frappe.get_doc( + { + "doctype": "Attendance", + "employee": employee_name, + "attendance_date": date, + "custom_absence_plan": absence_plan.name, + "custom_leave_type_absence_plan": absence_plan.leave_type, + } + ) + attendance.insert() + attendance.submit() + + return True + + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def task_query(doctype, txt, searchfield, start, page_len, filters): From 5baf36733401865b6b23320bd52e54a436b54492 Mon Sep 17 00:00:00 2001 From: Patrick Eissler <77415730+PatrickDEissler@users.noreply.github.com> Date: Tue, 3 Feb 2026 10:18:42 +0100 Subject: [PATCH 12/26] feat: handle cancellation logic --- .../doctype/absence_plan/absence_plan.js | 21 ++++++++ .../doctype/absence_plan/absence_plan.py | 48 +++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/time_capture/time_capture/doctype/absence_plan/absence_plan.js b/time_capture/time_capture/doctype/absence_plan/absence_plan.js index db5fab9..daf46f7 100644 --- a/time_capture/time_capture/doctype/absence_plan/absence_plan.js +++ b/time_capture/time_capture/doctype/absence_plan/absence_plan.js @@ -14,6 +14,27 @@ frappe.ui.form.on("Absence Plan", { }); }, + refresh: function (frm) { + if ( + frm.doc.docstatus === 1 && + frm.doc.from_date && + frm.doc.from_date <= frappe.datetime.get_today() + ) { + frm.add_custom_button(__("Delete Future Dates"), function () { + frappe.call({ + method: "time_capture.time_capture.doctype.absence_plan.absence_plan.delete_future_dates", + args: { name: frm.doc.name }, + callback: function (r) { + if (r && r.message) { + frappe.msgprint(r.message); + } + frm.reload_doc(); + }, + }); + }); + } + }, + employee: function (frm) { frm.trigger("set_leave_approver"); }, diff --git a/time_capture/time_capture/doctype/absence_plan/absence_plan.py b/time_capture/time_capture/doctype/absence_plan/absence_plan.py index 5b44f78..17358ad 100644 --- a/time_capture/time_capture/doctype/absence_plan/absence_plan.py +++ b/time_capture/time_capture/doctype/absence_plan/absence_plan.py @@ -58,6 +58,12 @@ def on_submit(self): self.notify_employee() def before_cancel(self): + if getdate(self.from_date) <= getdate(): + frappe.throw( + _( + "Absence Plan with dates in the past (or today) cannot be cancelled. Instead you can use the 'Delete Future Dates' button on top of the page. This will delete the future dates from the Absence Plan, but will not effect the past dates." + ) + ) self.status = "Cancelled" def notify_leave_approver(self): @@ -196,3 +202,45 @@ def _get_user_lang(user): if user_lang: return user_lang return frappe.local.lang or "en" + + +@frappe.whitelist() +def delete_future_dates(name: str) -> str: + """ + Delete all dates in the future from the Absence Plan child table and recalculate to_date. + Requires Cancel permission. Only for submitted docs with from_date not in the future. + """ + doc = frappe.get_doc("Absence Plan", name) + # Validations + if doc.docstatus != 1: + frappe.throw(_("Only submitted Absence Plans can use this action.")) + if getdate(doc.from_date) > getdate(): + frappe.throw( + _("From Date is in the future. This action is only for plans that have already started.") + ) + if not doc.has_permission("cancel") and doc.leave_approver != frappe.session.user: + frappe.throw( + _("Only the Leave Approver and users with the 'Cancel' permission can delete future dates."), + title=_("Permission Denied"), + ) + + # Delete future dates + today = getdate() + future_rows = [r for r in (doc.dates or []) if getdate(r.date) > today] + deleted_count = 0 + for row in future_rows: + frappe.delete_doc("Absence Plan Date", row.name) + deleted_count += 1 + + if deleted_count: + remaining_dates = [getdate(r.date) for r in doc.dates if getdate(r.date) <= today] + if remaining_dates: + frappe.db.set_value( + "Absence Plan", + doc.name, + {"from_date": min(remaining_dates), "to_date": max(remaining_dates)}, + ) + else: + frappe.throw(_("All Dates are in the future. Please cancel the Absence Plan instead.")) + + return _("Deleted {0} future date(s).").format(deleted_count) From fa0b9a220c466b5374d2876c80e89b3e69c2c0ce Mon Sep 17 00:00:00 2001 From: Patrick Eissler <77415730+PatrickDEissler@users.noreply.github.com> Date: Tue, 3 Feb 2026 10:35:25 +0100 Subject: [PATCH 13/26] feat: avoid duplicate dates --- .../doctype/absence_plan/absence_plan.py | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/time_capture/time_capture/doctype/absence_plan/absence_plan.py b/time_capture/time_capture/doctype/absence_plan/absence_plan.py index 17358ad..837a900 100644 --- a/time_capture/time_capture/doctype/absence_plan/absence_plan.py +++ b/time_capture/time_capture/doctype/absence_plan/absence_plan.py @@ -49,6 +49,9 @@ def before_validate(self): self.to_date = max([row.date for row in self.dates]) self.order_dates() + def validate(self): + self.avoid_duplicates_with_other_absence_plans() + def on_update(self): share_doc_with_approver(self, self.leave_approver) @@ -145,6 +148,45 @@ def remove_duplicate_dates(self): def order_dates(self): self.dates = sorted(self.dates, key=lambda x: x.date) + def avoid_duplicates_with_other_absence_plans(self): + """ + Check if the Absence Plan dates overlap with other Absence Plans for the same employee. + Dates must be unique per employee; throw if any duplicate is found. + """ + other_plans = frappe.get_all( + "Absence Plan", + filters={"employee": self.employee, "name": ["!=", self.name or ""]}, + pluck="name", + ) + if not other_plans: + return + + our_dates = {getdate(row.date) for row in self.dates or [] if row.date} + overlapping_dates = frappe.db.sql( + """ + SELECT apd.date, apd.parent + FROM `tabAbsence Plan Date` apd + WHERE apd.parent IN %(plans)s + AND apd.date IN %(our_dates)s + """, + {"plans": other_plans, "our_dates": our_dates}, + as_dict=True, + ) + + if overlapping_dates: + overlapping_dates_str = "
".join( + [ + f"{frappe.utils.format_date(d.date)} in Absence Plan {frappe.utils.get_link_to_form('Absence Plan', d.parent)}" + for d in overlapping_dates + ] + ) + frappe.throw( + _( + "There are overlapping dates with following Absence Plans:
{0}", + ).format(overlapping_dates_str), + title=_("Overlapping Dates are not allowed"), + ) + @frappe.whitelist() def bulk_insert_dates( From 84eb9f7f0918b2d8fec6cf2ac31a533a28fc05d7 Mon Sep 17 00:00:00 2001 From: Patrick Eissler <77415730+PatrickDEissler@users.noreply.github.com> Date: Tue, 3 Feb 2026 10:45:10 +0100 Subject: [PATCH 14/26] fix: do not consider cancelled absence plans for overlapping validation --- time_capture/time_capture/doctype/absence_plan/absence_plan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/time_capture/time_capture/doctype/absence_plan/absence_plan.py b/time_capture/time_capture/doctype/absence_plan/absence_plan.py index 837a900..a506754 100644 --- a/time_capture/time_capture/doctype/absence_plan/absence_plan.py +++ b/time_capture/time_capture/doctype/absence_plan/absence_plan.py @@ -155,7 +155,7 @@ def avoid_duplicates_with_other_absence_plans(self): """ other_plans = frappe.get_all( "Absence Plan", - filters={"employee": self.employee, "name": ["!=", self.name or ""]}, + filters={"employee": self.employee, "name": ["!=", self.name or ""], "docstatus": ["!=", 2]}, pluck="name", ) if not other_plans: From 5928d4ec484479ab97f8c573bff1320729f2d114 Mon Sep 17 00:00:00 2001 From: Patrick Eissler <77415730+PatrickDEissler@users.noreply.github.com> Date: Tue, 3 Feb 2026 10:45:29 +0100 Subject: [PATCH 15/26] chore: remove unused import --- time_capture/time_capture/doctype/absence_plan/absence_plan.py | 1 - 1 file changed, 1 deletion(-) diff --git a/time_capture/time_capture/doctype/absence_plan/absence_plan.py b/time_capture/time_capture/doctype/absence_plan/absence_plan.py index a506754..7c8512d 100644 --- a/time_capture/time_capture/doctype/absence_plan/absence_plan.py +++ b/time_capture/time_capture/doctype/absence_plan/absence_plan.py @@ -1,7 +1,6 @@ # Copyright (c) 2025, ALYF GmbH and contributors # For license information, please see license.txt -import json import frappe from frappe import _ From cf15254009cdc367c7d26b4824dad17727e402ea Mon Sep 17 00:00:00 2001 From: Patrick Eissler <77415730+PatrickDEissler@users.noreply.github.com> Date: Tue, 3 Feb 2026 10:46:08 +0100 Subject: [PATCH 16/26] docs: fix typo --- .../time_capture/doctype/absence_plan/absence_plan.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/time_capture/time_capture/doctype/absence_plan/absence_plan.py b/time_capture/time_capture/doctype/absence_plan/absence_plan.py index 7c8512d..2a737d6 100644 --- a/time_capture/time_capture/doctype/absence_plan/absence_plan.py +++ b/time_capture/time_capture/doctype/absence_plan/absence_plan.py @@ -72,7 +72,7 @@ def notify_leave_approver(self): """ Notify leave approver about new Absence Plan """ - # Preperation + # Preparation if not self.leave_approver: return from_user = self.employee_email @@ -104,7 +104,7 @@ def notify_employee(self): """ Notify employee about Absence Plan approval status """ - # Preperation + # Preparation to_user = self.employee_email if not to_user: return From 78baa4eca936b2700c06966a78aee5104a7f693b Mon Sep 17 00:00:00 2001 From: Patrick Eissler <77415730+PatrickDEissler@users.noreply.github.com> Date: Tue, 3 Feb 2026 10:46:45 +0100 Subject: [PATCH 17/26] fix: typo in error message --- time_capture/time_capture/doctype/absence_plan/absence_plan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/time_capture/time_capture/doctype/absence_plan/absence_plan.py b/time_capture/time_capture/doctype/absence_plan/absence_plan.py index 2a737d6..2917b41 100644 --- a/time_capture/time_capture/doctype/absence_plan/absence_plan.py +++ b/time_capture/time_capture/doctype/absence_plan/absence_plan.py @@ -63,7 +63,7 @@ def before_cancel(self): if getdate(self.from_date) <= getdate(): frappe.throw( _( - "Absence Plan with dates in the past (or today) cannot be cancelled. Instead you can use the 'Delete Future Dates' button on top of the page. This will delete the future dates from the Absence Plan, but will not effect the past dates." + "Absence Plan with dates in the past (or today) cannot be cancelled. Instead you can use the 'Delete Future Dates' button on top of the page. This will delete the future dates from the Absence Plan, but will not affect the past dates." ) ) self.status = "Cancelled" From 7f954fd9c717350219a58827c502432847558482 Mon Sep 17 00:00:00 2001 From: Patrick Eissler <77415730+PatrickDEissler@users.noreply.github.com> Date: Tue, 3 Feb 2026 11:02:40 +0100 Subject: [PATCH 18/26] fix: avoid past dates --- .../doctype/absence_plan/absence_plan.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/time_capture/time_capture/doctype/absence_plan/absence_plan.py b/time_capture/time_capture/doctype/absence_plan/absence_plan.py index 2917b41..1fa6e19 100644 --- a/time_capture/time_capture/doctype/absence_plan/absence_plan.py +++ b/time_capture/time_capture/doctype/absence_plan/absence_plan.py @@ -43,6 +43,7 @@ def after_insert(self): def before_validate(self): self.remove_duplicate_dates() + self.remove_past_dates() if self.dates: self.from_date = min([row.date for row in self.dates]) self.to_date = max([row.date for row in self.dates]) @@ -144,6 +145,24 @@ def remove_duplicate_dates(self): new_rows.append(row) self.dates = new_rows + def remove_past_dates(self): + """Remove dates that are in the past or today.""" + today = getdate() + past_dates = [] + future_dates = [] + for row in self.dates: + if getdate(row.date) <= today: + past_dates.append(row) + else: + future_dates.append(row) + self.dates = future_dates + if past_dates: + frappe.msgprint( + _( + "The following dates are in the past (or today) and have been removed from the Absence Plan:
{0}" + ).format("
".join([frappe.utils.format_date(row.date) for row in past_dates])) + ) + def order_dates(self): self.dates = sorted(self.dates, key=lambda x: x.date) From 7acf11f4414c260e5d98d5492b23229ec031b601 Mon Sep 17 00:00:00 2001 From: Patrick Eissler <77415730+PatrickDEissler@users.noreply.github.com> Date: Tue, 3 Feb 2026 11:29:02 +0100 Subject: [PATCH 19/26] fix: add user-facing strings --- .../doctype/absence_plan/absence_plan.js | 51 +++++++++++++++++++ .../doctype/absence_plan/absence_plan.py | 6 +++ 2 files changed, 57 insertions(+) diff --git a/time_capture/time_capture/doctype/absence_plan/absence_plan.js b/time_capture/time_capture/doctype/absence_plan/absence_plan.js index daf46f7..b2cc8da 100644 --- a/time_capture/time_capture/doctype/absence_plan/absence_plan.js +++ b/time_capture/time_capture/doctype/absence_plan/absence_plan.js @@ -39,6 +39,14 @@ frappe.ui.form.on("Absence Plan", { frm.trigger("set_leave_approver"); }, + leave_type: function (frm) { + frappe.msgprint( + __( + "Warning: The Leave Ledgers will not be updated for leaves with an Attendance Plan. Therefore, it is not suggested to use Leave Types (such as 'Annual Leave') that have limited Leave Allocation." + ) + ); + }, + bulk_insert_btn: function (frm) { open_bulk_insert_dialog(frm); }, @@ -61,6 +69,43 @@ frappe.ui.form.on("Absence Plan", { }); function open_bulk_insert_dialog(frm) { + const weekly_off_desc = __( + "Weekly Off: adds all dates in the range that fall on the selected weekday. The Holiday List already defines non-working days; adding e.g. every Sunday as weekly off is often redundant." + ); + const weekly_off_example_desc = __( + "Typical use case: Every Friday is an unofficial weekly off day." + ); + const weekly_off_exempt_desc = __( + "Official weekly offs (e.g. 4 day work week set in employee contract) should be handled via Holiday List." + ); + const timespan_desc = __("Timespan: adds all dates in the range with an optional reason."); + const timespan_example_desc = __( + "Typical use case: Employee is on Education Leave (University, School, etc.)." + ); + const holiday_list_desc = __( + "Holidays from a Holiday List have priority over leaves with an Attendance Plan." + ); + const warning_desc = __( + "Warning: The Leave Ledgers will not be updated for leaves with an Attendance Plan. Therefore, it is not suggested to use Leave Types (such as 'Annual Leave') that have limited Leave Allocation." + ); + + const descriptionHtml = + "

" + + weekly_off_desc + + "
" + + weekly_off_example_desc + + "
" + + weekly_off_exempt_desc + + "

" + + timespan_desc + + "
" + + timespan_example_desc + + "

" + + holiday_list_desc + + "
" + + warning_desc + + "

"; + const d = new frappe.ui.Dialog({ title: __("Bulk Insert"), fields: [ @@ -72,6 +117,12 @@ function open_bulk_insert_dialog(frm) { default: "Weekly Off", reqd: 1, }, + { + fieldtype: "HTML", + fieldname: "description", + label: __("Description"), + options: descriptionHtml, + }, { fieldtype: "Date", fieldname: "from_date", diff --git a/time_capture/time_capture/doctype/absence_plan/absence_plan.py b/time_capture/time_capture/doctype/absence_plan/absence_plan.py index 1fa6e19..52faa9f 100644 --- a/time_capture/time_capture/doctype/absence_plan/absence_plan.py +++ b/time_capture/time_capture/doctype/absence_plan/absence_plan.py @@ -59,6 +59,12 @@ def on_submit(self): if self.status in ["Open", "Cancelled"]: frappe.throw(_("Only Absence Plans with status 'Approved' and 'Rejected' can be submitted")) self.notify_employee() + if self.status == "Approved": + frappe.msgprint( + _( + "Absence Plan has been approved. The Employee will not have to capture any time for these dates." + ) + ) def before_cancel(self): if getdate(self.from_date) <= getdate(): From 25c978f11bd9bfb4c88185089180f135d05ea877 Mon Sep 17 00:00:00 2001 From: Patrick Eissler <77415730+PatrickDEissler@users.noreply.github.com> Date: Tue, 3 Feb 2026 11:50:33 +0100 Subject: [PATCH 20/26] feat: add translations --- time_capture/locale/de.po | 140 +++++++++++++----- time_capture/locale/main.pot | 138 +++++++++++++---- .../doctype/absence_plan/absence_plan.py | 4 +- 3 files changed, 214 insertions(+), 68 deletions(-) diff --git a/time_capture/locale/de.po b/time_capture/locale/de.po index 28f2b4f..cd57504 100644 --- a/time_capture/locale/de.po +++ b/time_capture/locale/de.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: Time Capture VERSION\n" "Report-Msgid-Bugs-To: patrick@alyf.de\n" -"POT-Creation-Date: 2026-02-02 17:16+0053\n" +"POT-Creation-Date: 2026-02-03 11:32+0053\n" "PO-Revision-Date: 2025-02-02 17:08+0053\n" "Last-Translator: patrick@alyf.de\n" "Language-Team: patrick@alyf.de\n" @@ -32,9 +32,10 @@ msgid "Freelancer Time Capture" msgstr "Freelancer Zeiterfassung" #. Name of a DocType +#: time_capture/custom_fields.py:31 #: time_capture/time_capture/doctype/absence_plan/absence_plan.json -#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:92 -#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:123 +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:102 +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:133 msgid "Absence Plan" msgstr "Abwesenheitsplan" @@ -43,23 +44,31 @@ msgstr "Abwesenheitsplan" msgid "Absence Plan Date" msgstr "Abwesenheitsplan-Datum" -#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:126 +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:64 +msgid "Absence Plan has been approved. The Employee will not have to capture any time for these dates." +msgstr "Der Abwesenheitsplan wurde genehmigt. Der Mitarbeiter muss für diese Daten keine Zeiterfassung vornehmen." + +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:72 +msgid "Absence Plan with dates in the past (or today) cannot be cancelled. Instead you can use the 'Delete Future Dates' button on top of the page. This will delete the future dates from the Absence Plan, but will not affect the past dates." +msgstr "Ein Abwesenheitsplan mit Daten in der Vergangenheit (oder heute) kann nicht storniert werden. Stattdessen können Sie den Knopf 'Zukünftige Daten löschen' oben auf der Seite verwenden. Dadurch werden die zukünftigen Daten aus dem Abwesenheitsplan gelöscht, die vergangenen Daten bleiben unberührt." + +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:136 msgid "Absence Plan {0} - {1}" msgstr "Abwesenheitsplan {0} - {1}" -#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:95 +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:105 msgid "Absence Plan {0} for approval" msgstr "Abwesenheitsplan {0} zum Freigeben" -#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:162 -msgid "Absence Plans (List)" -msgstr "Abwesenheitspläne (Liste)" - #. Label of a Duration field in DocType 'Mandatory Break' #: time_capture/time_capture/doctype/mandatory_break/mandatory_break.json msgid "Additional Break" msgstr "Zusätzliche Pause" +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:310 +msgid "All Dates are in the future. Please cancel the Absence Plan instead." +msgstr "Alle Daten liegen in der Zukunft. Bitte stornieren Sie stattdessen den Abwesenheitsplan." + #. Label of a Section Break field in DocType 'Absence Plan' #: time_capture/time_capture/doctype/absence_plan/absence_plan.json msgid "Approval" @@ -93,12 +102,12 @@ msgid "Break" msgstr "Pause" #. Label of a Button field in DocType 'Absence Plan' -#: time_capture/time_capture/doctype/absence_plan/absence_plan.js:44 +#: time_capture/time_capture/doctype/absence_plan/absence_plan.js:110 #: time_capture/time_capture/doctype/absence_plan/absence_plan.json msgid "Bulk Insert" msgstr "Massen einfügen" -#: time_capture/custom_fields.py:60 time_capture/public/js/employee.js:29 +#: time_capture/custom_fields.py:78 time_capture/public/js/employee.js:29 msgid "Change/Add Expected Hours" msgstr "Erwartete Stunden ändern/hinzufügen" @@ -143,7 +152,7 @@ msgstr "Freelancer-Benutzer wird erstellt" msgid "Current Balance" msgstr "Aktuelles Saldo" -#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:178 +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:225 msgid "Dates cannot be in the past." msgstr "Daten dürfen nicht in der Vergangenheit liegen." @@ -161,6 +170,14 @@ msgstr "Default-Aktivitätstyp" msgid "Default Freelancer Activity Type" msgstr "Standard-Freelancer-Aktivitätstyp" +#: time_capture/time_capture/doctype/absence_plan/absence_plan.js:23 +msgid "Delete Future Dates" +msgstr "Zukünftige Daten löschen" + +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:312 +msgid "Deleted {0} future date(s)." +msgstr "{0} zukünftige Datum/Daten gelöscht." + #. Label of a quick_list in the Freelancer Workspace #: time_capture/time_capture/workspace/freelancer/freelancer.json msgid "Drafts" @@ -189,7 +206,7 @@ msgstr "Mitarbeiterstatus" msgid "Employee {0} has Attendances outside the Holiday List period." msgstr "Mitarbeiter {0} hat Anwesenheiten außerhalb der Periode der Feiertagsliste." -#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:129 +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:139 msgid "Employee {0} has been notified via Email." msgstr "Mitarbeiter {0} wurde per E-Mail benachrichtigt." @@ -203,7 +220,7 @@ msgstr "Erinnerungen aktivieren" msgid "Enforce Mandatory Breaks" msgstr "Verpflichtende Pausen erzwingen" -#: time_capture/scripts/attendance.py:195 +#: time_capture/scripts/attendance.py:192 msgid "Error creating Attendance" msgstr "Fehler beim Erstellen der Anwesenheit" @@ -236,13 +253,13 @@ msgstr "Erwartete tägliche Arbeitsstunden" #: time_capture/scripts/employee.py:49 msgid "Expected Daily Working Hours cannot be greater than 12h." -msgstr "Die Erwartete tägliche Arbeitszeits darf nicht größer als 12h sein." +msgstr "Die erwartete tägliche Arbeitszeit darf nicht größer als 12h sein." #: time_capture/scripts/employee.py:43 msgid "Expected Daily Working Hours cannot be less than 0h." msgstr "Die erwartete tägliche Arbeitszeit darf nicht geringer als 0h sein." -#: time_capture/custom_fields.py:11 time_capture/custom_fields.py:52 +#: time_capture/custom_fields.py:11 time_capture/custom_fields.py:70 #: time_capture/public/js/employee.js:35 #: time_capture/time_capture/report/working_time/working_time.py:30 msgid "Expected Working Hours" @@ -291,7 +308,7 @@ msgid "Freelancer Name" msgstr "Freelancer-Name" #. Name of a DocType -#: time_capture/custom_fields.py:101 +#: time_capture/custom_fields.py:119 #: time_capture/time_capture/doctype/freelancer_time_capture/freelancer_time_capture.json msgid "Freelancer Time Capture" msgstr "Freelancer Zeiterfassung" @@ -300,10 +317,14 @@ msgstr "Freelancer Zeiterfassung" msgid "Freelancer Time Capture can only be created for users with the 'Freelancer' role. Change the user in the field 'User'." msgstr "Freelancer Zeiterfassung kann nur für Benutzer mit der 'Freelancer'-Rolle erstellt werden. Ändern Sie den Benutzer im Feld 'Benutzer'." -#: time_capture/custom_fields.py:109 +#: time_capture/custom_fields.py:127 msgid "Freelancer User" msgstr "Freelancer-Benutzer" +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:285 +msgid "From Date is in the future. This means you can just cancel the whole Absence Plan." +msgstr "Das Von-Datum liegt in der Zukunft. Daher kann der komplette Abwesenheitsplan storniert werden." + #: time_capture/time_capture/report/leave_and_working_time_summaries/leave_and_working_time_summaries.py:89 msgid "Future Balance" msgstr "Zukünftiges Saldo" @@ -330,6 +351,10 @@ msgstr "Allgemeiner Vorschlag: Verwenden Sie einen Aktivitätstyp mit msgid "Go to list view" msgstr "Zur Listenansicht" +#: time_capture/time_capture/doctype/absence_plan/absence_plan.js:85 +msgid "Holidays from a Holiday List have priority over leaves with an Attendance Plan." +msgstr "Feiertage aus einer Feiertagsliste haben Vorrang vor Abwesenheiten mit einem Abwesenheitsplan." + #. Description of the 'Enable Reminders' (Check) field in DocType 'Time Capture #. Settings' #: time_capture/time_capture/doctype/time_capture_settings/time_capture_settings.json @@ -351,7 +376,7 @@ msgstr "Angegebene Pause" msgid "Indicated Break is not in the proper format." msgstr "Angegebene Pause ist nicht im korrekten Format." -#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:187 +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:234 msgid "Invalid mode" msgstr "Ungültiger Modus" @@ -387,7 +412,7 @@ msgstr "Letzte Benachrichtigung (gesendet am)" msgid "Leave Approver" msgstr "Urlaubsgenehmiger" -#: time_capture/custom_fields.py:65 +#: time_capture/custom_fields.py:83 msgid "Leave Policy" msgstr "Urlaubsrichtlinie" @@ -405,12 +430,16 @@ msgstr "Urlaubsübersicht" msgid "Leave Type" msgstr "Urlaubstyp" +#: time_capture/custom_fields.py:40 +msgid "Leave Type (Absence Plan)" +msgstr "Urlaubstyp (Abwesenheitsplan)" + #. Name of a report #: time_capture/time_capture/report/leave_and_working_time_summaries/leave_and_working_time_summaries.json msgid "Leave and Working Time Summaries" msgstr "Urlaubs- und Arbeitszeitübersichten" -#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:98 +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:108 msgid "Leave approver {0} has been notified via Email." msgstr "Urlaubsgenehmiger {0} wurde per E-Mail benachrichtigt." @@ -450,11 +479,11 @@ msgstr "Mai" msgid "Minimum Draft Age (Days)" msgstr "Mindestalter für Entwürfe (Tage)" -#: time_capture/time_capture/doctype/absence_plan/absence_plan.js:49 +#: time_capture/time_capture/doctype/absence_plan/absence_plan.js:115 msgid "Mode" msgstr "Modus" -#: time_capture/custom_fields.py:41 +#: time_capture/custom_fields.py:59 msgid "No Supervisor Required" msgstr "Kein Vorgesetzter Benötigt" @@ -514,7 +543,11 @@ msgstr "November" msgid "October" msgstr "Oktober" -#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:62 +#: time_capture/time_capture/doctype/absence_plan/absence_plan.js:78 +msgid "Official weekly offs (e.g. 4 day work week set in employee contract) should be handled via Holiday List." +msgstr "Offizielle wöchentliche freie Tage (z. B. 4-Tage-Woche im Arbeitsvertrag) sollten über die Feiertagsliste abgebildet werden." + +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:60 msgid "Only Absence Plans with status 'Approved' and 'Rejected' can be submitted" msgstr "Nur Abwesenheitspläne mit Status 'Genehmigt' und 'Abgelehnt' können gebucht werden." @@ -528,6 +561,14 @@ msgstr "Nur System Manager dürfen Erwartete Arbeitsstunden anpassen." msgid "Only send reminders for draft Time Captures that are older than this number of days. Set to 0 to include all drafts regardless of date." msgstr "Nur Erinnerungen für Entwürfe senden, die älter sind als diese Anzahl von Tagen. Setzen Sie auf 0, um alle Entwürfe unabhängig vom Datum zu senden." +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:282 +msgid "Only submitted Absence Plans can use this action." +msgstr "Nur gebuchte Abwesenheitspläne können diese Aktion ausführen." + +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:289 +msgid "Only the Leave Approver and users with the 'Cancel' permission can delete future dates." +msgstr "Nur der Urlaubsgenehmiger und Benutzer mit der Berechtigung „Stornieren“ können zukünftige Daten löschen." + #: time_capture/time_capture/report/leave_and_working_time_summaries/leave_and_working_time_summaries.py:96 msgid "Open Time Captures" msgstr "Offene Zeiterfassungen" @@ -540,6 +581,10 @@ msgstr "Andere Abwesenheitstage" msgid "Overdue Time Captures" msgstr "Überfällige Zeiterfassungen" +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:211 +msgid "Overlapping Dates are not allowed" +msgstr "Überlappende Daten sind nicht erlaubt" + #: time_capture/time_capture/report/working_time/working_time.py:50 msgid "Paid Holidays" msgstr "Bezahlte Feiertage" @@ -553,6 +598,10 @@ msgstr "Urlaubstyp Bezahlt" msgid "Past time captures, that are not yet submitted. These count as absent and reduce the balance." msgstr "Vergangene Zeiterfassungen, die noch nicht eingereicht wurden. Diese zählen als abwesend und reduzieren das Saldo." +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:290 +msgid "Permission Denied" +msgstr "Berechtigung verweigert" + #: time_capture/public/js/utils.js:191 msgid "Planned Overtime Reduction" msgstr "Geplante Überstundenreduzierung" @@ -569,7 +618,7 @@ msgstr "Bitte überprüfen und buchen Sie die ausstehenden Zeiterfassungen." msgid "Please save the Employee first." msgstr "Bitte speichern Sie den Mitarbeiter zuerst." -#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:182 +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:229 msgid "Please select at least one weekday" msgstr "Bitte wählen Sie mindestens einen Wochentag." @@ -610,7 +659,7 @@ msgstr "Speichern & Anwesenheiten aktualisieren" msgid "September" msgstr "September" -#: time_capture/custom_fields.py:44 +#: time_capture/custom_fields.py:62 msgid "Should be checked if the employee is a CEO (or has no supervisor for some reason)." msgstr "Sollte aktiviert sein, wenn der Mitarbeiter ein Geschäftsführer ist (oder aus anderem Grund keinen Vorgesetzten hat)." @@ -658,6 +707,10 @@ msgstr "Das Datum {0} liegt nicht innerhalb der Periode der Feiertagsliste des M msgid "The following Time Captures are still in draft status:" msgstr "Die folgenden Zeiterfassungen sind noch im Entwurfsstatus:" +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:167 +msgid "The following dates are in the past (or today) and have been removed from the Absence Plan:
{0}" +msgstr "Die folgenden Daten liegen in der Vergangenheit (oder heute) und wurden aus dem Abwesenheitsplan entfernt:
{0}" + #: time_capture/scripts/employee.py:77 msgid "The supervisor cannot be the same as the employee." msgstr "Der Vorgesetzte kann nicht gleich dem Mitarbeiter sein." @@ -666,9 +719,9 @@ msgstr "Der Vorgesetzte kann nicht gleich dem Mitarbeiter sein." msgid "The supervisor has no User linked in his/her Employee record." msgstr "Der Vorgesetzte hat keinen Nutzer in seinem/ihren Mitarbeiter-Eintrag." -#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:158 -msgid "There is already an Absence Plan for this employee for the selected date range. See here: {0}" -msgstr "Es existiert bereits ein Abwesenheitsplan für diesen Mitarbeiter im gewählten Zeitraum. Siehe hier: {0}" +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:208 +msgid "There are overlapping dates with following Absence Plans:
{0}" +msgstr "Es gibt überlappende Daten mit folgenden Abwesenheitsplänen:
{0}" #: time_capture/scripts/employee.py:87 msgid "This Employee has no supervisor (Reports To), but has Create User Permission checkbox enabled. Please, make sure that this intended." @@ -687,7 +740,7 @@ msgid "This is the projected balance, that includes planned overtime reductions" msgstr "Dies ist das prognostizierte Saldo, das geplante Überstundenreduzierungen enthält" #. Name of a DocType -#: time_capture/custom_fields.py:28 time_capture/custom_fields.py:93 +#: time_capture/custom_fields.py:46 time_capture/custom_fields.py:111 #: time_capture/time_capture/doctype/time_capture/time_capture.json msgid "Time Capture" msgstr "Zeiterfassung" @@ -701,7 +754,7 @@ msgstr "Zeiterfassungs- & Urlaubsübersicht" msgid "Time Capture Log" msgstr "Zeiterfassungsprotokoll" -#: time_capture/scripts/attendance.py:157 +#: time_capture/scripts/attendance.py:154 msgid "Time Capture Not Found" msgstr "Zeiterfassung nicht gefunden" @@ -723,6 +776,18 @@ msgstr "Frist für die Zeiterfassung verpasst. Bitte wenden Sie sich an Ihre Fü msgid "Time to Submit (in days)" msgstr "Frist zur Einreichung (in Tagen)" +#: time_capture/time_capture/doctype/absence_plan/absence_plan.js:81 +msgid "Timespan: adds all dates in the range with an optional reason." +msgstr "Zeitspanne: fügt alle Daten im Bereich mit optionalem Grund hinzu." + +#: time_capture/time_capture/doctype/absence_plan/absence_plan.js:82 +msgid "Typical use case: Employee is on Education Leave (University, School, etc.)." +msgstr "Typischer Anwendungsfall: Mitarbeiter hat Bildungsurlaub (Universität, Schule usw.)." + +#: time_capture/time_capture/doctype/absence_plan/absence_plan.js:75 +msgid "Typical use case: Every Friday is an unofficial weekly off day." +msgstr "Typischer Anwendungsfall: Jeden Freitag ist ein inoffizieller freier Tag." + #: time_capture/public/js/utils.js:120 msgid "Unable to fetch summary data" msgstr "Zusammenfassungsdaten konnten nicht abgerufen werden" @@ -742,6 +807,15 @@ msgstr "Verwendet für Mitarbeiter ohne verknüpften Nutzer (=E-Mail-Adresse)" msgid "User is not a Freelancer" msgstr "Benutzer ist kein Freelancer" +#: time_capture/time_capture/doctype/absence_plan/absence_plan.js:44 +#: time_capture/time_capture/doctype/absence_plan/absence_plan.js:88 +msgid "Warning: The Leave Ledgers will not be updated for leaves with an Attendance Plan. Therefore, it is not suggested to use Leave Types (such as 'Annual Leave') that have limited Leave Allocation." +msgstr "Hinweis: Die Urlaubskonten werden bei Abwesenheiten mit einem Abwesenheitsplan nicht aktualisiert. Daher wird davon abgeraten, Urlaubstypen mit begrenzter Urlaubszuweisung (z. B. 'Erholungsurlaub') zu verwenden." + +#: time_capture/time_capture/doctype/absence_plan/absence_plan.js:72 +msgid "Weekly Off: adds all dates in the range that fall on the selected weekday. The Holiday List already defines non-working days; adding e.g. every Sunday as weekly off is often redundant." +msgstr "Wöchentlicher freier Tag: fügt alle Daten im Bereich hinzu, die auf den gewählten Wochentag fallen. Die Feiertagsliste legt bereits arbeitsfreie Tage fest; z. B. jeden Sonntag als wöchentlichen freien Tag hinzuzufügen ist oft überflüssig." + #. Label of a Duration field in DocType 'Time Capture' #. Name of a report #: time_capture/time_capture/doctype/time_capture/time_capture.json @@ -764,7 +838,7 @@ msgstr "Arbeitszeitübersicht" msgid "Working time must be completely booked on projects and tasks." msgstr "Die Arbeitszeit muss vollständig auf Projekte und Aufgaben gebucht werden." -#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:113 +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:123 msgid "Your Absence Plan {0} has been {1}." msgstr "Ihr Abwesenheitsplan {0} wurde {1}" @@ -780,7 +854,7 @@ msgstr "{0} Attendances erfolgreich aktualisiert." msgid "{0} already exists for this date and person." msgstr "{0} existiert bereits für dieses Datum und diese Person." -#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:80 +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:90 msgid "{0} raised a new Absence Plan for approval: {1}" msgstr "{0} hat einen neuen Abwesenheitsplan zum Freigeben erstellt: {1}" diff --git a/time_capture/locale/main.pot b/time_capture/locale/main.pot index 6002e23..08d8cf3 100644 --- a/time_capture/locale/main.pot +++ b/time_capture/locale/main.pot @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: Time Capture VERSION\n" "Report-Msgid-Bugs-To: patrick@alyf.de\n" -"POT-Creation-Date: 2026-02-02 17:16+0053\n" -"PO-Revision-Date: 2026-02-02 17:16+0053\n" +"POT-Creation-Date: 2026-02-03 11:32+0053\n" +"PO-Revision-Date: 2026-02-03 11:32+0053\n" "Last-Translator: patrick@alyf.de\n" "Language-Team: patrick@alyf.de\n" "MIME-Version: 1.0\n" @@ -32,9 +32,10 @@ msgid "Freelancer Time Capture" msgstr "" #. Name of a DocType +#: time_capture/custom_fields.py:31 #: time_capture/time_capture/doctype/absence_plan/absence_plan.json -#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:92 -#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:123 +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:102 +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:133 msgid "Absence Plan" msgstr "" @@ -43,16 +44,20 @@ msgstr "" msgid "Absence Plan Date" msgstr "" -#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:126 -msgid "Absence Plan {0} - {1}" +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:64 +msgid "Absence Plan has been approved. The Employee will not have to capture any time for these dates." msgstr "" -#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:95 -msgid "Absence Plan {0} for approval" +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:72 +msgid "Absence Plan with dates in the past (or today) cannot be cancelled. Instead you can use the 'Delete Future Dates' button on top of the page. This will delete the future dates from the Absence Plan, but will not affect the past dates." msgstr "" -#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:162 -msgid "Absence Plans (List)" +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:136 +msgid "Absence Plan {0} - {1}" +msgstr "" + +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:105 +msgid "Absence Plan {0} for approval" msgstr "" #. Label of a Duration field in DocType 'Mandatory Break' @@ -60,6 +65,10 @@ msgstr "" msgid "Additional Break" msgstr "" +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:310 +msgid "All Dates are in the future. Please cancel the Absence Plan instead." +msgstr "" + #. Label of a Section Break field in DocType 'Absence Plan' #: time_capture/time_capture/doctype/absence_plan/absence_plan.json msgid "Approval" @@ -93,12 +102,12 @@ msgid "Break" msgstr "" #. Label of a Button field in DocType 'Absence Plan' -#: time_capture/time_capture/doctype/absence_plan/absence_plan.js:44 +#: time_capture/time_capture/doctype/absence_plan/absence_plan.js:110 #: time_capture/time_capture/doctype/absence_plan/absence_plan.json msgid "Bulk Insert" msgstr "" -#: time_capture/custom_fields.py:60 time_capture/public/js/employee.js:29 +#: time_capture/custom_fields.py:78 time_capture/public/js/employee.js:29 msgid "Change/Add Expected Hours" msgstr "" @@ -143,7 +152,7 @@ msgstr "" msgid "Current Balance" msgstr "" -#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:178 +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:225 msgid "Dates cannot be in the past." msgstr "" @@ -161,6 +170,14 @@ msgstr "" msgid "Default Freelancer Activity Type" msgstr "" +#: time_capture/time_capture/doctype/absence_plan/absence_plan.js:23 +msgid "Delete Future Dates" +msgstr "" + +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:312 +msgid "Deleted {0} future date(s)." +msgstr "" + #. Label of a quick_list in the Freelancer Workspace #: time_capture/time_capture/workspace/freelancer/freelancer.json msgid "Drafts" @@ -189,7 +206,7 @@ msgstr "" msgid "Employee {0} has Attendances outside the Holiday List period." msgstr "" -#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:129 +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:139 msgid "Employee {0} has been notified via Email." msgstr "" @@ -203,7 +220,7 @@ msgstr "" msgid "Enforce Mandatory Breaks" msgstr "" -#: time_capture/scripts/attendance.py:195 +#: time_capture/scripts/attendance.py:192 msgid "Error creating Attendance" msgstr "" @@ -242,7 +259,7 @@ msgstr "" msgid "Expected Daily Working Hours cannot be less than 0h." msgstr "" -#: time_capture/custom_fields.py:11 time_capture/custom_fields.py:52 +#: time_capture/custom_fields.py:11 time_capture/custom_fields.py:70 #: time_capture/public/js/employee.js:35 #: time_capture/time_capture/report/working_time/working_time.py:30 msgid "Expected Working Hours" @@ -291,7 +308,7 @@ msgid "Freelancer Name" msgstr "" #. Name of a DocType -#: time_capture/custom_fields.py:101 +#: time_capture/custom_fields.py:119 #: time_capture/time_capture/doctype/freelancer_time_capture/freelancer_time_capture.json msgid "Freelancer Time Capture" msgstr "" @@ -300,10 +317,14 @@ msgstr "" msgid "Freelancer Time Capture can only be created for users with the 'Freelancer' role. Change the user in the field 'User'." msgstr "" -#: time_capture/custom_fields.py:109 +#: time_capture/custom_fields.py:127 msgid "Freelancer User" msgstr "" +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:285 +msgid "From Date is in the future. This means you can just cancel the whole Absence Plan." +msgstr "" + #: time_capture/time_capture/report/leave_and_working_time_summaries/leave_and_working_time_summaries.py:89 msgid "Future Balance" msgstr "" @@ -330,6 +351,10 @@ msgstr "" msgid "Go to list view" msgstr "" +#: time_capture/time_capture/doctype/absence_plan/absence_plan.js:85 +msgid "Holidays from a Holiday List have priority over leaves with an Attendance Plan." +msgstr "" + #. Description of the 'Enable Reminders' (Check) field in DocType 'Time Capture #. Settings' #: time_capture/time_capture/doctype/time_capture_settings/time_capture_settings.json @@ -351,7 +376,7 @@ msgstr "" msgid "Indicated Break is not in the proper format." msgstr "" -#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:187 +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:234 msgid "Invalid mode" msgstr "" @@ -387,7 +412,7 @@ msgstr "" msgid "Leave Approver" msgstr "" -#: time_capture/custom_fields.py:65 +#: time_capture/custom_fields.py:83 msgid "Leave Policy" msgstr "" @@ -405,12 +430,16 @@ msgstr "" msgid "Leave Type" msgstr "" +#: time_capture/custom_fields.py:40 +msgid "Leave Type (Absence Plan)" +msgstr "" + #. Name of a report #: time_capture/time_capture/report/leave_and_working_time_summaries/leave_and_working_time_summaries.json msgid "Leave and Working Time Summaries" msgstr "" -#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:98 +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:108 msgid "Leave approver {0} has been notified via Email." msgstr "" @@ -450,11 +479,11 @@ msgstr "" msgid "Minimum Draft Age (Days)" msgstr "" -#: time_capture/time_capture/doctype/absence_plan/absence_plan.js:49 +#: time_capture/time_capture/doctype/absence_plan/absence_plan.js:115 msgid "Mode" msgstr "" -#: time_capture/custom_fields.py:41 +#: time_capture/custom_fields.py:59 msgid "No Supervisor Required" msgstr "" @@ -514,7 +543,11 @@ msgstr "" msgid "October" msgstr "" -#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:62 +#: time_capture/time_capture/doctype/absence_plan/absence_plan.js:78 +msgid "Official weekly offs (e.g. 4 day work week set in employee contract) should be handled via Holiday List." +msgstr "" + +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:60 msgid "Only Absence Plans with status 'Approved' and 'Rejected' can be submitted" msgstr "" @@ -528,6 +561,14 @@ msgstr "" msgid "Only send reminders for draft Time Captures that are older than this number of days. Set to 0 to include all drafts regardless of date." msgstr "" +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:282 +msgid "Only submitted Absence Plans can use this action." +msgstr "" + +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:289 +msgid "Only the Leave Approver and users with the 'Cancel' permission can delete future dates." +msgstr "" + #: time_capture/time_capture/report/leave_and_working_time_summaries/leave_and_working_time_summaries.py:96 msgid "Open Time Captures" msgstr "" @@ -540,6 +581,10 @@ msgstr "" msgid "Overdue Time Captures" msgstr "" +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:211 +msgid "Overlapping Dates are not allowed" +msgstr "" + #: time_capture/time_capture/report/working_time/working_time.py:50 msgid "Paid Holidays" msgstr "" @@ -553,6 +598,10 @@ msgstr "" msgid "Past time captures, that are not yet submitted. These count as absent and reduce the balance." msgstr "" +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:290 +msgid "Permission Denied" +msgstr "" + #: time_capture/public/js/utils.js:191 msgid "Planned Overtime Reduction" msgstr "" @@ -569,7 +618,7 @@ msgstr "" msgid "Please save the Employee first." msgstr "" -#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:182 +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:229 msgid "Please select at least one weekday" msgstr "" @@ -610,7 +659,7 @@ msgstr "" msgid "September" msgstr "" -#: time_capture/custom_fields.py:44 +#: time_capture/custom_fields.py:62 msgid "Should be checked if the employee is a CEO (or has no supervisor for some reason)." msgstr "" @@ -658,6 +707,10 @@ msgstr "" msgid "The following Time Captures are still in draft status:" msgstr "" +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:167 +msgid "The following dates are in the past (or today) and have been removed from the Absence Plan:
{0}" +msgstr "" + #: time_capture/scripts/employee.py:77 msgid "The supervisor cannot be the same as the employee." msgstr "" @@ -666,8 +719,8 @@ msgstr "" msgid "The supervisor has no User linked in his/her Employee record." msgstr "" -#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:158 -msgid "There is already an Absence Plan for this employee for the selected date range. See here: {0}" +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:208 +msgid "There are overlapping dates with following Absence Plans:
{0}" msgstr "" #: time_capture/scripts/employee.py:87 @@ -687,7 +740,7 @@ msgid "This is the projected balance, that includes planned overtime reductions" msgstr "" #. Name of a DocType -#: time_capture/custom_fields.py:28 time_capture/custom_fields.py:93 +#: time_capture/custom_fields.py:46 time_capture/custom_fields.py:111 #: time_capture/time_capture/doctype/time_capture/time_capture.json msgid "Time Capture" msgstr "" @@ -701,7 +754,7 @@ msgstr "" msgid "Time Capture Log" msgstr "" -#: time_capture/scripts/attendance.py:157 +#: time_capture/scripts/attendance.py:154 msgid "Time Capture Not Found" msgstr "" @@ -723,6 +776,18 @@ msgstr "" msgid "Time to Submit (in days)" msgstr "" +#: time_capture/time_capture/doctype/absence_plan/absence_plan.js:81 +msgid "Timespan: adds all dates in the range with an optional reason." +msgstr "" + +#: time_capture/time_capture/doctype/absence_plan/absence_plan.js:82 +msgid "Typical use case: Employee is on Education Leave (University, School, etc.)." +msgstr "" + +#: time_capture/time_capture/doctype/absence_plan/absence_plan.js:75 +msgid "Typical use case: Every Friday is an unofficial weekly off day." +msgstr "" + #: time_capture/public/js/utils.js:120 msgid "Unable to fetch summary data" msgstr "" @@ -742,6 +807,15 @@ msgstr "" msgid "User is not a Freelancer" msgstr "" +#: time_capture/time_capture/doctype/absence_plan/absence_plan.js:44 +#: time_capture/time_capture/doctype/absence_plan/absence_plan.js:88 +msgid "Warning: The Leave Ledgers will not be updated for leaves with an Attendance Plan. Therefore, it is not suggested to use Leave Types (such as 'Annual Leave') that have limited Leave Allocation." +msgstr "" + +#: time_capture/time_capture/doctype/absence_plan/absence_plan.js:72 +msgid "Weekly Off: adds all dates in the range that fall on the selected weekday. The Holiday List already defines non-working days; adding e.g. every Sunday as weekly off is often redundant." +msgstr "" + #. Label of a Duration field in DocType 'Time Capture' #. Name of a report #: time_capture/time_capture/doctype/time_capture/time_capture.json @@ -764,7 +838,7 @@ msgstr "" msgid "Working time must be completely booked on projects and tasks." msgstr "" -#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:113 +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:123 msgid "Your Absence Plan {0} has been {1}." msgstr "" @@ -780,7 +854,7 @@ msgstr "" msgid "{0} already exists for this date and person." msgstr "" -#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:80 +#: time_capture/time_capture/doctype/absence_plan/absence_plan.py:90 msgid "{0} raised a new Absence Plan for approval: {1}" msgstr "" diff --git a/time_capture/time_capture/doctype/absence_plan/absence_plan.py b/time_capture/time_capture/doctype/absence_plan/absence_plan.py index 52faa9f..847777a 100644 --- a/time_capture/time_capture/doctype/absence_plan/absence_plan.py +++ b/time_capture/time_capture/doctype/absence_plan/absence_plan.py @@ -281,9 +281,7 @@ def delete_future_dates(name: str) -> str: if doc.docstatus != 1: frappe.throw(_("Only submitted Absence Plans can use this action.")) if getdate(doc.from_date) > getdate(): - frappe.throw( - _("From Date is in the future. This action is only for plans that have already started.") - ) + frappe.throw(_("From Date is in the future. This means you can just cancel the whole Absence Plan.")) if not doc.has_permission("cancel") and doc.leave_approver != frappe.session.user: frappe.throw( _("Only the Leave Approver and users with the 'Cancel' permission can delete future dates."), From c4602657f63cad6f73908d1ed931492ce3028ce6 Mon Sep 17 00:00:00 2001 From: Patrick Eissler <77415730+PatrickDEissler@users.noreply.github.com> Date: Tue, 3 Feb 2026 11:52:11 +0100 Subject: [PATCH 21/26] fix: keep from_date consistently in the past or today --- time_capture/locale/de.po | 4 ++-- time_capture/locale/main.pot | 2 +- .../time_capture/doctype/absence_plan/absence_plan.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/time_capture/locale/de.po b/time_capture/locale/de.po index cd57504..1b9eb66 100644 --- a/time_capture/locale/de.po +++ b/time_capture/locale/de.po @@ -153,8 +153,8 @@ msgid "Current Balance" msgstr "Aktuelles Saldo" #: time_capture/time_capture/doctype/absence_plan/absence_plan.py:225 -msgid "Dates cannot be in the past." -msgstr "Daten dürfen nicht in der Vergangenheit liegen." +msgid "Dates cannot be in the past or today." +msgstr "Daten dürfen nicht in der Vergangenheit (oder auf heute) liegen." #: time_capture/time_capture/report/working_time/working_time.js:32 msgid "December" diff --git a/time_capture/locale/main.pot b/time_capture/locale/main.pot index 08d8cf3..9281275 100644 --- a/time_capture/locale/main.pot +++ b/time_capture/locale/main.pot @@ -153,7 +153,7 @@ msgid "Current Balance" msgstr "" #: time_capture/time_capture/doctype/absence_plan/absence_plan.py:225 -msgid "Dates cannot be in the past." +msgid "Dates cannot be in the past or today." msgstr "" #: time_capture/time_capture/report/working_time/working_time.js:32 diff --git a/time_capture/time_capture/doctype/absence_plan/absence_plan.py b/time_capture/time_capture/doctype/absence_plan/absence_plan.py index 847777a..e0f566e 100644 --- a/time_capture/time_capture/doctype/absence_plan/absence_plan.py +++ b/time_capture/time_capture/doctype/absence_plan/absence_plan.py @@ -221,8 +221,8 @@ def bulk_insert_dates( to_date = getdate(to_date) if to_date < from_date: frappe.throw(_("To Date cannot be before From Date.")) - if from_date < getdate(): - frappe.throw(_("Dates cannot be in the past.")) + if from_date <= getdate(): + frappe.throw(_("Dates cannot be in the past or today.")) if mode == "Weekly Off": if not weekday: From b09f9bfb1841d6c59b8f8ab0d40785f007cf7990 Mon Sep 17 00:00:00 2001 From: Patrick Eissler <77415730+PatrickDEissler@users.noreply.github.com> Date: Tue, 3 Feb 2026 11:56:06 +0100 Subject: [PATCH 22/26] fix: remove un-necessary ct column "weekday" --- .../doctype/absence_plan_date/absence_plan_date.json | 9 --------- 1 file changed, 9 deletions(-) diff --git a/time_capture/time_capture/doctype/absence_plan_date/absence_plan_date.json b/time_capture/time_capture/doctype/absence_plan_date/absence_plan_date.json index ee8da12..9128970 100644 --- a/time_capture/time_capture/doctype/absence_plan_date/absence_plan_date.json +++ b/time_capture/time_capture/doctype/absence_plan_date/absence_plan_date.json @@ -7,7 +7,6 @@ "engine": "InnoDB", "field_order": [ "date", - "weekday", "reason" ], "fields": [ @@ -18,14 +17,6 @@ "label": "Date", "reqd": 1 }, - { - "fieldname": "weekday", - "fieldtype": "Select", - "label": "Weekday", - "options": "Monday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday", - "read_only": 1, - "no_copy": 1 - }, { "fieldname": "reason", "fieldtype": "Text Editor", From 967e3a075ee98225779a9b04cee559f9b9f2cb64 Mon Sep 17 00:00:00 2001 From: Patrick Eissler <77415730+PatrickDEissler@users.noreply.github.com> Date: Tue, 3 Feb 2026 11:57:40 +0100 Subject: [PATCH 23/26] fix: replace "Attendance Plan" with "Absence Plan" --- time_capture/locale/main.pot | 4 ++-- .../time_capture/doctype/absence_plan/absence_plan.js | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/time_capture/locale/main.pot b/time_capture/locale/main.pot index 9281275..ac658b9 100644 --- a/time_capture/locale/main.pot +++ b/time_capture/locale/main.pot @@ -352,7 +352,7 @@ msgid "Go to list view" msgstr "" #: time_capture/time_capture/doctype/absence_plan/absence_plan.js:85 -msgid "Holidays from a Holiday List have priority over leaves with an Attendance Plan." +msgid "Holidays from a Holiday List have priority over leaves with an Absence Plan." msgstr "" #. Description of the 'Enable Reminders' (Check) field in DocType 'Time Capture @@ -809,7 +809,7 @@ msgstr "" #: time_capture/time_capture/doctype/absence_plan/absence_plan.js:44 #: time_capture/time_capture/doctype/absence_plan/absence_plan.js:88 -msgid "Warning: The Leave Ledgers will not be updated for leaves with an Attendance Plan. Therefore, it is not suggested to use Leave Types (such as 'Annual Leave') that have limited Leave Allocation." +msgid "Warning: The Leave Ledgers will not be updated for leaves with an Absence Plan. Therefore, it is not suggested to use Leave Types (such as 'Annual Leave') that have limited Leave Allocation." msgstr "" #: time_capture/time_capture/doctype/absence_plan/absence_plan.js:72 diff --git a/time_capture/time_capture/doctype/absence_plan/absence_plan.js b/time_capture/time_capture/doctype/absence_plan/absence_plan.js index b2cc8da..e7416a4 100644 --- a/time_capture/time_capture/doctype/absence_plan/absence_plan.js +++ b/time_capture/time_capture/doctype/absence_plan/absence_plan.js @@ -42,7 +42,7 @@ frappe.ui.form.on("Absence Plan", { leave_type: function (frm) { frappe.msgprint( __( - "Warning: The Leave Ledgers will not be updated for leaves with an Attendance Plan. Therefore, it is not suggested to use Leave Types (such as 'Annual Leave') that have limited Leave Allocation." + "Warning: The Leave Ledgers will not be updated for leaves with an Absence Plan. Therefore, it is not suggested to use Leave Types (such as 'Annual Leave') that have limited Leave Allocation." ) ); }, @@ -83,10 +83,10 @@ function open_bulk_insert_dialog(frm) { "Typical use case: Employee is on Education Leave (University, School, etc.)." ); const holiday_list_desc = __( - "Holidays from a Holiday List have priority over leaves with an Attendance Plan." + "Holidays from a Holiday List have priority over leaves with an Absence Plan." ); const warning_desc = __( - "Warning: The Leave Ledgers will not be updated for leaves with an Attendance Plan. Therefore, it is not suggested to use Leave Types (such as 'Annual Leave') that have limited Leave Allocation." + "Warning: The Leave Ledgers will not be updated for leaves with an Absence Plan. Therefore, it is not suggested to use Leave Types (such as 'Annual Leave') that have limited Leave Allocation." ); const descriptionHtml = From 08be0762b74650fab1f85f24b0b68acd80ce2c59 Mon Sep 17 00:00:00 2001 From: Patrick Eissler <77415730+PatrickDEissler@users.noreply.github.com> Date: Tue, 3 Feb 2026 12:00:35 +0100 Subject: [PATCH 24/26] fix: change translations --- time_capture/locale/de.po | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/time_capture/locale/de.po b/time_capture/locale/de.po index 1b9eb66..7e4a085 100644 --- a/time_capture/locale/de.po +++ b/time_capture/locale/de.po @@ -352,7 +352,7 @@ msgid "Go to list view" msgstr "Zur Listenansicht" #: time_capture/time_capture/doctype/absence_plan/absence_plan.js:85 -msgid "Holidays from a Holiday List have priority over leaves with an Attendance Plan." +msgid "Holidays from a Holiday List have priority over leaves with an Absence Plan." msgstr "Feiertage aus einer Feiertagsliste haben Vorrang vor Abwesenheiten mit einem Abwesenheitsplan." #. Description of the 'Enable Reminders' (Check) field in DocType 'Time Capture @@ -809,7 +809,7 @@ msgstr "Benutzer ist kein Freelancer" #: time_capture/time_capture/doctype/absence_plan/absence_plan.js:44 #: time_capture/time_capture/doctype/absence_plan/absence_plan.js:88 -msgid "Warning: The Leave Ledgers will not be updated for leaves with an Attendance Plan. Therefore, it is not suggested to use Leave Types (such as 'Annual Leave') that have limited Leave Allocation." +msgid "Warning: The Leave Ledgers will not be updated for leaves with an Absence Plan. Therefore, it is not suggested to use Leave Types (such as 'Annual Leave') that have limited Leave Allocation." msgstr "Hinweis: Die Urlaubskonten werden bei Abwesenheiten mit einem Abwesenheitsplan nicht aktualisiert. Daher wird davon abgeraten, Urlaubstypen mit begrenzter Urlaubszuweisung (z. B. 'Erholungsurlaub') zu verwenden." #: time_capture/time_capture/doctype/absence_plan/absence_plan.js:72 From 12d83025adfe0a9cbc7937a35a95b65b4c56aa5a Mon Sep 17 00:00:00 2001 From: Patrick Eissler <77415730+PatrickDEissler@users.noreply.github.com> Date: Tue, 3 Feb 2026 12:00:47 +0100 Subject: [PATCH 25/26] fix: reload doc --- time_capture/time_capture/doctype/absence_plan/absence_plan.py | 1 + 1 file changed, 1 insertion(+) diff --git a/time_capture/time_capture/doctype/absence_plan/absence_plan.py b/time_capture/time_capture/doctype/absence_plan/absence_plan.py index e0f566e..59d4e76 100644 --- a/time_capture/time_capture/doctype/absence_plan/absence_plan.py +++ b/time_capture/time_capture/doctype/absence_plan/absence_plan.py @@ -297,6 +297,7 @@ def delete_future_dates(name: str) -> str: deleted_count += 1 if deleted_count: + doc.reload() remaining_dates = [getdate(r.date) for r in doc.dates if getdate(r.date) <= today] if remaining_dates: frappe.db.set_value( From d8c476d83c1ea2ab6ebffce6a77fe0ee77656a5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Ei=C3=9Fler?= <77415730+PatrickDEissler@users.noreply.github.com> Date: Tue, 3 Feb 2026 12:02:06 +0100 Subject: [PATCH 26/26] docs: update docstring Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../time_capture/doctype/time_capture/time_capture.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/time_capture/time_capture/doctype/time_capture/time_capture.py b/time_capture/time_capture/doctype/time_capture/time_capture.py index 3150d5b..59911a9 100644 --- a/time_capture/time_capture/doctype/time_capture/time_capture.py +++ b/time_capture/time_capture/doctype/time_capture/time_capture.py @@ -325,9 +325,11 @@ def _create_time_capture(employee, date): def _create_attendance_for_absence_plan(employee_name, date): """ - Check if the employee has an approved Absence Plan that includes the given date. - Returns the leave_type from the matching plan, or None if none found. - Caller is responsible for attendance creation etc. + Check if the employee has an approved Absence Plan that includes the given date + and create an Attendance record for it if found. + + Returns: + bool: True if an Attendance record was created, False otherwise. """ # Get Absence Plan and Leave Type from Absence Plan AP = frappe.qb.DocType("Absence Plan")