From c5f5aa8208ce2449e626cc0e4cf120f30b0260b9 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Wed, 1 Nov 2023 11:19:21 +0100 Subject: [PATCH] feat: report Timesheet Billing Summary (#37451) --- erpnext/projects/report/billing_summary.py | 155 ------------------ .../employee_billing_summary.js | 34 ---- .../employee_billing_summary.json | 36 ---- .../employee_billing_summary.py | 15 -- .../project_billing_summary/__init__.py | 0 .../project_billing_summary.js | 34 ---- .../project_billing_summary.py | 15 -- .../__init__.py | 0 .../timesheet_billing_summary.js | 67 ++++++++ .../timesheet_billing_summary.json} | 22 ++- .../timesheet_billing_summary.py | 146 +++++++++++++++++ .../projects/workspace/projects/projects.json | 10 +- 12 files changed, 232 insertions(+), 302 deletions(-) delete mode 100644 erpnext/projects/report/billing_summary.py delete mode 100644 erpnext/projects/report/employee_billing_summary/employee_billing_summary.js delete mode 100644 erpnext/projects/report/employee_billing_summary/employee_billing_summary.json delete mode 100644 erpnext/projects/report/employee_billing_summary/employee_billing_summary.py delete mode 100644 erpnext/projects/report/project_billing_summary/__init__.py delete mode 100644 erpnext/projects/report/project_billing_summary/project_billing_summary.js delete mode 100644 erpnext/projects/report/project_billing_summary/project_billing_summary.py rename erpnext/projects/report/{employee_billing_summary => timesheet_billing_summary}/__init__.py (100%) create mode 100644 erpnext/projects/report/timesheet_billing_summary/timesheet_billing_summary.js rename erpnext/projects/report/{project_billing_summary/project_billing_summary.json => timesheet_billing_summary/timesheet_billing_summary.json} (61%) create mode 100644 erpnext/projects/report/timesheet_billing_summary/timesheet_billing_summary.py diff --git a/erpnext/projects/report/billing_summary.py b/erpnext/projects/report/billing_summary.py deleted file mode 100644 index ac1524a49d..0000000000 --- a/erpnext/projects/report/billing_summary.py +++ /dev/null @@ -1,155 +0,0 @@ -# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -import frappe -from frappe import _ -from frappe.utils import flt, time_diff_in_hours - - -def get_columns(): - return [ - { - "label": _("Employee ID"), - "fieldtype": "Link", - "fieldname": "employee", - "options": "Employee", - "width": 300, - }, - { - "label": _("Employee Name"), - "fieldtype": "data", - "fieldname": "employee_name", - "hidden": 1, - "width": 200, - }, - { - "label": _("Timesheet"), - "fieldtype": "Link", - "fieldname": "timesheet", - "options": "Timesheet", - "width": 150, - }, - {"label": _("Working Hours"), "fieldtype": "Float", "fieldname": "total_hours", "width": 150}, - { - "label": _("Billable Hours"), - "fieldtype": "Float", - "fieldname": "total_billable_hours", - "width": 150, - }, - {"label": _("Billing Amount"), "fieldtype": "Currency", "fieldname": "amount", "width": 150}, - ] - - -def get_data(filters): - data = [] - if filters.from_date > filters.to_date: - frappe.msgprint(_("From Date can not be greater than To Date")) - return data - - timesheets = get_timesheets(filters) - - filters.from_date = frappe.utils.get_datetime(filters.from_date) - filters.to_date = frappe.utils.add_to_date( - frappe.utils.get_datetime(filters.to_date), days=1, seconds=-1 - ) - - timesheet_details = get_timesheet_details(filters, timesheets.keys()) - - for ts, ts_details in timesheet_details.items(): - total_hours = 0 - total_billing_hours = 0 - total_amount = 0 - - for row in ts_details: - from_time, to_time = filters.from_date, filters.to_date - - if row.to_time < from_time or row.from_time > to_time: - continue - - if row.from_time > from_time: - from_time = row.from_time - - if row.to_time < to_time: - to_time = row.to_time - - activity_duration, billing_duration = get_billable_and_total_duration(row, from_time, to_time) - - total_hours += activity_duration - total_billing_hours += billing_duration - total_amount += billing_duration * flt(row.billing_rate) - - if total_hours: - data.append( - { - "employee": timesheets.get(ts).employee, - "employee_name": timesheets.get(ts).employee_name, - "timesheet": ts, - "total_billable_hours": total_billing_hours, - "total_hours": total_hours, - "amount": total_amount, - } - ) - - return data - - -def get_timesheets(filters): - record_filters = [ - ["start_date", "<=", filters.to_date], - ["end_date", ">=", filters.from_date], - ] - if not filters.get("include_draft_timesheets"): - record_filters.append(["docstatus", "=", 1]) - else: - record_filters.append(["docstatus", "!=", 2]) - if "employee" in filters: - record_filters.append(["employee", "=", filters.employee]) - - timesheets = frappe.get_all( - "Timesheet", filters=record_filters, fields=["employee", "employee_name", "name"] - ) - timesheet_map = frappe._dict() - for d in timesheets: - timesheet_map.setdefault(d.name, d) - - return timesheet_map - - -def get_timesheet_details(filters, timesheet_list): - timesheet_details_filter = {"parent": ["in", timesheet_list]} - - if "project" in filters: - timesheet_details_filter["project"] = filters.project - - timesheet_details = frappe.get_all( - "Timesheet Detail", - filters=timesheet_details_filter, - fields=[ - "from_time", - "to_time", - "hours", - "is_billable", - "billing_hours", - "billing_rate", - "parent", - ], - ) - - timesheet_details_map = frappe._dict() - for d in timesheet_details: - timesheet_details_map.setdefault(d.parent, []).append(d) - - return timesheet_details_map - - -def get_billable_and_total_duration(activity, start_time, end_time): - precision = frappe.get_precision("Timesheet Detail", "hours") - activity_duration = time_diff_in_hours(end_time, start_time) - billing_duration = 0.0 - if activity.is_billable: - billing_duration = activity.billing_hours - if activity_duration != activity.billing_hours: - billing_duration = activity_duration * activity.billing_hours / activity.hours - - return flt(activity_duration, precision), flt(billing_duration, precision) diff --git a/erpnext/projects/report/employee_billing_summary/employee_billing_summary.js b/erpnext/projects/report/employee_billing_summary/employee_billing_summary.js deleted file mode 100644 index 2c25465a61..0000000000 --- a/erpnext/projects/report/employee_billing_summary/employee_billing_summary.js +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - - -frappe.query_reports["Employee Billing Summary"] = { - "filters": [ - { - fieldname: "employee", - label: __("Employee"), - fieldtype: "Link", - options: "Employee", - reqd: 1 - }, - { - fieldname:"from_date", - label: __("From Date"), - fieldtype: "Date", - default: frappe.datetime.add_months(frappe.datetime.month_start(), -1), - reqd: 1 - }, - { - fieldname:"to_date", - label: __("To Date"), - fieldtype: "Date", - default: frappe.datetime.add_days(frappe.datetime.month_start(), -1), - reqd: 1 - }, - { - fieldname:"include_draft_timesheets", - label: __("Include Timesheets in Draft Status"), - fieldtype: "Check", - }, - ] -} diff --git a/erpnext/projects/report/employee_billing_summary/employee_billing_summary.json b/erpnext/projects/report/employee_billing_summary/employee_billing_summary.json deleted file mode 100644 index e5626a0206..0000000000 --- a/erpnext/projects/report/employee_billing_summary/employee_billing_summary.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "add_total_row": 1, - "creation": "2019-03-08 15:08:19.929728", - "disable_prepared_report": 0, - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 0, - "is_standard": "Yes", - "modified": "2019-06-13 15:54:49.213973", - "modified_by": "Administrator", - "module": "Projects", - "name": "Employee Billing Summary", - "owner": "Administrator", - "prepared_report": 0, - "ref_doctype": "Timesheet", - "report_name": "Employee Billing Summary", - "report_type": "Script Report", - "roles": [ - { - "role": "Projects User" - }, - { - "role": "HR User" - }, - { - "role": "Manufacturing User" - }, - { - "role": "Employee" - }, - { - "role": "Accounts User" - } - ] -} \ No newline at end of file diff --git a/erpnext/projects/report/employee_billing_summary/employee_billing_summary.py b/erpnext/projects/report/employee_billing_summary/employee_billing_summary.py deleted file mode 100644 index a2f7378d1b..0000000000 --- a/erpnext/projects/report/employee_billing_summary/employee_billing_summary.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -import frappe - -from erpnext.projects.report.billing_summary import get_columns, get_data - - -def execute(filters=None): - filters = frappe._dict(filters or {}) - columns = get_columns() - - data = get_data(filters) - return columns, data diff --git a/erpnext/projects/report/project_billing_summary/__init__.py b/erpnext/projects/report/project_billing_summary/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/projects/report/project_billing_summary/project_billing_summary.js b/erpnext/projects/report/project_billing_summary/project_billing_summary.js deleted file mode 100644 index fce0c68f11..0000000000 --- a/erpnext/projects/report/project_billing_summary/project_billing_summary.js +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - - -frappe.query_reports["Project Billing Summary"] = { - "filters": [ - { - fieldname: "project", - label: __("Project"), - fieldtype: "Link", - options: "Project", - reqd: 1 - }, - { - fieldname:"from_date", - label: __("From Date"), - fieldtype: "Date", - default: frappe.datetime.add_months(frappe.datetime.month_start(), -1), - reqd: 1 - }, - { - fieldname:"to_date", - label: __("To Date"), - fieldtype: "Date", - default: frappe.datetime.add_days(frappe.datetime.month_start(),-1), - reqd: 1 - }, - { - fieldname:"include_draft_timesheets", - label: __("Include Timesheets in Draft Status"), - fieldtype: "Check", - }, - ] -} diff --git a/erpnext/projects/report/project_billing_summary/project_billing_summary.py b/erpnext/projects/report/project_billing_summary/project_billing_summary.py deleted file mode 100644 index a2f7378d1b..0000000000 --- a/erpnext/projects/report/project_billing_summary/project_billing_summary.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -import frappe - -from erpnext.projects.report.billing_summary import get_columns, get_data - - -def execute(filters=None): - filters = frappe._dict(filters or {}) - columns = get_columns() - - data = get_data(filters) - return columns, data diff --git a/erpnext/projects/report/employee_billing_summary/__init__.py b/erpnext/projects/report/timesheet_billing_summary/__init__.py similarity index 100% rename from erpnext/projects/report/employee_billing_summary/__init__.py rename to erpnext/projects/report/timesheet_billing_summary/__init__.py diff --git a/erpnext/projects/report/timesheet_billing_summary/timesheet_billing_summary.js b/erpnext/projects/report/timesheet_billing_summary/timesheet_billing_summary.js new file mode 100644 index 0000000000..1efd0c6733 --- /dev/null +++ b/erpnext/projects/report/timesheet_billing_summary/timesheet_billing_summary.js @@ -0,0 +1,67 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.query_reports["Timesheet Billing Summary"] = { + tree: true, + initial_depth: 0, + filters: [ + { + fieldname: "employee", + label: __("Employee"), + fieldtype: "Link", + options: "Employee", + on_change: function (report) { + unset_group_by(report, "employee"); + }, + }, + { + fieldname: "project", + label: __("Project"), + fieldtype: "Link", + options: "Project", + on_change: function (report) { + unset_group_by(report, "project"); + }, + }, + { + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: frappe.datetime.add_months( + frappe.datetime.month_start(), + -1 + ), + }, + { + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + default: frappe.datetime.add_days( + frappe.datetime.month_start(), + -1 + ), + }, + { // NOTE: `update_group_by_options` expects this filter to be the fifth in the list + fieldname: "group_by", + label: __("Group By"), + fieldtype: "Select", + options: [ + "", + { value: "employee", label: __("Employee") }, + { value: "project", label: __("Project") }, + { value: "date", label: __("Start Date") }, + ], + }, + { + fieldname: "include_draft_timesheets", + label: __("Include Timesheets in Draft Status"), + fieldtype: "Check", + }, + ], +}; + +function unset_group_by(report, fieldname) { + if (report.get_filter_value(fieldname) && report.get_filter_value("group_by") == fieldname) { + report.set_filter_value("group_by", ""); + } +} diff --git a/erpnext/projects/report/project_billing_summary/project_billing_summary.json b/erpnext/projects/report/timesheet_billing_summary/timesheet_billing_summary.json similarity index 61% rename from erpnext/projects/report/project_billing_summary/project_billing_summary.json rename to erpnext/projects/report/timesheet_billing_summary/timesheet_billing_summary.json index 817d0cdb66..0f070cb457 100644 --- a/erpnext/projects/report/project_billing_summary/project_billing_summary.json +++ b/erpnext/projects/report/timesheet_billing_summary/timesheet_billing_summary.json @@ -1,36 +1,42 @@ { "add_total_row": 1, - "creation": "2019-03-11 16:22:39.460524", - "disable_prepared_report": 0, + "columns": [], + "creation": "2023-10-10 23:53:43.692067", "disabled": 0, "docstatus": 0, "doctype": "Report", + "filters": [], "idx": 0, "is_standard": "Yes", - "modified": "2019-06-13 15:54:55.255947", + "letter_head": "ALYF GmbH", + "letterhead": null, + "modified": "2023-10-11 00:58:30.639078", "modified_by": "Administrator", "module": "Projects", - "name": "Project Billing Summary", + "name": "Timesheet Billing Summary", "owner": "Administrator", "prepared_report": 0, "ref_doctype": "Timesheet", - "report_name": "Project Billing Summary", + "report_name": "Timesheet Billing Summary", "report_type": "Script Report", "roles": [ { "role": "Projects User" }, { - "role": "HR User" + "role": "Employee" + }, + { + "role": "Accounts User" }, { "role": "Manufacturing User" }, { - "role": "Employee" + "role": "HR User" }, { - "role": "Accounts User" + "role": "Employee Self Service" } ] } \ No newline at end of file diff --git a/erpnext/projects/report/timesheet_billing_summary/timesheet_billing_summary.py b/erpnext/projects/report/timesheet_billing_summary/timesheet_billing_summary.py new file mode 100644 index 0000000000..a6e7150e41 --- /dev/null +++ b/erpnext/projects/report/timesheet_billing_summary/timesheet_billing_summary.py @@ -0,0 +1,146 @@ +import frappe +from frappe import _ +from frappe.model.docstatus import DocStatus + + +def execute(filters=None): + group_fieldname = filters.pop("group_by", None) + + filters = frappe._dict(filters or {}) + columns = get_columns(filters, group_fieldname) + + data = get_data(filters, group_fieldname) + return columns, data + + +def get_columns(filters, group_fieldname=None): + group_columns = { + "date": { + "label": _("Date"), + "fieldtype": "Date", + "fieldname": "date", + "width": 150, + }, + "project": { + "label": _("Project"), + "fieldtype": "Link", + "fieldname": "project", + "options": "Project", + "width": 200, + "hidden": int(bool(filters.get("project"))), + }, + "employee": { + "label": _("Employee ID"), + "fieldtype": "Link", + "fieldname": "employee", + "options": "Employee", + "width": 200, + "hidden": int(bool(filters.get("employee"))), + }, + } + columns = [] + if group_fieldname: + columns.append(group_columns.get(group_fieldname)) + columns.extend( + column for column in group_columns.values() if column.get("fieldname") != group_fieldname + ) + else: + columns.extend(group_columns.values()) + + columns.extend( + [ + { + "label": _("Employee Name"), + "fieldtype": "data", + "fieldname": "employee_name", + "hidden": 1, + }, + { + "label": _("Timesheet"), + "fieldtype": "Link", + "fieldname": "timesheet", + "options": "Timesheet", + "width": 150, + }, + {"label": _("Working Hours"), "fieldtype": "Float", "fieldname": "hours", "width": 150}, + { + "label": _("Billing Hours"), + "fieldtype": "Float", + "fieldname": "billing_hours", + "width": 150, + }, + { + "label": _("Billing Amount"), + "fieldtype": "Currency", + "fieldname": "billing_amount", + "width": 150, + }, + ] + ) + + return columns + + +def get_data(filters, group_fieldname=None): + _filters = [] + if filters.get("employee"): + _filters.append(("employee", "=", filters.get("employee"))) + if filters.get("project"): + _filters.append(("Timesheet Detail", "project", "=", filters.get("project"))) + if filters.get("from_date"): + _filters.append(("Timesheet Detail", "from_time", ">=", filters.get("from_date"))) + if filters.get("to_date"): + _filters.append(("Timesheet Detail", "to_time", "<=", filters.get("to_date"))) + if not filters.get("include_draft_timesheets"): + _filters.append(("docstatus", "=", DocStatus.submitted())) + else: + _filters.append(("docstatus", "in", (DocStatus.submitted(), DocStatus.draft()))) + + data = frappe.get_list( + "Timesheet", + fields=[ + "name as timesheet", + "`tabTimesheet`.employee", + "`tabTimesheet`.employee_name", + "`tabTimesheet Detail`.from_time as date", + "`tabTimesheet Detail`.project", + "`tabTimesheet Detail`.hours", + "`tabTimesheet Detail`.billing_hours", + "`tabTimesheet Detail`.billing_amount", + ], + filters=_filters, + order_by="`tabTimesheet Detail`.from_time", + ) + + return group_by(data, group_fieldname) if group_fieldname else data + + +def group_by(data, fieldname): + groups = {row.get(fieldname) for row in data} + grouped_data = [] + for group in sorted(groups): + group_row = { + fieldname: group, + "hours": sum(row.get("hours") for row in data if row.get(fieldname) == group), + "billing_hours": sum(row.get("billing_hours") for row in data if row.get(fieldname) == group), + "billing_amount": sum(row.get("billing_amount") for row in data if row.get(fieldname) == group), + "indent": 0, + "is_group": 1, + } + if fieldname == "employee": + group_row["employee_name"] = next( + row.get("employee_name") for row in data if row.get(fieldname) == group + ) + + grouped_data.append(group_row) + for row in data: + if row.get(fieldname) != group: + continue + + _row = row.copy() + _row[fieldname] = None + _row["indent"] = 1 + _row["is_group"] = 0 + grouped_data.append(_row) + + return grouped_data diff --git a/erpnext/projects/workspace/projects/projects.json b/erpnext/projects/workspace/projects/projects.json index 94ae9c04a4..e6bead9ff4 100644 --- a/erpnext/projects/workspace/projects/projects.json +++ b/erpnext/projects/workspace/projects/projects.json @@ -155,9 +155,9 @@ "dependencies": "Project", "hidden": 0, "is_query_report": 1, - "label": "Project Billing Summary", + "label": "Timesheet Billing Summary", "link_count": 0, - "link_to": "Project Billing Summary", + "link_to": "Timesheet Billing Summary", "link_type": "Report", "onboard": 0, "type": "Link" @@ -192,7 +192,7 @@ "type": "Link" } ], - "modified": "2023-07-04 14:39:08.935853", + "modified": "2023-10-10 23:54:33.082108", "modified_by": "Administrator", "module": "Projects", "name": "Projects", @@ -234,8 +234,8 @@ "type": "DocType" }, { - "label": "Project Billing Summary", - "link_to": "Project Billing Summary", + "label": "Timesheet Billing Summary", + "link_to": "Timesheet Billing Summary", "type": "Report" }, {