Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 74 additions & 5 deletions README.md

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, or markup standard is:

  • DocType names in bold: e.g. Sales Invoice
  • Field labels in italics: e.g. Customer Name
  • Field names as inline code: e.g. customer_name
  • Field values in quotes: e.g. "Kunde GmbH"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

noted

Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,16 @@ Companies that use Atlassian Jira for project management and ERPNext for time tr
## Features

- 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
- Optional **Activity Type** on each project time log (defaults to `"Default"`)
- Creates separate **Timesheet** entries when the same project is logged with different activity types

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't separate Timesheet Detail rows be enough? If that's already the case, it could be clarified in the README.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, the timesheet notes (coming from the internal notes) would be merged on timesheet level, as they do not exist on Timesheet Detail level. I would opt against this change in this PR.

- Resolves billing and costing rates from **Activity Cost**, then **Project**, then **Activity Type**
- Optional project-specific **Activity Cost** records for different rates per project
- 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)
- Creates ERPNext **Timesheets**
- Creates ERPNext **Attendances**
- Whole-day project timesheets (single 8-hour **Timesheet** merged from multiple logs)
- Report of actual vs. expected working time per Employee
- Sends email reminders to employees for submitting their draft working time entries
- If a draft working time entry is older than 3 days, and
Expand All @@ -23,6 +28,58 @@ Companies that use Atlassian Jira for project management and ERPNext for time tr
- Blocked weekdays
- Holiday blocking (based on the employee's holiday list)

## Activity types and billing rates

This section explains how **Working Time** turns your daily logs into **Timesheet** rows with the correct rates.

### Activity Type on time logs

When a **Working Time Log** row is linked to a **Project**, you can choose an **Activity Type** (for example `"Default"` or `"Support"`). If you leave it empty, `"Default"` is used automatically.

Different activity types on the same day produce **separate Timesheet entries**, even when project, task, and Jira key are the same. That lets you bill or cost the same project at different rates within one day.

### How rates are chosen

When you submit **Working Time**, each **Timesheet** line gets a billing rate and a costing rate. They are resolved independently:

**Billing rate** (what the customer is charged):

1. **Activity Cost** for this employee, activity type, and project (if a matching record exists)
2. Otherwise **Activity Cost** for the same employee and activity type without a project
3. Otherwise the **Project** billing rate per hour
4. Otherwise the default rates on the **Activity Type**
5. Otherwise `0`

**Costing rate** (internal employee cost):

1. **Activity Cost** for this employee, activity type, and project (if a matching record exists)
2. Otherwise **Activity Cost** for the same employee and activity type without a project
3. Otherwise the default rates on the **Activity Type**
4. Otherwise `0`

Whole-day **Timesheet** entries use the **Project** billing rate per day (or per hour) for billing, but still use **Activity Cost** for costing.

### Project-specific Activity Cost

**Activity Cost** has an optional **Project** field. Use it when one employee should have different rates on different projects while keeping the same activity type.

Example:

| Employee | Activity Type | Project | Billing rate | Costing rate |
|----------|---------------|---------|--------------|--------------|
| Jane Doe | Default | *(empty)* | 100 | 45 |
| Jane Doe | Default | Client A | 120 | 50 |

When Jane logs time on Client A, the project-specific row is used. On any other project, the row without a project applies.

Each combination of employee, activity type, and project may exist only once. You can have one generic record (no project) and additional records for specific projects.

After upgrading an existing site, run `bench migrate` so the **Project** field is added to **Activity Cost**.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
After upgrading an existing site, run `bench migrate` so the **Project** field is added to **Activity Cost**.

Migrate is needed on every update regardless of this feature.


### Whole-day project

If **Working Time** has a **Whole Day Project** set, all logs on that project are merged into one 8-hour **Timesheet** when you submit. Every log on that project must use the **same activity type** (or all default to `"Default"`). Mixed activity types are rejected with an error instead of silently using only the first log's type.

## Setup

- Install this app
Expand All @@ -37,13 +94,25 @@ Companies that use Atlassian Jira for project management and ERPNext for time tr
- Enable _Ignore Employee Time Overlap_ and _Ignore User Time Overlap_ in **Projects Settings**
- Open or create an ERPNext **Project**
- Link it to your **Jira Site**
- Set the _Billing Rate per Hour_
- Create **Activity Cost** records for your **Employees** (_Activity Type_: "Default")
- Set the _Billing Rate per Hour_ (and optionally _Billing Rate per Day_ for whole-day billing)
- Create **Activity Type** records if you need types other than `"Default"` (installed automatically on first install)
- Create **Activity Cost** records for your **Employees**
- One row per employee and activity type without a project for your standard rates
- Optional extra rows with **Project** set for project-specific rates
- Optionally assign a **Working Time Policy** on each **Employee**
- Create your first **Working Time**
- Add a time log with description,
- Add a time log and mark it as a break,
- Add a time log and link it to a _Project_ and Jira issue _Key_
- Submit your **Working Time**
- Add a time log and link it to a _Project_, Jira issue _Key_, and optionally _Activity Type_

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- Add a time log and link it to a _Project_, Jira issue _Key_, and optionally _Activity Type_
- Add a time log and link it to a _Project_, Jira issue _Key_ or ERPNext _Task_, and optionally to an _Activity Type_.

- Submit your **Working Time** and review the draft **Timesheets** created for that day

### Quick checklist for rates

1. Create **Activity Type** records (e.g. `"Default"`, `"Support"`).
2. For each employee, create **Activity Cost** with activity type `"Default"` and your standard hourly rates.
3. Add project-specific **Activity Cost** rows only where rates differ from the standard.
4. Set **Billing Rate per Hour** on each **Project** as a fallback when no **Activity Cost** billing rate applies.
5. Submit a test **Working Time** with two logs on the same project but different activity types; confirm two **Timesheets** with the expected rates.

## Time Fields

Expand Down
21 changes: 18 additions & 3 deletions working_time/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
app_email = "hallo@alyf.de"
app_license = "-"

from working_time.install import ACTIVITY_COST_PROJECT_DESCRIPTION

# Includes in <head>
# ------------------

Expand Down Expand Up @@ -85,9 +87,9 @@
# ---------------
# Override standard doctype classes

# override_doctype_class = {
# "ToDo": "custom_app.overrides.CustomToDo"
# }
override_doctype_class = {
"Activity Cost": "working_time.overrides.activity_cost.WorkingTimeActivityCost",
}

# Document Events
# ---------------
Expand Down Expand Up @@ -280,4 +282,17 @@
"insert_after": "holiday_list",
}
],
"Activity Cost": [
{
"fieldname": "project",
"label": "Project",
"fieldtype": "Link",
"options": "Project",
"insert_after": "employee",
"in_list_view": 1,
"in_standard_filter": 1,
"translatable": 0,
"description": ACTIVITY_COST_PROJECT_DESCRIPTION,
},
],
}
7 changes: 7 additions & 0 deletions working_time/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
# For license information, please see license.txt

import frappe
from frappe import _
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields

ACTIVITY_COST_PROJECT_DESCRIPTION = "The filter by project is only available for the Working Time app."


def after_install():
make_custom_fields()
Expand Down Expand Up @@ -48,3 +51,7 @@ def update_projects_settings():
}
)
settings.save()


# Description of the 'Project' (Link) custom field on Activity Cost
_("The filter by project is only available for the Working Time app.")
66 changes: 45 additions & 21 deletions working_time/locale/de.po
Original file line number Diff line number Diff line change
Expand Up @@ -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-06-11 17:09+0053\n"
"POT-Creation-Date: 2026-06-30 08:09+0000\n"
"PO-Revision-Date: 2026-03-27 14:57+0053\n"
"Last-Translator: hallo@alyf.de\n"
"Language-Team: hallo@alyf.de\n"
Expand All @@ -19,37 +19,37 @@ msgstr ""
#. Option for the 'Billable' (Select) field in DocType 'Working Time Log'
#: working_time/working_time/doctype/working_time_log/working_time_log.json
msgid "0%"
msgstr ""
msgstr "0%"

#. Option for the 'Billable' (Select) field in DocType 'Working Time Log'
#: working_time/working_time/doctype/working_time_log/working_time_log.json
msgid "100%"
msgstr ""
msgstr "100%"

#. Option for the 'Billable' (Select) field in DocType 'Working Time Log'
#: working_time/working_time/doctype/working_time_log/working_time_log.json
msgid "125%"
msgstr ""
msgstr "125%"

#. Option for the 'Billable' (Select) field in DocType 'Working Time Log'
#: working_time/working_time/doctype/working_time_log/working_time_log.json
msgid "150%"
msgstr ""
msgstr "150%"

#. Option for the 'Billable' (Select) field in DocType 'Working Time Log'
#: working_time/working_time/doctype/working_time_log/working_time_log.json
msgid "25%"
msgstr ""
msgstr "25%"

#. Option for the 'Billable' (Select) field in DocType 'Working Time Log'
#: working_time/working_time/doctype/working_time_log/working_time_log.json
msgid "50%"
msgstr ""
msgstr "50%"

#. Option for the 'Billable' (Select) field in DocType 'Working Time Log'
#: working_time/working_time/doctype/working_time_log/working_time_log.json
msgid "75%"
msgstr ""
msgstr "75%"

#. Content of the 'note_explainer' (HTML) field in DocType 'Freelancer Time'
#. Content of the 'note_explainer' (HTML) field in DocType 'Working Time'
Expand All @@ -68,10 +68,22 @@ msgstr "<span class=\"h4\">Zeiterfassung</span>"
msgid "API Token"
msgstr "API-Token"

#: working_time/overrides/activity_cost.py:38
msgid "Activity Cost exists for Employee {0} against Activity Type {1}"
msgstr "Für Mitarbeiter {0} bestehen zur Aktivitätsart {1} Aktivitätskosten"

#: working_time/overrides/activity_cost.py:32
msgid "Activity Cost exists for Employee {0} against Activity Type {1} and Project {2}"
msgstr "Für Mitarbeiter {0} bestehen zur Aktivitätsart {1} und Projekt {2} Aktivitätskosten"

#: working_time/working_time/report/expected_and_actual_working_time/expected_and_actual_working_time.py:66
msgid "Actual Working Time"
msgstr "Tatsächliche Arbeitszeit"

#: working_time/working_time/doctype/working_time/working_time.py:292
msgid "All time logs for the whole day project {0} must use the same activity type."
msgstr "Alle Zeiteinträge für das Ganztagesprojekt {0} müssen dieselbe Aktivitätsart verwenden."

#. Label of a Select field in DocType 'Working Time Log'
#: working_time/working_time/doctype/working_time_log/working_time_log.json
msgid "Billable"
Expand Down Expand Up @@ -177,6 +189,10 @@ msgstr ""
"\n"
"Vielen Dank!"

#: working_time/overrides/activity_cost.py:45
msgid "Default Activity Cost exists for Activity Type {0}"
msgstr "Es gibt Standard-Aktivitätskosten für Aktivitätsart {0}"

#. 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)"
Expand Down Expand Up @@ -221,7 +237,7 @@ msgstr "Freelancer-Zeiteintrag"
#. Time'
#: working_time/working_time/doctype/working_time/working_time.json
msgid "If set, all time logs for this project will be merged into a single 8-hour timesheet."
msgstr "Wenn gesetzt, werden alle Einträge für dieses Projekt in ein 8-Stunden-Timesheet zusammengefasst."
msgstr "Wenn gesetzt, werden alle Einträge für dieses Projekt in eine 8-Stunden-Zeiterfassung zusammengefasst."

#. Description of the 'Note' (Small Text) field in DocType 'Working Time Log'
#: working_time/working_time/doctype/working_time_log/working_time_log.json
Expand All @@ -247,7 +263,7 @@ msgstr "Jira-Site"
#. Label of a Data field in DocType 'Working Time Log'
#: working_time/working_time/doctype/working_time_log/working_time_log.json
msgid "Jira-Key"
msgstr ""
msgstr "Jira-Key"

#. Label of a Table field in DocType 'Working Time Policy'
#: working_time/working_time/doctype/working_time_policy/working_time_policy.json
Expand All @@ -264,10 +280,14 @@ msgstr "Maximale produktive Arbeitszeit pro Tag"
msgid "Min Rest Between Days"
msgstr "Mindest-Ruhezeit zwischen Tagen"

#: working_time/working_time/doctype/working_time/working_time.py:188
#: working_time/working_time/doctype/working_time/working_time.py:186
msgid "Missing Time Log"
msgstr "Fehlender Zeiteintrag"

#: working_time/working_time/doctype/working_time/working_time.py:295
msgid "Mixed Activity Types"
msgstr "Gemischte Aktivitätsarten"

#. Label of a shortcut in the Time Tracking Workspace
#: working_time/working_time/workspace/time_tracking/time_tracking.json
msgid "New Freelancer Time"
Expand Down Expand Up @@ -296,15 +316,15 @@ msgstr "Bezahlte Pausenzeit"
msgid "Paid Working Time"
msgstr "Bezahlte Arbeitszeit"

#: working_time/working_time/doctype/working_time/working_time.py:65
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:63
msgid "Please add a task, Jira key, or external note to billable row {0}"
msgstr "Bitte Aufgabe, Jira-Key oder externe Notiz zur abrechenbaren Zeile {0} hinzufügen."

#: working_time/working_time/doctype/working_time/working_time.py:185
#: working_time/working_time/doctype/working_time/working_time.py:183
msgid "Please add at least one time log for the whole day project {0}."
msgstr "Bitte mindestens einen Zeiteintrag für das Ganztagesprojekt {0} hinzufügen."

#: working_time/working_time/doctype/working_time/working_time.py:56
#: working_time/working_time/doctype/working_time/working_time.py:59
msgid "Please fix negative duration in row {0}"
msgstr "Bitte negative Dauer in Zeile {0} korrigieren"

Expand All @@ -313,11 +333,11 @@ msgstr "Bitte negative Dauer in Zeile {0} korrigieren"
msgid "Productive Time"
msgstr "Produktive Arbeitszeit"

#: working_time/working_time/doctype/working_time/working_time.py:117
#: working_time/working_time/doctype/working_time/working_time.py:115
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:130
#: working_time/working_time/doctype/working_time/working_time.py:128
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"

Expand All @@ -340,7 +360,7 @@ msgstr "Erinnerung: Arbeitszeit einreichen"
msgid "Required Break Minutes"
msgstr "Erforderliche Pausenminuten"

#: working_time/working_time/doctype/working_time/working_time.py:174
#: working_time/working_time/doctype/working_time/working_time.py:172
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})"

Expand All @@ -366,6 +386,10 @@ msgstr "Das Datum in Zeile {0} liegt nach dem Enddatum."
msgid "The date in row {0} is before the start date."
msgstr "Das Datum in Zeile {0} liegt vor dem Startdatum."

#: working_time/install.py:57
msgid "The filter by project is only available for the Working Time app."
msgstr "Die Filterung nach Projekt ist nur in der Arbeitszeit-App verfügbar."

#. Label of a Duration field in DocType 'Freelancer Time'
#: working_time/working_time/doctype/freelancer_time/freelancer_time.json
msgid "Total Duration"
Expand Down Expand Up @@ -424,11 +448,11 @@ msgstr "Arbeitszeit Zusammenfassung"
msgid "e.g. your-domain.atlassian.net"
msgstr "z.B. your-domain.atlassian.net"

#: working_time/working_time/doctype/working_time/working_time.py:90
#: working_time/working_time/doctype/working_time/working_time.py:88
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:106
#: working_time/working_time/doctype/working_time/working_time.py:104
msgid "{0} is a holiday according to your holiday list"
msgstr "{0} ist laut Ihrer Feiertagsliste ein Feiertag"

Expand Down
Loading
Loading