feat: report Timesheet Billing Summary (#37451)
This commit is contained in:
parent
7e67d42d1d
commit
c5f5aa8208
@ -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)
|
@ -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",
|
||||
},
|
||||
]
|
||||
}
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
@ -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
|
@ -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",
|
||||
},
|
||||
]
|
||||
}
|
@ -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
|
@ -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", "");
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
@ -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
|
@ -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"
|
||||
},
|
||||
{
|
||||
|
Loading…
x
Reference in New Issue
Block a user