From 08874ac9376903111367b578fbcb5f38fbb1015d Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Tue, 26 May 2026 23:10:52 +0200
Subject: [PATCH 1/6] feat: paid breaks
---
working_time/patches.txt | 2 +
...backfill_productive_and_paid_break_time.py | 6 +
...ame_max_working_time_to_productive_time.py | 9 ++
.../doctype/working_time/test_working_time.py | 115 ++++++++++++++++++
.../doctype/working_time/working_time.js | 6 +
.../doctype/working_time/working_time.json | 33 ++++-
.../doctype/working_time/working_time.py | 35 ++++--
.../working_time_log/working_time_log.json | 13 +-
.../working_time_policy.json | 18 +--
9 files changed, 209 insertions(+), 28 deletions(-)
create mode 100644 working_time/patches/backfill_productive_and_paid_break_time.py
create mode 100644 working_time/patches/rename_max_working_time_to_productive_time.py
diff --git a/working_time/patches.txt b/working_time/patches.txt
index 5bf4ed9..495241f 100644
--- a/working_time/patches.txt
+++ b/working_time/patches.txt
@@ -6,3 +6,5 @@ working_time.patches.link_timesheet_and_attendance_to_working_time # 2023-08-14
working_time.patches.billable_time_pct # 1
working_time.patches.add_total_to_old_freelancer_time
execute:from working_time.install import make_custom_fields; make_custom_fields() # 2026-03-27
+working_time.patches.rename_max_working_time_to_productive_time
+working_time.patches.backfill_productive_and_paid_break_time # 1
diff --git a/working_time/patches/backfill_productive_and_paid_break_time.py b/working_time/patches/backfill_productive_and_paid_break_time.py
new file mode 100644
index 0000000..ef03d3f
--- /dev/null
+++ b/working_time/patches/backfill_productive_and_paid_break_time.py
@@ -0,0 +1,6 @@
+import frappe
+
+
+def execute():
+ wt = frappe.qb.DocType("Working Time")
+ frappe.qb.update(wt).set(wt.productive_time, wt.working_time).set(wt.paid_break_time, 0).run()
diff --git a/working_time/patches/rename_max_working_time_to_productive_time.py b/working_time/patches/rename_max_working_time_to_productive_time.py
new file mode 100644
index 0000000..26e56b9
--- /dev/null
+++ b/working_time/patches/rename_max_working_time_to_productive_time.py
@@ -0,0 +1,9 @@
+from frappe.model.utils.rename_field import rename_field
+
+
+def execute():
+ rename_field(
+ "Working Time Policy",
+ "max_working_time_per_day",
+ "max_productive_time_per_day",
+ )
diff --git a/working_time/working_time/doctype/working_time/test_working_time.py b/working_time/working_time/doctype/working_time/test_working_time.py
index c7e6b58..5b1be10 100644
--- a/working_time/working_time/doctype/working_time/test_working_time.py
+++ b/working_time/working_time/doctype/working_time/test_working_time.py
@@ -3,12 +3,23 @@
import unittest
+import frappe
from frappe import _dict
from working_time.working_time.doctype.working_time.working_time import aggregate_time_logs
class TestWorkingTime(unittest.TestCase):
+ def get_working_time(self, time_logs):
+ return frappe.get_doc(
+ {
+ "doctype": "Working Time",
+ "employee": "Test Employee",
+ "date": "2026-05-26",
+ "time_logs": time_logs,
+ }
+ )
+
def test_aggregate_time_logs(self):
logs = [
_dict(
@@ -72,3 +83,107 @@ def test_aggregate_time_logs(self):
self.assertEqual(project_b["hours"], 2.0)
self.assertEqual(project_b["internal_notes"], [])
self.assertEqual(project_b["customer_notes"], ["Customer Note 1"])
+
+ def test_paid_break_totals(self):
+ working_time = self.get_working_time(
+ [
+ {"from_time": "09:00:00", "to_time": "12:00:00", "is_break": 0},
+ {
+ "from_time": "12:00:00",
+ "to_time": "12:30:00",
+ "is_break": 1,
+ "is_paid_break": 1,
+ },
+ {"from_time": "12:30:00", "to_time": "13:00:00", "is_break": 1},
+ {"from_time": "13:00:00", "to_time": "17:00:00", "is_break": 0},
+ ]
+ )
+
+ working_time.before_validate()
+
+ self.assertEqual(working_time.productive_time, 7 * 60 * 60)
+ self.assertEqual(working_time.paid_break_time, 30 * 60)
+ self.assertEqual(working_time.break_time, 60 * 60)
+ self.assertEqual(working_time.working_time, 7.5 * 60 * 60)
+
+ def test_non_break_clears_paid_break_flag(self):
+ working_time = self.get_working_time(
+ [
+ {
+ "from_time": "09:00:00",
+ "to_time": "10:00:00",
+ "is_break": 0,
+ "is_paid_break": 1,
+ }
+ ]
+ )
+
+ working_time.before_validate()
+
+ self.assertEqual(working_time.time_logs[0].is_paid_break, 0)
+ self.assertEqual(working_time.productive_time, 60 * 60)
+ self.assertEqual(working_time.paid_break_time, 0)
+
+ def test_max_working_time_policy_uses_productive_time(self):
+ policy = _dict({"max_productive_time_per_day": 8 * 60 * 60})
+ working_time = self.get_working_time(
+ [
+ {"from_time": "09:00:00", "to_time": "17:00:00", "is_break": 0},
+ {
+ "from_time": "17:00:00",
+ "to_time": "18:00:00",
+ "is_break": 1,
+ "is_paid_break": 1,
+ },
+ ]
+ )
+ working_time.before_validate()
+
+ self.assertEqual(working_time.productive_time, 8 * 60 * 60)
+ self.assertEqual(working_time.working_time, 9 * 60 * 60)
+ working_time.validate_max_working_time(policy)
+
+ over_limit = self.get_working_time(
+ [
+ {"from_time": "09:00:00", "to_time": "17:15:00", "is_break": 0},
+ ]
+ )
+ over_limit.before_validate()
+
+ self.assertRaises(frappe.ValidationError, over_limit.validate_max_working_time, policy)
+
+ def test_mandatory_breaks_policy_uses_productive_time(self):
+ policy = _dict(
+ {"mandatory_breaks": [_dict({"work_threshold": 6 * 60 * 60, "required_break_minutes": 30 * 60})]}
+ )
+ working_time = self.get_working_time(
+ [
+ {"from_time": "09:00:00", "to_time": "14:45:00", "is_break": 0},
+ {
+ "from_time": "14:45:00",
+ "to_time": "15:05:00",
+ "is_break": 1,
+ "is_paid_break": 1,
+ },
+ ]
+ )
+ working_time.before_validate()
+
+ self.assertEqual(working_time.productive_time, 5.75 * 60 * 60)
+ self.assertEqual(working_time.working_time, (5.75 * 60 * 60) + (20 * 60))
+ working_time.validate_mandatory_breaks(policy)
+
+ missing_break = self.get_working_time(
+ [
+ {"from_time": "09:00:00", "to_time": "15:00:00", "is_break": 0},
+ {
+ "from_time": "15:00:00",
+ "to_time": "15:20:00",
+ "is_break": 1,
+ "is_paid_break": 1,
+ },
+ ]
+ )
+ missing_break.before_validate()
+
+ self.assertRaises(frappe.ValidationError, missing_break.validate_mandatory_breaks, policy)
diff --git a/working_time/working_time/doctype/working_time/working_time.js b/working_time/working_time/doctype/working_time/working_time.js
index 622f3d6..8b623c6 100644
--- a/working_time/working_time/doctype/working_time/working_time.js
+++ b/working_time/working_time/doctype/working_time/working_time.js
@@ -81,6 +81,12 @@ frappe.ui.form.on("Working Time Log", {
);
frappe.model.set_value(cdt, cdn, "to_time", ""); // Otherwise Frappe may overwrite empty values with the current time on save.
},
+ is_break: function (frm, cdt, cdn) {
+ const child = locals[cdt][cdn];
+ if (!child.is_break) {
+ frappe.model.set_value(cdt, cdn, "is_paid_break", 0);
+ }
+ },
project: function (frm, cdt, cdn) {
// set billable time to 0% if Project is of Type "Internal", reset to 100% otherwise
const child = locals[cdt][cdn];
diff --git a/working_time/working_time/doctype/working_time/working_time.json b/working_time/working_time/doctype/working_time/working_time.json
index 91cf677..25ac582 100644
--- a/working_time/working_time/doctype/working_time/working_time.json
+++ b/working_time/working_time/doctype/working_time/working_time.json
@@ -15,10 +15,14 @@
"section_break_7",
"project_time",
"billable_time",
- "working_time",
"column_break_covil",
"project_pct",
"billable_pct",
+ "section_break_cimk",
+ "productive_time",
+ "paid_break_time",
+ "working_time",
+ "column_break_vtvv",
"break_time",
"stats_section",
"stats_html",
@@ -72,7 +76,7 @@
"hide_days": 1,
"hide_seconds": 1,
"in_list_view": 1,
- "label": "Working Time",
+ "label": "Paid Working Time",
"read_only": 1
},
{
@@ -86,7 +90,7 @@
"fieldtype": "Duration",
"hide_days": 1,
"hide_seconds": 1,
- "label": "Break",
+ "label": "Break Time",
"read_only": 1
},
{
@@ -142,6 +146,26 @@
{
"fieldname": "section_break_cg6mr",
"fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "section_break_cimk",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "paid_break_time",
+ "fieldtype": "Duration",
+ "label": "Paid Break Time",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_vtvv",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "productive_time",
+ "fieldtype": "Duration",
+ "label": "Productive Time",
+ "read_only": 1
}
],
"grid_page_length": 50,
@@ -156,7 +180,7 @@
"link_fieldname": "working_time"
}
],
- "modified": "2025-10-17 14:41:15.770625",
+ "modified": "2026-05-26 21:24:36.464213",
"modified_by": "Administrator",
"module": "Working Time",
"name": "Working Time",
@@ -193,6 +217,7 @@
}
],
"row_format": "Dynamic",
+ "rows_threshold_for_grid_search": 20,
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
diff --git a/working_time/working_time/doctype/working_time/working_time.py b/working_time/working_time/doctype/working_time/working_time.py
index 549caec..3d26d7f 100644
--- a/working_time/working_time/doctype/working_time/working_time.py
+++ b/working_time/working_time/doctype/working_time/working_time.py
@@ -22,7 +22,8 @@
class WorkingTime(Document):
def before_validate(self):
- self.break_time = self.working_time = self.project_time = self.billable_time = 0
+ self.break_time = self.working_time = self.productive_time = self.paid_break_time = 0
+ self.project_time = self.billable_time = 0
self.project_pct = self.billable_pct = 0
last_idx = len(self.time_logs) - 1
@@ -30,11 +31,19 @@ def before_validate(self):
log.to_time = self.time_logs[idx + 1].from_time if idx < last_idx else log.to_time
log.cleanup_and_set_duration()
log.duration = log.duration or 0
- self.break_time += log.duration if log.is_break else 0
- self.working_time += 0 if log.is_break else log.duration
- if log.project and not log.is_break:
- self.project_time += log.duration
- self.billable_time += get_billable_duration(log)
+
+ if log.is_break:
+ self.break_time += log.duration
+ if log.is_paid_break:
+ self.paid_break_time += log.duration
+ self.working_time += log.duration
+ else:
+ log.is_paid_break = 0
+ self.productive_time += log.duration
+ self.working_time += log.duration
+ if log.project:
+ self.project_time += log.duration
+ self.billable_time += get_billable_duration(log)
if self.working_time:
self.project_pct = round(self.project_time / self.working_time * 100, 0)
@@ -99,14 +108,14 @@ def validate_holiday_block(self, policy):
)
def validate_max_working_time(self, policy):
- if not policy.max_working_time_per_day:
+ if not policy.max_productive_time_per_day:
return
- if self.working_time > policy.max_working_time_per_day:
+ if self.productive_time > policy.max_productive_time_per_day:
frappe.throw(
- _("Working time ({0}) exceeds the maximum allowed ({1}) per day").format(
- format_duration(self.working_time),
- format_duration(policy.max_working_time_per_day),
+ _("Productive time ({0}) exceeds the maximum allowed ({1}) per day").format(
+ format_duration(self.productive_time),
+ format_duration(policy.max_productive_time_per_day),
)
)
@@ -115,9 +124,9 @@ def validate_mandatory_breaks(self, policy):
return
for row in policy.mandatory_breaks:
- if self.working_time >= row.work_threshold and self.break_time < row.required_break_minutes:
+ if self.productive_time >= row.work_threshold and self.break_time < row.required_break_minutes:
frappe.throw(
- _("Working time of {0} or more requires at least {1} of break time").format(
+ _("Productive time of {0} or more requires at least {1} of break time").format(
format_duration(row.work_threshold),
format_duration(row.required_break_minutes),
)
diff --git a/working_time/working_time/doctype/working_time_log/working_time_log.json b/working_time/working_time/doctype/working_time_log/working_time_log.json
index 80e454b..2ebe625 100644
--- a/working_time/working_time/doctype/working_time_log/working_time_log.json
+++ b/working_time/working_time/doctype/working_time_log/working_time_log.json
@@ -17,7 +17,8 @@
"key",
"note",
"section_break_9",
- "is_break"
+ "is_break",
+ "is_paid_break"
],
"fields": [
{
@@ -101,12 +102,20 @@
"label": "Task",
"link_filters": "[[\"Task\",\"project\",\"=\",\"eval: doc.project\"],[\"Task\",\"status\",\"in\",[\"Open\",\"Pending Review\",\"Working\",\"Overdue\"]]]",
"options": "Task"
+ },
+ {
+ "default": "0",
+ "depends_on": "is_break",
+ "description": "Enable to count this break as paid working time (e.g. mandatory travel time)",
+ "fieldname": "is_paid_break",
+ "fieldtype": "Check",
+ "label": "Paid"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2025-10-05 18:37:53.164955",
+ "modified": "2026-05-26 23:07:44.926553",
"modified_by": "Administrator",
"module": "Working Time",
"name": "Working Time Log",
diff --git a/working_time/working_time/doctype/working_time_policy/working_time_policy.json b/working_time/working_time/doctype/working_time_policy/working_time_policy.json
index 349d262..a945748 100644
--- a/working_time/working_time/doctype/working_time_policy/working_time_policy.json
+++ b/working_time/working_time/doctype/working_time_policy/working_time_policy.json
@@ -6,7 +6,7 @@
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
- "max_working_time_per_day",
+ "max_productive_time_per_day",
"min_rest_between_days",
"section_break_qroa",
"mandatory_breaks",
@@ -15,13 +15,6 @@
"consider_holiday_list"
],
"fields": [
- {
- "fieldname": "max_working_time_per_day",
- "fieldtype": "Duration",
- "hide_days": 1,
- "hide_seconds": 1,
- "label": "Max Working Time Per Day"
- },
{
"fieldname": "min_rest_between_days",
"fieldtype": "Duration",
@@ -55,12 +48,19 @@
"fieldname": "consider_holiday_list",
"fieldtype": "Check",
"label": "Block holidays"
+ },
+ {
+ "fieldname": "max_productive_time_per_day",
+ "fieldtype": "Duration",
+ "hide_days": 1,
+ "hide_seconds": 1,
+ "label": "Max Productive Time Per Day"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2026-03-27 14:44:55.711799",
+ "modified": "2026-05-26 21:32:14.336531",
"modified_by": "Administrator",
"module": "Working Time",
"name": "Working Time Policy",
From c4212292dc26e0d5f43278601ed27c55c4675a53 Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Tue, 26 May 2026 23:11:28 +0200
Subject: [PATCH 2/6] chore: update translations
---
working_time/locale/de.po | 74 ++++++++++++++++++++++--------------
working_time/locale/main.pot | 68 ++++++++++++++++++++++-----------
2 files changed, 91 insertions(+), 51 deletions(-)
diff --git a/working_time/locale/de.po b/working_time/locale/de.po
index fac6e8f..7785773 100644
--- a/working_time/locale/de.po
+++ b/working_time/locale/de.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Working Time VERSION\n"
"Report-Msgid-Bugs-To: hallo@alyf.de\n"
-"POT-Creation-Date: 2026-03-27 14:57+0053\n"
+"POT-Creation-Date: 2026-05-26 23:08+0053\n"
"PO-Revision-Date: 2026-03-27 14:57+0053\n"
"Last-Translator: hallo@alyf.de\n"
"Language-Team: hallo@alyf.de\n"
@@ -112,13 +112,16 @@ msgstr "Gesperrte Tage"
msgid "Blocks real holidays from an employee's holiday list. Ignores weekly off."
msgstr "Sperrt echte Feiertage aus der Feiertagsliste des Mitarbeiters. Wöchentliche freie Tage werden ignoriert."
-#. Label of a Duration field in DocType 'Working Time'
#. Label of a Check field in DocType 'Working Time Log'
-#: working_time/working_time/doctype/working_time/working_time.json
#: working_time/working_time/doctype/working_time_log/working_time_log.json
msgid "Break"
msgstr "Pause"
+#. Label of a Duration field in DocType 'Working Time'
+#: working_time/working_time/doctype/working_time/working_time.json
+msgid "Break Time"
+msgstr "Pausenzeit"
+
#. Label of a number card in the Time Tracking Workspace
#: working_time/working_time/workspace/time_tracking/time_tracking.json
msgid "Daily Billable Time (this month)"
@@ -146,21 +149,16 @@ msgstr "Tägliche Arbeitsstunden"
msgid "Daily Working Time"
msgstr "Tägliche Arbeitszeit"
-#: working_time/reminders.py:138
+#: working_time/reminders.py:146
msgid ""
"Dear {first_name},\n"
"\n"
-"Your have a draft working time entry that is older than {cutoff_days} days. Please submit it as soon as possible.\n"
+"You have a draft working time entry that is older than {cutoff_days} days. Please submit it as soon as possible.\n"
"\n"
"Thanks in advance!"
msgstr ""
-"Hallo {first_name},\n"
-"\n"
-"du hast einen Arbeitszeiteintrag im Entwurf, der älter als {cutoff_days} Tage ist. Bitte reiche ihn so bald wie möglich ein.\n"
-"\n"
-"Vielen Dank!"
-#: working_time/reminders.py:63
+#: working_time/reminders.py:66
msgid ""
"Dear {first_name},\n"
"\n"
@@ -174,6 +172,11 @@ msgstr ""
"\n"
"Vielen Dank!"
+#. Description of the 'Paid' (Check) field in DocType 'Working Time Log'
+#: working_time/working_time/doctype/working_time_log/working_time_log.json
+msgid "Enable to count this break as paid working time (e.g. mandatory travel time)"
+msgstr "Aktivieren, um diese Pause als bezahlte Arbeitszeit zu zählen (z.B. Pflichtreisezeit)"
+
#: working_time/working_time/report/expected_and_actual_working_time/expected_and_actual_working_time.py:58
msgid "Expected Working Time"
msgstr "Erwartete Arbeitszeit"
@@ -242,8 +245,8 @@ msgstr "Pflichtpausen"
#. Label of a Duration field in DocType 'Working Time Policy'
#: working_time/working_time/doctype/working_time_policy/working_time_policy.json
-msgid "Max Working Time Per Day"
-msgstr "Maximale Arbeitszeit pro Tag"
+msgid "Max Productive Time Per Day"
+msgstr "Maximale produktive Arbeitszeit pro Tag"
#. Label of a Duration field in DocType 'Working Time Policy'
#: working_time/working_time/doctype/working_time_policy/working_time_policy.json
@@ -268,14 +271,37 @@ msgstr "Nicht Plausibel"
msgid "Only submitted working times are considered in this table."
msgstr "Nur gebuchte Arbeitszeiten werden in dieser Tabelle berücksichtigt."
-#: working_time/working_time/doctype/working_time/working_time.py:55
+#. Label of a Duration field in DocType 'Working Time'
+#: working_time/working_time/doctype/working_time/working_time.json
+msgid "Paid Break Time"
+msgstr "Bezahlte Pausenzeit"
+
+#. Label of a Duration field in DocType 'Working Time'
+#: working_time/working_time/doctype/working_time/working_time.json
+msgid "Paid Working Time"
+msgstr "Bezahlte Arbeitszeit"
+
+#: working_time/working_time/doctype/working_time/working_time.py:64
msgid "Please add an issue key or invoice note to the billable row {0}"
msgstr "Bitte einen Issue-Key oder eine Rechnungsnotiz zur abrechenbaren Zeile {0} hinzufügen"
-#: working_time/working_time/doctype/working_time/working_time.py:46
+#: working_time/working_time/doctype/working_time/working_time.py:55
msgid "Please fix negative duration in row {0}"
msgstr "Bitte negative Dauer in Zeile {0} korrigieren"
+#. Label of a Duration field in DocType 'Working Time'
+#: working_time/working_time/doctype/working_time/working_time.json
+msgid "Productive Time"
+msgstr "Produktive Arbeitszeit"
+
+#: working_time/working_time/doctype/working_time/working_time.py:116
+msgid "Productive time ({0}) exceeds the maximum allowed ({1}) per day"
+msgstr "Produktive Arbeitszeit ({0}) überschreitet die maximal erlaubte ({1}) pro Tag"
+
+#: working_time/working_time/doctype/working_time/working_time.py:129
+msgid "Productive time of {0} or more requires at least {1} of break time"
+msgstr "Produktive Arbeitszeit von {0} oder mehr benötigt mindestens {1} Pausenzeit"
+
#. Label of a Percent field in DocType 'Working Time'
#: working_time/working_time/doctype/working_time/working_time.json
msgid "Project %"
@@ -286,7 +312,7 @@ msgstr "Projekt %"
msgid "Project Time"
msgstr "Projektzeit"
-#: working_time/reminders.py:62 working_time/reminders.py:137
+#: working_time/reminders.py:65 working_time/reminders.py:145
msgid "Remember to submit your working time"
msgstr "Erinnerung: Arbeitszeit einreichen"
@@ -295,7 +321,7 @@ msgstr "Erinnerung: Arbeitszeit einreichen"
msgid "Required Break Minutes"
msgstr "Erforderliche Pausenminuten"
-#: working_time/working_time/doctype/working_time/working_time.py:164
+#: working_time/working_time/doctype/working_time/working_time.py:173
msgid "Rest time since previous day ({0}) is less than the required minimum ({1})"
msgstr "Die Ruhezeit seit dem Vortag ({0}) ist kürzer als das erforderliche Minimum ({1})"
@@ -332,7 +358,6 @@ msgid "Work Threshold"
msgstr "Arbeitsschwelle"
#. Name of a DocType
-#. Label of a Duration field in DocType 'Working Time'
#. Label of a Link in the Time Tracking Workspace
#: working_time/config/desktop.py:11
#: working_time/working_time/doctype/working_time/working_time.json
@@ -370,24 +395,16 @@ msgstr "Arbeitszeitrichtlinie"
msgid "Working Time Summary"
msgstr "Arbeitszeit Zusammenfassung"
-#: working_time/working_time/doctype/working_time/working_time.py:107
-msgid "Working time ({0}) exceeds the maximum allowed ({1}) per day"
-msgstr "Die Arbeitszeit ({0}) überschreitet das erlaubte Maximum ({1}) pro Tag"
-
-#: working_time/working_time/doctype/working_time/working_time.py:120
-msgid "Working time of {0} or more requires at least {1} of break time"
-msgstr "Eine Arbeitszeit von {0} oder mehr erfordert mindestens {1} Pausenzeit"
-
#. Description of the 'Site URL' (Data) field in DocType 'Jira Site'
#: working_time/working_time/doctype/jira_site/jira_site.json
msgid "e.g. your-domain.atlassian.net"
msgstr "z.B. your-domain.atlassian.net"
-#: working_time/working_time/doctype/working_time/working_time.py:80
+#: working_time/working_time/doctype/working_time/working_time.py:89
msgid "{0} is a blocked day according to the Working Time Policy"
msgstr "{0} ist laut Arbeitszeitrichtlinie ein gesperrter Tag"
-#: working_time/working_time/doctype/working_time/working_time.py:96
+#: working_time/working_time/doctype/working_time/working_time.py:105
msgid "{0} is a holiday according to your holiday list"
msgstr "{0} ist laut Ihrer Feiertagsliste ein Feiertag"
@@ -395,3 +412,4 @@ msgstr "{0} ist laut Ihrer Feiertagsliste ein Feiertag"
#: working_time/working_time/workspace/time_tracking/time_tracking.json
msgid "{} Drafts"
msgstr "{} Entwürfe"
+
diff --git a/working_time/locale/main.pot b/working_time/locale/main.pot
index d18b758..e5934c2 100644
--- a/working_time/locale/main.pot
+++ b/working_time/locale/main.pot
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: Working Time VERSION\n"
"Report-Msgid-Bugs-To: hallo@alyf.de\n"
-"POT-Creation-Date: 2026-03-27 14:57+0053\n"
-"PO-Revision-Date: 2026-03-27 14:57+0053\n"
+"POT-Creation-Date: 2026-05-26 23:08+0053\n"
+"PO-Revision-Date: 2026-05-26 23:08+0053\n"
"Last-Translator: hallo@alyf.de\n"
"Language-Team: hallo@alyf.de\n"
"MIME-Version: 1.0\n"
@@ -112,13 +112,16 @@ msgstr ""
msgid "Blocks real holidays from an employee's holiday list. Ignores weekly off."
msgstr ""
-#. Label of a Duration field in DocType 'Working Time'
#. Label of a Check field in DocType 'Working Time Log'
-#: working_time/working_time/doctype/working_time/working_time.json
#: working_time/working_time/doctype/working_time_log/working_time_log.json
msgid "Break"
msgstr ""
+#. Label of a Duration field in DocType 'Working Time'
+#: working_time/working_time/doctype/working_time/working_time.json
+msgid "Break Time"
+msgstr ""
+
#. Label of a number card in the Time Tracking Workspace
#: working_time/working_time/workspace/time_tracking/time_tracking.json
msgid "Daily Billable Time (this month)"
@@ -146,16 +149,16 @@ msgstr ""
msgid "Daily Working Time"
msgstr ""
-#: working_time/reminders.py:138
+#: working_time/reminders.py:146
msgid ""
"Dear {first_name},\n"
"\n"
-"Your have a draft working time entry that is older than {cutoff_days} days. Please submit it as soon as possible.\n"
+"You have a draft working time entry that is older than {cutoff_days} days. Please submit it as soon as possible.\n"
"\n"
"Thanks in advance!"
msgstr ""
-#: working_time/reminders.py:63
+#: working_time/reminders.py:66
msgid ""
"Dear {first_name},\n"
"\n"
@@ -164,6 +167,11 @@ msgid ""
"Thanks in advance!"
msgstr ""
+#. Description of the 'Paid' (Check) field in DocType 'Working Time Log'
+#: working_time/working_time/doctype/working_time_log/working_time_log.json
+msgid "Enable to count this break as paid working time (e.g. mandatory travel time)"
+msgstr ""
+
#: working_time/working_time/report/expected_and_actual_working_time/expected_and_actual_working_time.py:58
msgid "Expected Working Time"
msgstr ""
@@ -232,7 +240,7 @@ msgstr ""
#. Label of a Duration field in DocType 'Working Time Policy'
#: working_time/working_time/doctype/working_time_policy/working_time_policy.json
-msgid "Max Working Time Per Day"
+msgid "Max Productive Time Per Day"
msgstr ""
#. Label of a Duration field in DocType 'Working Time Policy'
@@ -258,14 +266,37 @@ msgstr ""
msgid "Only submitted working times are considered in this table."
msgstr ""
-#: working_time/working_time/doctype/working_time/working_time.py:55
+#. Label of a Duration field in DocType 'Working Time'
+#: working_time/working_time/doctype/working_time/working_time.json
+msgid "Paid Break Time"
+msgstr ""
+
+#. Label of a Duration field in DocType 'Working Time'
+#: working_time/working_time/doctype/working_time/working_time.json
+msgid "Paid Working Time"
+msgstr ""
+
+#: working_time/working_time/doctype/working_time/working_time.py:64
msgid "Please add an issue key or invoice note to the billable row {0}"
msgstr ""
-#: working_time/working_time/doctype/working_time/working_time.py:46
+#: working_time/working_time/doctype/working_time/working_time.py:55
msgid "Please fix negative duration in row {0}"
msgstr ""
+#. Label of a Duration field in DocType 'Working Time'
+#: working_time/working_time/doctype/working_time/working_time.json
+msgid "Productive Time"
+msgstr ""
+
+#: working_time/working_time/doctype/working_time/working_time.py:116
+msgid "Productive time ({0}) exceeds the maximum allowed ({1}) per day"
+msgstr ""
+
+#: working_time/working_time/doctype/working_time/working_time.py:129
+msgid "Productive time of {0} or more requires at least {1} of break time"
+msgstr ""
+
#. Label of a Percent field in DocType 'Working Time'
#: working_time/working_time/doctype/working_time/working_time.json
msgid "Project %"
@@ -276,7 +307,7 @@ msgstr ""
msgid "Project Time"
msgstr ""
-#: working_time/reminders.py:62 working_time/reminders.py:137
+#: working_time/reminders.py:65 working_time/reminders.py:145
msgid "Remember to submit your working time"
msgstr ""
@@ -285,7 +316,7 @@ msgstr ""
msgid "Required Break Minutes"
msgstr ""
-#: working_time/working_time/doctype/working_time/working_time.py:164
+#: working_time/working_time/doctype/working_time/working_time.py:173
msgid "Rest time since previous day ({0}) is less than the required minimum ({1})"
msgstr ""
@@ -322,7 +353,6 @@ msgid "Work Threshold"
msgstr ""
#. Name of a DocType
-#. Label of a Duration field in DocType 'Working Time'
#. Label of a Link in the Time Tracking Workspace
#: working_time/config/desktop.py:11
#: working_time/working_time/doctype/working_time/working_time.json
@@ -360,24 +390,16 @@ msgstr ""
msgid "Working Time Summary"
msgstr ""
-#: working_time/working_time/doctype/working_time/working_time.py:107
-msgid "Working time ({0}) exceeds the maximum allowed ({1}) per day"
-msgstr ""
-
-#: working_time/working_time/doctype/working_time/working_time.py:120
-msgid "Working time of {0} or more requires at least {1} of break time"
-msgstr ""
-
#. Description of the 'Site URL' (Data) field in DocType 'Jira Site'
#: working_time/working_time/doctype/jira_site/jira_site.json
msgid "e.g. your-domain.atlassian.net"
msgstr ""
-#: working_time/working_time/doctype/working_time/working_time.py:80
+#: working_time/working_time/doctype/working_time/working_time.py:89
msgid "{0} is a blocked day according to the Working Time Policy"
msgstr ""
-#: working_time/working_time/doctype/working_time/working_time.py:96
+#: working_time/working_time/doctype/working_time/working_time.py:105
msgid "{0} is a holiday according to your holiday list"
msgstr ""
From 657293793e015df2238a3b4494d80c566b579076 Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Tue, 26 May 2026 23:19:17 +0200
Subject: [PATCH 3/6] fix: hide productive time when identical to paid working
time
---
.../working_time/doctype/working_time/working_time.json | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/working_time/working_time/doctype/working_time/working_time.json b/working_time/working_time/doctype/working_time/working_time.json
index 25ac582..2621246 100644
--- a/working_time/working_time/doctype/working_time/working_time.json
+++ b/working_time/working_time/doctype/working_time/working_time.json
@@ -162,6 +162,7 @@
"fieldtype": "Column Break"
},
{
+ "depends_on": "eval: doc.productive_time !== doc.working_time",
"fieldname": "productive_time",
"fieldtype": "Duration",
"label": "Productive Time",
@@ -180,7 +181,7 @@
"link_fieldname": "working_time"
}
],
- "modified": "2026-05-26 21:24:36.464213",
+ "modified": "2026-05-26 23:16:47.703228",
"modified_by": "Administrator",
"module": "Working Time",
"name": "Working Time",
From 02b375581363b5ffc665b99a7cbcfc83233c5fcb Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Tue, 26 May 2026 23:20:20 +0200
Subject: [PATCH 4/6] fix: hide days and seconds on new fields
---
.../working_time/doctype/working_time/working_time.json | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/working_time/working_time/doctype/working_time/working_time.json b/working_time/working_time/doctype/working_time/working_time.json
index 2621246..5526be9 100644
--- a/working_time/working_time/doctype/working_time/working_time.json
+++ b/working_time/working_time/doctype/working_time/working_time.json
@@ -154,6 +154,8 @@
{
"fieldname": "paid_break_time",
"fieldtype": "Duration",
+ "hide_days": 1,
+ "hide_seconds": 1,
"label": "Paid Break Time",
"read_only": 1
},
@@ -165,6 +167,8 @@
"depends_on": "eval: doc.productive_time !== doc.working_time",
"fieldname": "productive_time",
"fieldtype": "Duration",
+ "hide_days": 1,
+ "hide_seconds": 1,
"label": "Productive Time",
"read_only": 1
}
@@ -181,7 +185,7 @@
"link_fieldname": "working_time"
}
],
- "modified": "2026-05-26 23:16:47.703228",
+ "modified": "2026-05-26 23:20:02.508838",
"modified_by": "Administrator",
"module": "Working Time",
"name": "Working Time",
From a8174df658765ec3dc385484c0ed68102aa9f9e1 Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Tue, 26 May 2026 23:23:31 +0200
Subject: [PATCH 5/6] fix: restore translation that was accidentally removed
---
working_time/locale/de.po | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/working_time/locale/de.po b/working_time/locale/de.po
index 7785773..5d22390 100644
--- a/working_time/locale/de.po
+++ b/working_time/locale/de.po
@@ -157,6 +157,11 @@ msgid ""
"\n"
"Thanks in advance!"
msgstr ""
+"Hallo {first_name},\n"
+"\n"
+"du hast einen Arbeitszeiteintrag im Entwurf, der älter als {cutoff_days} Tage ist. Bitte reiche ihn so bald wie möglich ein.\n"
+"\n"
+"Vielen Dank!"
#: working_time/reminders.py:66
msgid ""
From 642b5cc73e28e4c1e03f89b0ca5289cd6c46c94a Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Tue, 26 May 2026 23:42:48 +0200
Subject: [PATCH 6/6] docs: update README
---
README.md | 22 +++++++++++++++++++---
1 file changed, 19 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index 92eae44..ce830e1 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@ Companies that use Atlassian Jira for project management and ERPNext for time tr
## Features
-- Allows logging of miscellanous time, project time and breaks
+- Allows logging of miscellaneous time, project time, breaks and paid breaks
- Allows to set a percentage of working time as billable time in a Working Time Log
- Rounds billable time to 5 minutes
- Fetches issue titles from Jira (used as time log description)
@@ -17,8 +17,8 @@ Companies that use Atlassian Jira for project management and ERPNext for time tr
- If a draft working time entry is older than 3 days, and
- on the last working day of the month
- **Working Time Policy** enforcement per employee, including:
- - Maximum working time per day
- - Mandatory break requirements based on working time thresholds
+ - Maximum productive time per day
+ - Mandatory break requirements based on productive time thresholds
- Minimum rest time between days
- Blocked weekdays
- Holiday blocking (based on the employee's holiday list)
@@ -45,6 +45,22 @@ Companies that use Atlassian Jira for project management and ERPNext for time tr
- Add a time log and link it to a _Project_ and Jira issue _Key_
- Submit your **Working Time**
+## Time Fields
+
+**Working Time** separates productive time, break time and paid time:
+
+- _Productive Time_ (`productive_time`) is the total duration of logs that are not marked as breaks.
+- _Break Time_ (`break_time`) is the total duration of logs marked as breaks, including paid breaks.
+- _Paid Break Time_ (`paid_break_time`) is the total duration of break logs where _Paid_ (`is_paid_break`) is enabled.
+- _Paid Working Time_ (`working_time`) is the paid total: _Productive Time_ plus _Paid Break Time_. Number cards, stats and reports use this field as the paid working time total.
+- _Project Time_ (`project_time`) and _Billable Time_ (`billable_time`) are calculated from non-break project logs.
+
+In a **Working Time Log**, mark _Break_ (`is_break`) for any physical break. Regular working logs are paid by default and should not be marked as breaks. Regular breaks are unpaid by default. Use _Paid_ (`is_paid_break`) only for exceptional break rows that should count toward paid working time, such as mandatory but passive travel time.
+
+**Working Time Policy** restrictions use _Productive Time_ for maximum time and threshold checks, while mandatory break requirements use _Break Time_. This means paid breaks count as paid time, but do not increase productive time for policy restrictions.
+
+German users may refer to [this article](https://www.kanzlei-chevalier.de/blog/dienstreise-als-arbeitszeit) for more information.
+
## Further Reading
Want to add pretty time logs to your invoice? Check out our [print formats](https://github.com/alyf-de/erpnext_druckformate).