From 80a95b0983cd84cc38fb148334c78bbbb50c9cad Mon Sep 17 00:00:00 2001 From: scdanieli <23150094+scdanieli@users.noreply.github.com> Date: Fri, 27 Sep 2024 12:20:16 +0200 Subject: [PATCH 1/3] feat: add option to enforce mandatory breaks (DRC-26) --- .../doctype/working_time/working_time.js | 3 + .../doctype/working_time/working_time.json | 43 +++++++++++- .../doctype/working_time/working_time.py | 66 +++++++++++++++++-- .../working_time_settings.json | 11 +++- arbeitszeiterfassung_s4a/translations/de.csv | 9 +++ 5 files changed, 122 insertions(+), 10 deletions(-) create mode 100644 arbeitszeiterfassung_s4a/translations/de.csv diff --git a/arbeitszeiterfassung_s4a/arbeitszeiterfassung_s4a/doctype/working_time/working_time.js b/arbeitszeiterfassung_s4a/arbeitszeiterfassung_s4a/doctype/working_time/working_time.js index f533f3c..32faccf 100644 --- a/arbeitszeiterfassung_s4a/arbeitszeiterfassung_s4a/doctype/working_time/working_time.js +++ b/arbeitszeiterfassung_s4a/arbeitszeiterfassung_s4a/doctype/working_time/working_time.js @@ -23,6 +23,9 @@ frappe.ui.form.on("Working Time", { }); frappe.ui.form.on("Working Time Log", { + is_break: function (frm) { + frm.refresh_fields(); + }, time_logs_add: function (frm, cdt, cdn) { let current_row = locals[cdt][cdn]; let index = frm.doc.time_logs.findIndex((row) => row.name === current_row.name); diff --git a/arbeitszeiterfassung_s4a/arbeitszeiterfassung_s4a/doctype/working_time/working_time.json b/arbeitszeiterfassung_s4a/arbeitszeiterfassung_s4a/doctype/working_time/working_time.json index 94f071d..d784af0 100644 --- a/arbeitszeiterfassung_s4a/arbeitszeiterfassung_s4a/doctype/working_time/working_time.json +++ b/arbeitszeiterfassung_s4a/arbeitszeiterfassung_s4a/doctype/working_time/working_time.json @@ -13,9 +13,14 @@ "date", "time_logs", "section_break_7", + "indicated_break_time", + "mandatory_break_time", "break_time", + "column_break_amiy", + "total_time", "working_time", "project_time", + "section_break_syac", "amended_from" ], "fields": [ @@ -60,6 +65,7 @@ "read_only": 1 }, { + "bold": 1, "fieldname": "working_time", "fieldtype": "Duration", "hide_days": 1, @@ -75,6 +81,7 @@ "options": "Working Time Log" }, { + "bold": 1, "fieldname": "break_time", "fieldtype": "Duration", "hide_days": 1, @@ -89,6 +96,8 @@ { "fieldname": "project_time", "fieldtype": "Duration", + "hide_days": 1, + "hide_seconds": 1, "label": "Project Time", "read_only": 1 }, @@ -99,6 +108,38 @@ "label": "Department", "options": "Department", "read_only": 1 + }, + { + "fieldname": "column_break_amiy", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_syac", + "fieldtype": "Section Break" + }, + { + "fieldname": "indicated_break_time", + "fieldtype": "Duration", + "hide_days": 1, + "hide_seconds": 1, + "label": "Indicated Break", + "read_only_depends_on": "eval:doc.time_logs.some(row => row.is_break === 1)" + }, + { + "fieldname": "mandatory_break_time", + "fieldtype": "Duration", + "hide_days": 1, + "hide_seconds": 1, + "label": "Mandatory Break", + "read_only": 1 + }, + { + "fieldname": "total_time", + "fieldtype": "Duration", + "hide_days": 1, + "hide_seconds": 1, + "label": "Total Time", + "read_only": 1 } ], "is_submittable": 1, @@ -112,7 +153,7 @@ "link_fieldname": "working_time" } ], - "modified": "2023-06-22 12:25:38.722346", + "modified": "2024-09-27 11:50:43.070779", "modified_by": "Administrator", "module": "Arbeitszeiterfassung S4A", "name": "Working Time", diff --git a/arbeitszeiterfassung_s4a/arbeitszeiterfassung_s4a/doctype/working_time/working_time.py b/arbeitszeiterfassung_s4a/arbeitszeiterfassung_s4a/doctype/working_time/working_time.py index 1513b31..3819d27 100644 --- a/arbeitszeiterfassung_s4a/arbeitszeiterfassung_s4a/doctype/working_time/working_time.py +++ b/arbeitszeiterfassung_s4a/arbeitszeiterfassung_s4a/doctype/working_time/working_time.py @@ -17,13 +17,17 @@ def get_default_activity(): class WorkingTime(Document): def before_validate(self): - self.break_time = self.working_time = self.project_time = 0 - for log in self.time_logs: - log.set_duration() - duration = log.duration or 0 - self.break_time += duration if log.is_break else 0 - self.working_time += 0 if log.is_break else duration - self.project_time += duration if log.project and not log.is_break else 0 + self.set_total_times() + + def set_total_times(self): + ( + self.total_time, + self.working_time, + self.project_time, + self.indicated_break_time, + self.mandatory_break_time, + self.break_time, + ) = calculate_total_times(self.time_logs, self.indicated_break_time or 0) def validate(self): for log in self.time_logs: @@ -111,3 +115,51 @@ def get_costing_rate(employee): {"activity_type": get_default_activity(), "employee": employee}, "costing_rate", ) + + +def calculate_total_times(time_logs, user_indicated_break_time): + total_time = 0 + total_working_time = 0 + total_project_time = 0 + total_indicated_break_time = 0 + + for log in time_logs: + log.set_duration() + duration = log.duration or 0 + + if log.is_break: + total_indicated_break_time += duration + else: + total_working_time += duration + if log.project: + total_project_time += duration + + total_time += duration + + mandatory_break_time = calcualte_mandatory_break_time( + total_working_time, total_indicated_break_time + ) + actual_break_time = max( + mandatory_break_time, total_indicated_break_time or user_indicated_break_time + ) + adjusted_working_time = total_time - actual_break_time + + return ( + total_time, + adjusted_working_time, + total_project_time, + total_indicated_break_time or user_indicated_break_time, + mandatory_break_time, + actual_break_time, + ) + + +def calcualte_mandatory_break_time(working_time, break_time): + if not frappe.db.get_single_value("Working Time Settings", "enforce_mandatory_breaks"): + return 0 + elif working_time + break_time > 9.75 * ONE_HOUR or working_time > 9 * ONE_HOUR: + return 45 * 60 + elif working_time + break_time > 6.5 * ONE_HOUR or working_time > 6 * ONE_HOUR: + return 30 * 60 + else: + return 0 diff --git a/arbeitszeiterfassung_s4a/arbeitszeiterfassung_s4a/doctype/working_time_settings/working_time_settings.json b/arbeitszeiterfassung_s4a/arbeitszeiterfassung_s4a/doctype/working_time_settings/working_time_settings.json index fd16e98..3135a0b 100644 --- a/arbeitszeiterfassung_s4a/arbeitszeiterfassung_s4a/doctype/working_time_settings/working_time_settings.json +++ b/arbeitszeiterfassung_s4a/arbeitszeiterfassung_s4a/doctype/working_time_settings/working_time_settings.json @@ -7,7 +7,8 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "default_activity" + "default_activity", + "enforce_mandatory_breaks" ], "fields": [ { @@ -17,12 +18,18 @@ "label": "Default Activity", "options": "Activity Type", "reqd": 1 + }, + { + "default": "0", + "fieldname": "enforce_mandatory_breaks", + "fieldtype": "Check", + "label": "Enforce Mandatory Breaks" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-08-02 17:24:48.248467", + "modified": "2024-09-27 10:14:36.062308", "modified_by": "Administrator", "module": "Arbeitszeiterfassung S4A", "name": "Working Time Settings", diff --git a/arbeitszeiterfassung_s4a/translations/de.csv b/arbeitszeiterfassung_s4a/translations/de.csv new file mode 100644 index 0000000..bc33898 --- /dev/null +++ b/arbeitszeiterfassung_s4a/translations/de.csv @@ -0,0 +1,9 @@ +Working Time Settings,Arbeitszeiteinstellungen, +Default Activity,Standardtätigkeit, +Enforce Mandatory Breaks,Verpflichtende Pausen erzwingen, +Break,Pause, +Indicated Break,Angegebene Pause, +Mandatory Break,Verpflichtende Pause, +Total Time,Gesamtzeit, +Working Time,Arbeitszeit, +Project Time,Projektzeit, From d0aa5017de436ad871e332bcbc3ef812aff82cf0 Mon Sep 17 00:00:00 2001 From: scdanieli <23150094+scdanieli@users.noreply.github.com> Date: Fri, 27 Sep 2024 12:25:25 +0200 Subject: [PATCH 2/3] chore: default activity should not be mandatory (DRC-26) --- .../doctype/working_time_settings/working_time_settings.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/arbeitszeiterfassung_s4a/arbeitszeiterfassung_s4a/doctype/working_time_settings/working_time_settings.json b/arbeitszeiterfassung_s4a/arbeitszeiterfassung_s4a/doctype/working_time_settings/working_time_settings.json index 3135a0b..490bc28 100644 --- a/arbeitszeiterfassung_s4a/arbeitszeiterfassung_s4a/doctype/working_time_settings/working_time_settings.json +++ b/arbeitszeiterfassung_s4a/arbeitszeiterfassung_s4a/doctype/working_time_settings/working_time_settings.json @@ -16,8 +16,7 @@ "fieldtype": "Link", "in_list_view": 1, "label": "Default Activity", - "options": "Activity Type", - "reqd": 1 + "options": "Activity Type" }, { "default": "0", @@ -29,7 +28,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2024-09-27 10:14:36.062308", + "modified": "2024-09-27 12:24:23.603346", "modified_by": "Administrator", "module": "Arbeitszeiterfassung S4A", "name": "Working Time Settings", From 7d013e51b15ae715fbbaf6bd7e1505d9cdd20540 Mon Sep 17 00:00:00 2001 From: scdanieli <23150094+scdanieli@users.noreply.github.com> Date: Sat, 28 Sep 2024 18:39:06 +0200 Subject: [PATCH 3/3] feat: make mandatory breaks flexible (DRC-26) --- .../doctype/mandatory_break/__init__.py | 0 .../mandatory_break/mandatory_break.json | 44 +++++++++++++++++++ .../mandatory_break/mandatory_break.py | 9 ++++ .../doctype/working_time/working_time.py | 42 +++++++++++++----- .../working_time_settings.json | 18 +++++++- arbeitszeiterfassung_s4a/translations/de.csv | 3 ++ 6 files changed, 103 insertions(+), 13 deletions(-) create mode 100644 arbeitszeiterfassung_s4a/arbeitszeiterfassung_s4a/doctype/mandatory_break/__init__.py create mode 100644 arbeitszeiterfassung_s4a/arbeitszeiterfassung_s4a/doctype/mandatory_break/mandatory_break.json create mode 100644 arbeitszeiterfassung_s4a/arbeitszeiterfassung_s4a/doctype/mandatory_break/mandatory_break.py diff --git a/arbeitszeiterfassung_s4a/arbeitszeiterfassung_s4a/doctype/mandatory_break/__init__.py b/arbeitszeiterfassung_s4a/arbeitszeiterfassung_s4a/doctype/mandatory_break/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/arbeitszeiterfassung_s4a/arbeitszeiterfassung_s4a/doctype/mandatory_break/mandatory_break.json b/arbeitszeiterfassung_s4a/arbeitszeiterfassung_s4a/doctype/mandatory_break/mandatory_break.json new file mode 100644 index 0000000..0f696da --- /dev/null +++ b/arbeitszeiterfassung_s4a/arbeitszeiterfassung_s4a/doctype/mandatory_break/mandatory_break.json @@ -0,0 +1,44 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2024-09-27 15:10:44.141822", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "working_time", + "additional_break_time" + ], + "fields": [ + { + "fieldname": "working_time", + "fieldtype": "Duration", + "hide_days": 1, + "hide_seconds": 1, + "in_list_view": 1, + "label": "Working Time (Greater Than)", + "reqd": 1 + }, + { + "fieldname": "additional_break_time", + "fieldtype": "Duration", + "hide_days": 1, + "hide_seconds": 1, + "in_list_view": 1, + "label": "Additional Break", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2024-09-28 18:33:23.991534", + "modified_by": "Administrator", + "module": "Arbeitszeiterfassung S4A", + "name": "Mandatory Break", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/arbeitszeiterfassung_s4a/arbeitszeiterfassung_s4a/doctype/mandatory_break/mandatory_break.py b/arbeitszeiterfassung_s4a/arbeitszeiterfassung_s4a/doctype/mandatory_break/mandatory_break.py new file mode 100644 index 0000000..061d2f5 --- /dev/null +++ b/arbeitszeiterfassung_s4a/arbeitszeiterfassung_s4a/doctype/mandatory_break/mandatory_break.py @@ -0,0 +1,9 @@ +# Copyright (c) 2024, ALYF GmbH and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class MandatoryBreak(Document): + pass diff --git a/arbeitszeiterfassung_s4a/arbeitszeiterfassung_s4a/doctype/working_time/working_time.py b/arbeitszeiterfassung_s4a/arbeitszeiterfassung_s4a/doctype/working_time/working_time.py index 3819d27..0a71e50 100644 --- a/arbeitszeiterfassung_s4a/arbeitszeiterfassung_s4a/doctype/working_time/working_time.py +++ b/arbeitszeiterfassung_s4a/arbeitszeiterfassung_s4a/doctype/working_time/working_time.py @@ -43,7 +43,8 @@ def on_submit(self): def create_attendance(self): if not frappe.db.exists( - "Attendance", {"employee": self.employee, "attendance_date": self.date, "docstatus": ("!=", 2)} + "Attendance", + {"employee": self.employee, "attendance_date": self.date, "docstatus": ("!=", 2)}, ): HALF_DAY = frappe.get_value("Employee", self.employee, "expected_daily_working_hours") / 2 OVERTIME_FACTOR = 1.15 @@ -136,8 +137,11 @@ def calculate_total_times(time_logs, user_indicated_break_time): total_time += duration - mandatory_break_time = calcualte_mandatory_break_time( - total_working_time, total_indicated_break_time + mandatory_break_time = calculate_mandatory_break_time( + total_working_time + if total_indicated_break_time + else total_working_time - user_indicated_break_time, + total_indicated_break_time or user_indicated_break_time, ) actual_break_time = max( mandatory_break_time, total_indicated_break_time or user_indicated_break_time @@ -154,12 +158,28 @@ def calculate_total_times(time_logs, user_indicated_break_time): ) -def calcualte_mandatory_break_time(working_time, break_time): - if not frappe.db.get_single_value("Working Time Settings", "enforce_mandatory_breaks"): - return 0 - elif working_time + break_time > 9.75 * ONE_HOUR or working_time > 9 * ONE_HOUR: - return 45 * 60 - elif working_time + break_time > 6.5 * ONE_HOUR or working_time > 6 * ONE_HOUR: - return 30 * 60 - else: +def calculate_mandatory_break_time(working_time, break_time): + # TODO: Write comprehensive tests to cover all edge cases + settings = frappe.get_single("Working Time Settings") + + if not settings.enforce_mandatory_breaks: return 0 + + break_cases = sorted( + (entry.working_time, entry.additional_break_time) for entry in settings.mandatory_breaks + ) + + total_mandatory_break = 0 + + for threshold, mandatory_break in break_cases: + if ( + working_time + break_time > threshold + mandatory_break + total_mandatory_break + or working_time > threshold + ): + total_mandatory_break += mandatory_break + + if total_mandatory_break > break_time: + working_time = working_time - total_mandatory_break + break_time + break_time = total_mandatory_break + + return total_mandatory_break diff --git a/arbeitszeiterfassung_s4a/arbeitszeiterfassung_s4a/doctype/working_time_settings/working_time_settings.json b/arbeitszeiterfassung_s4a/arbeitszeiterfassung_s4a/doctype/working_time_settings/working_time_settings.json index 490bc28..14dae4e 100644 --- a/arbeitszeiterfassung_s4a/arbeitszeiterfassung_s4a/doctype/working_time_settings/working_time_settings.json +++ b/arbeitszeiterfassung_s4a/arbeitszeiterfassung_s4a/doctype/working_time_settings/working_time_settings.json @@ -8,7 +8,9 @@ "engine": "InnoDB", "field_order": [ "default_activity", - "enforce_mandatory_breaks" + "mandatory_breaks_tab", + "enforce_mandatory_breaks", + "mandatory_breaks" ], "fields": [ { @@ -23,12 +25,24 @@ "fieldname": "enforce_mandatory_breaks", "fieldtype": "Check", "label": "Enforce Mandatory Breaks" + }, + { + "depends_on": "eval:doc.enforce_mandatory_breaks", + "fieldname": "mandatory_breaks", + "fieldtype": "Table", + "label": "Mandatory Breaks", + "options": "Mandatory Break" + }, + { + "fieldname": "mandatory_breaks_tab", + "fieldtype": "Tab Break", + "label": "Mandatory Breaks" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2024-09-27 12:24:23.603346", + "modified": "2024-09-28 16:20:12.769126", "modified_by": "Administrator", "module": "Arbeitszeiterfassung S4A", "name": "Working Time Settings", diff --git a/arbeitszeiterfassung_s4a/translations/de.csv b/arbeitszeiterfassung_s4a/translations/de.csv index bc33898..0b3f898 100644 --- a/arbeitszeiterfassung_s4a/translations/de.csv +++ b/arbeitszeiterfassung_s4a/translations/de.csv @@ -2,8 +2,11 @@ Working Time Settings,Arbeitszeiteinstellungen, Default Activity,Standardtätigkeit, Enforce Mandatory Breaks,Verpflichtende Pausen erzwingen, Break,Pause, +Additional Break,Zusätzliche Pause, Indicated Break,Angegebene Pause, Mandatory Break,Verpflichtende Pause, +Mandatory Breaks,Verpflichtende Pausen, Total Time,Gesamtzeit, Working Time,Arbeitszeit, +Working Time (Greater Than),Arbeitszeit (Größer als), Project Time,Projektzeit,