mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-27 00:44:45 +00:00
style: format code with black
This commit is contained in:
@@ -2,7 +2,6 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import flt, time_diff_in_hours
|
||||
@@ -15,52 +14,45 @@ def get_columns():
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "employee",
|
||||
"options": "Employee",
|
||||
"width": 300
|
||||
"width": 300,
|
||||
},
|
||||
{
|
||||
"label": _("Employee Name"),
|
||||
"fieldtype": "data",
|
||||
"fieldname": "employee_name",
|
||||
"hidden": 1,
|
||||
"width": 200
|
||||
"width": 200,
|
||||
},
|
||||
{
|
||||
"label": _("Timesheet"),
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "timesheet",
|
||||
"options": "Timesheet",
|
||||
"width": 150
|
||||
},
|
||||
{
|
||||
"label": _("Working Hours"),
|
||||
"fieldtype": "Float",
|
||||
"fieldname": "total_hours",
|
||||
"width": 150
|
||||
"width": 150,
|
||||
},
|
||||
{"label": _("Working Hours"), "fieldtype": "Float", "fieldname": "total_hours", "width": 150},
|
||||
{
|
||||
"label": _("Billable Hours"),
|
||||
"fieldtype": "Float",
|
||||
"fieldname": "total_billable_hours",
|
||||
"width": 150
|
||||
"width": 150,
|
||||
},
|
||||
{
|
||||
"label": _("Billing Amount"),
|
||||
"fieldtype": "Currency",
|
||||
"fieldname": "amount",
|
||||
"width": 150
|
||||
}
|
||||
{"label": _("Billing Amount"), "fieldtype": "Currency", "fieldname": "amount", "width": 150},
|
||||
]
|
||||
|
||||
|
||||
def get_data(filters):
|
||||
data = []
|
||||
if(filters.from_date > filters.to_date):
|
||||
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)
|
||||
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())
|
||||
|
||||
@@ -88,46 +80,58 @@ def get_data(filters):
|
||||
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
|
||||
})
|
||||
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],
|
||||
["docstatus", "=", 1]
|
||||
]
|
||||
["start_date", "<=", filters.to_date],
|
||||
["end_date", ">=", filters.from_date],
|
||||
["docstatus", "=", 1],
|
||||
]
|
||||
|
||||
if "employee" in filters:
|
||||
record_filters.append(["employee", "=", filters.employee])
|
||||
|
||||
timesheets = frappe.get_all("Timesheet", filters=record_filters, fields=["employee", "employee_name", "name"])
|
||||
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]
|
||||
}
|
||||
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"]
|
||||
filters=timesheet_details_filter,
|
||||
fields=[
|
||||
"from_time",
|
||||
"to_time",
|
||||
"hours",
|
||||
"is_billable",
|
||||
"billing_hours",
|
||||
"billing_rate",
|
||||
"parent",
|
||||
],
|
||||
)
|
||||
|
||||
timesheet_details_map = frappe._dict()
|
||||
@@ -136,6 +140,7 @@ def get_timesheet_details(filters, timesheet_list):
|
||||
|
||||
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)
|
||||
|
||||
@@ -20,21 +20,37 @@ def execute(filters=None):
|
||||
|
||||
return columns, data
|
||||
|
||||
|
||||
def get_column():
|
||||
return [_("Timesheet") + ":Link/Timesheet:120", _("Employee") + "::150", _("Employee Name") + "::150",
|
||||
_("From Datetime") + "::140", _("To Datetime") + "::140", _("Hours") + "::70",
|
||||
_("Activity Type") + "::120", _("Task") + ":Link/Task:150",
|
||||
_("Project") + ":Link/Project:120", _("Status") + "::70"]
|
||||
return [
|
||||
_("Timesheet") + ":Link/Timesheet:120",
|
||||
_("Employee") + "::150",
|
||||
_("Employee Name") + "::150",
|
||||
_("From Datetime") + "::140",
|
||||
_("To Datetime") + "::140",
|
||||
_("Hours") + "::70",
|
||||
_("Activity Type") + "::120",
|
||||
_("Task") + ":Link/Task:150",
|
||||
_("Project") + ":Link/Project:120",
|
||||
_("Status") + "::70",
|
||||
]
|
||||
|
||||
|
||||
def get_data(conditions, filters):
|
||||
time_sheet = frappe.db.sql(""" select `tabTimesheet`.name, `tabTimesheet`.employee, `tabTimesheet`.employee_name,
|
||||
time_sheet = frappe.db.sql(
|
||||
""" select `tabTimesheet`.name, `tabTimesheet`.employee, `tabTimesheet`.employee_name,
|
||||
`tabTimesheet Detail`.from_time, `tabTimesheet Detail`.to_time, `tabTimesheet Detail`.hours,
|
||||
`tabTimesheet Detail`.activity_type, `tabTimesheet Detail`.task, `tabTimesheet Detail`.project,
|
||||
`tabTimesheet`.status from `tabTimesheet Detail`, `tabTimesheet` where
|
||||
`tabTimesheet Detail`.parent = `tabTimesheet`.name and %s order by `tabTimesheet`.name"""%(conditions), filters, as_list=1)
|
||||
`tabTimesheet Detail`.parent = `tabTimesheet`.name and %s order by `tabTimesheet`.name"""
|
||||
% (conditions),
|
||||
filters,
|
||||
as_list=1,
|
||||
)
|
||||
|
||||
return time_sheet
|
||||
|
||||
|
||||
def get_conditions(filters):
|
||||
conditions = "`tabTimesheet`.docstatus = 1"
|
||||
if filters.get("from_date"):
|
||||
|
||||
@@ -13,14 +13,24 @@ def execute(filters=None):
|
||||
charts = get_chart_data(data)
|
||||
return columns, data, None, charts
|
||||
|
||||
|
||||
def get_data(filters):
|
||||
conditions = get_conditions(filters)
|
||||
tasks = frappe.get_all("Task",
|
||||
filters = conditions,
|
||||
fields = ["name", "subject", "exp_start_date", "exp_end_date",
|
||||
"status", "priority", "completed_on", "progress"],
|
||||
order_by="creation"
|
||||
)
|
||||
tasks = frappe.get_all(
|
||||
"Task",
|
||||
filters=conditions,
|
||||
fields=[
|
||||
"name",
|
||||
"subject",
|
||||
"exp_start_date",
|
||||
"exp_end_date",
|
||||
"status",
|
||||
"priority",
|
||||
"completed_on",
|
||||
"progress",
|
||||
],
|
||||
order_by="creation",
|
||||
)
|
||||
for task in tasks:
|
||||
if task.exp_end_date:
|
||||
if task.completed_on:
|
||||
@@ -39,6 +49,7 @@ def get_data(filters):
|
||||
tasks.sort(key=lambda x: x["delay"], reverse=True)
|
||||
return tasks
|
||||
|
||||
|
||||
def get_conditions(filters):
|
||||
conditions = frappe._dict()
|
||||
keys = ["priority", "status"]
|
||||
@@ -51,6 +62,7 @@ def get_conditions(filters):
|
||||
conditions.exp_start_date = ["<=", filters.get("to_date")]
|
||||
return conditions
|
||||
|
||||
|
||||
def get_chart_data(data):
|
||||
delay, on_track = 0, 0
|
||||
for entry in data:
|
||||
@@ -61,74 +73,29 @@ def get_chart_data(data):
|
||||
charts = {
|
||||
"data": {
|
||||
"labels": ["On Track", "Delayed"],
|
||||
"datasets": [
|
||||
{
|
||||
"name": "Delayed",
|
||||
"values": [on_track, delay]
|
||||
}
|
||||
]
|
||||
"datasets": [{"name": "Delayed", "values": [on_track, delay]}],
|
||||
},
|
||||
"type": "percentage",
|
||||
"colors": ["#84D5BA", "#CB4B5F"]
|
||||
"colors": ["#84D5BA", "#CB4B5F"],
|
||||
}
|
||||
return charts
|
||||
|
||||
|
||||
def get_columns():
|
||||
columns = [
|
||||
{
|
||||
"fieldname": "name",
|
||||
"fieldtype": "Link",
|
||||
"label": "Task",
|
||||
"options": "Task",
|
||||
"width": 150
|
||||
},
|
||||
{
|
||||
"fieldname": "subject",
|
||||
"fieldtype": "Data",
|
||||
"label": "Subject",
|
||||
"width": 200
|
||||
},
|
||||
{
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Data",
|
||||
"label": "Status",
|
||||
"width": 100
|
||||
},
|
||||
{
|
||||
"fieldname": "priority",
|
||||
"fieldtype": "Data",
|
||||
"label": "Priority",
|
||||
"width": 80
|
||||
},
|
||||
{
|
||||
"fieldname": "progress",
|
||||
"fieldtype": "Data",
|
||||
"label": "Progress (%)",
|
||||
"width": 120
|
||||
},
|
||||
{"fieldname": "name", "fieldtype": "Link", "label": "Task", "options": "Task", "width": 150},
|
||||
{"fieldname": "subject", "fieldtype": "Data", "label": "Subject", "width": 200},
|
||||
{"fieldname": "status", "fieldtype": "Data", "label": "Status", "width": 100},
|
||||
{"fieldname": "priority", "fieldtype": "Data", "label": "Priority", "width": 80},
|
||||
{"fieldname": "progress", "fieldtype": "Data", "label": "Progress (%)", "width": 120},
|
||||
{
|
||||
"fieldname": "exp_start_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Expected Start Date",
|
||||
"width": 150
|
||||
"width": 150,
|
||||
},
|
||||
{
|
||||
"fieldname": "exp_end_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Expected End Date",
|
||||
"width": 150
|
||||
},
|
||||
{
|
||||
"fieldname": "completed_on",
|
||||
"fieldtype": "Date",
|
||||
"label": "Actual End Date",
|
||||
"width": 130
|
||||
},
|
||||
{
|
||||
"fieldname": "delay",
|
||||
"fieldtype": "Data",
|
||||
"label": "Delay (In Days)",
|
||||
"width": 120
|
||||
}
|
||||
{"fieldname": "exp_end_date", "fieldtype": "Date", "label": "Expected End Date", "width": 150},
|
||||
{"fieldname": "completed_on", "fieldtype": "Date", "label": "Actual End Date", "width": 130},
|
||||
{"fieldname": "delay", "fieldtype": "Data", "label": "Delay (In Days)", "width": 120},
|
||||
]
|
||||
return columns
|
||||
|
||||
@@ -18,25 +18,17 @@ class TestDelayedTasksSummary(unittest.TestCase):
|
||||
task1.save()
|
||||
|
||||
def test_delayed_tasks_summary(self):
|
||||
filters = frappe._dict({
|
||||
"from_date": add_months(nowdate(), -1),
|
||||
"to_date": nowdate(),
|
||||
"priority": "Low",
|
||||
"status": "Open"
|
||||
})
|
||||
expected_data = [
|
||||
filters = frappe._dict(
|
||||
{
|
||||
"subject": "_Test Task 99",
|
||||
"from_date": add_months(nowdate(), -1),
|
||||
"to_date": nowdate(),
|
||||
"priority": "Low",
|
||||
"status": "Open",
|
||||
"priority": "Low",
|
||||
"delay": 1
|
||||
},
|
||||
{
|
||||
"subject": "_Test Task 98",
|
||||
"status": "Completed",
|
||||
"priority": "Low",
|
||||
"delay": -1
|
||||
}
|
||||
)
|
||||
expected_data = [
|
||||
{"subject": "_Test Task 99", "status": "Open", "priority": "Low", "delay": 1},
|
||||
{"subject": "_Test Task 98", "status": "Completed", "priority": "Low", "delay": -1},
|
||||
]
|
||||
report = execute(filters)
|
||||
data = list(filter(lambda x: x.subject == "_Test Task 99", report[1]))[0]
|
||||
|
||||
@@ -10,8 +10,10 @@ from frappe.utils import flt, getdate
|
||||
def execute(filters=None):
|
||||
return EmployeeHoursReport(filters).run()
|
||||
|
||||
|
||||
class EmployeeHoursReport:
|
||||
'''Employee Hours Utilization Report Based On Timesheet'''
|
||||
"""Employee Hours Utilization Report Based On Timesheet"""
|
||||
|
||||
def __init__(self, filters=None):
|
||||
self.filters = frappe._dict(filters or {})
|
||||
|
||||
@@ -25,13 +27,17 @@ class EmployeeHoursReport:
|
||||
self.day_span = (self.to_date - self.from_date).days
|
||||
|
||||
if self.day_span <= 0:
|
||||
frappe.throw(_('From Date must come before To Date'))
|
||||
frappe.throw(_("From Date must come before To Date"))
|
||||
|
||||
def validate_standard_working_hours(self):
|
||||
self.standard_working_hours = frappe.db.get_single_value('HR Settings', 'standard_working_hours')
|
||||
self.standard_working_hours = frappe.db.get_single_value("HR Settings", "standard_working_hours")
|
||||
if not self.standard_working_hours:
|
||||
msg = _('The metrics for this report are calculated based on the Standard Working Hours. Please set {0} in {1}.').format(
|
||||
frappe.bold('Standard Working Hours'), frappe.utils.get_link_to_form('HR Settings', 'HR Settings'))
|
||||
msg = _(
|
||||
"The metrics for this report are calculated based on the Standard Working Hours. Please set {0} in {1}."
|
||||
).format(
|
||||
frappe.bold("Standard Working Hours"),
|
||||
frappe.utils.get_link_to_form("HR Settings", "HR Settings"),
|
||||
)
|
||||
|
||||
frappe.throw(msg)
|
||||
|
||||
@@ -46,55 +52,50 @@ class EmployeeHoursReport:
|
||||
def generate_columns(self):
|
||||
self.columns = [
|
||||
{
|
||||
'label': _('Employee'),
|
||||
'options': 'Employee',
|
||||
'fieldname': 'employee',
|
||||
'fieldtype': 'Link',
|
||||
'width': 230
|
||||
"label": _("Employee"),
|
||||
"options": "Employee",
|
||||
"fieldname": "employee",
|
||||
"fieldtype": "Link",
|
||||
"width": 230,
|
||||
},
|
||||
{
|
||||
'label': _('Department'),
|
||||
'options': 'Department',
|
||||
'fieldname': 'department',
|
||||
'fieldtype': 'Link',
|
||||
'width': 120
|
||||
"label": _("Department"),
|
||||
"options": "Department",
|
||||
"fieldname": "department",
|
||||
"fieldtype": "Link",
|
||||
"width": 120,
|
||||
},
|
||||
{"label": _("Total Hours (T)"), "fieldname": "total_hours", "fieldtype": "Float", "width": 120},
|
||||
{
|
||||
"label": _("Billed Hours (B)"),
|
||||
"fieldname": "billed_hours",
|
||||
"fieldtype": "Float",
|
||||
"width": 170,
|
||||
},
|
||||
{
|
||||
'label': _('Total Hours (T)'),
|
||||
'fieldname': 'total_hours',
|
||||
'fieldtype': 'Float',
|
||||
'width': 120
|
||||
"label": _("Non-Billed Hours (NB)"),
|
||||
"fieldname": "non_billed_hours",
|
||||
"fieldtype": "Float",
|
||||
"width": 170,
|
||||
},
|
||||
{
|
||||
'label': _('Billed Hours (B)'),
|
||||
'fieldname': 'billed_hours',
|
||||
'fieldtype': 'Float',
|
||||
'width': 170
|
||||
"label": _("Untracked Hours (U)"),
|
||||
"fieldname": "untracked_hours",
|
||||
"fieldtype": "Float",
|
||||
"width": 170,
|
||||
},
|
||||
{
|
||||
'label': _('Non-Billed Hours (NB)'),
|
||||
'fieldname': 'non_billed_hours',
|
||||
'fieldtype': 'Float',
|
||||
'width': 170
|
||||
"label": _("% Utilization (B + NB) / T"),
|
||||
"fieldname": "per_util",
|
||||
"fieldtype": "Percentage",
|
||||
"width": 200,
|
||||
},
|
||||
{
|
||||
'label': _('Untracked Hours (U)'),
|
||||
'fieldname': 'untracked_hours',
|
||||
'fieldtype': 'Float',
|
||||
'width': 170
|
||||
"label": _("% Utilization (B / T)"),
|
||||
"fieldname": "per_util_billed_only",
|
||||
"fieldtype": "Percentage",
|
||||
"width": 200,
|
||||
},
|
||||
{
|
||||
'label': _('% Utilization (B + NB) / T'),
|
||||
'fieldname': 'per_util',
|
||||
'fieldtype': 'Percentage',
|
||||
'width': 200
|
||||
},
|
||||
{
|
||||
'label': _('% Utilization (B / T)'),
|
||||
'fieldname': 'per_util_billed_only',
|
||||
'fieldtype': 'Percentage',
|
||||
'width': 200
|
||||
}
|
||||
]
|
||||
|
||||
def generate_data(self):
|
||||
@@ -111,35 +112,36 @@ class EmployeeHoursReport:
|
||||
|
||||
for emp, data in self.stats_by_employee.items():
|
||||
row = frappe._dict()
|
||||
row['employee'] = emp
|
||||
row["employee"] = emp
|
||||
row.update(data)
|
||||
self.data.append(row)
|
||||
|
||||
# Sort by descending order of percentage utilization
|
||||
self.data.sort(key=lambda x: x['per_util'], reverse=True)
|
||||
self.data.sort(key=lambda x: x["per_util"], reverse=True)
|
||||
|
||||
def filter_stats_by_department(self):
|
||||
filtered_data = frappe._dict()
|
||||
for emp, data in self.stats_by_employee.items():
|
||||
if data['department'] == self.filters.department:
|
||||
if data["department"] == self.filters.department:
|
||||
filtered_data[emp] = data
|
||||
|
||||
# Update stats
|
||||
self.stats_by_employee = filtered_data
|
||||
|
||||
def generate_filtered_time_logs(self):
|
||||
additional_filters = ''
|
||||
additional_filters = ""
|
||||
|
||||
filter_fields = ['employee', 'project', 'company']
|
||||
filter_fields = ["employee", "project", "company"]
|
||||
|
||||
for field in filter_fields:
|
||||
if self.filters.get(field):
|
||||
if field == 'project':
|
||||
if field == "project":
|
||||
additional_filters += f"AND ttd.{field} = '{self.filters.get(field)}'"
|
||||
else:
|
||||
additional_filters += f"AND tt.{field} = '{self.filters.get(field)}'"
|
||||
|
||||
self.filtered_time_logs = frappe.db.sql('''
|
||||
self.filtered_time_logs = frappe.db.sql(
|
||||
"""
|
||||
SELECT tt.employee AS employee, ttd.hours AS hours, ttd.is_billable AS is_billable, ttd.project AS project
|
||||
FROM `tabTimesheet Detail` AS ttd
|
||||
JOIN `tabTimesheet` AS tt
|
||||
@@ -148,47 +150,46 @@ class EmployeeHoursReport:
|
||||
AND tt.start_date BETWEEN '{0}' AND '{1}'
|
||||
AND tt.end_date BETWEEN '{0}' AND '{1}'
|
||||
{2}
|
||||
'''.format(self.filters.from_date, self.filters.to_date, additional_filters))
|
||||
""".format(
|
||||
self.filters.from_date, self.filters.to_date, additional_filters
|
||||
)
|
||||
)
|
||||
|
||||
def generate_stats_by_employee(self):
|
||||
self.stats_by_employee = frappe._dict()
|
||||
|
||||
for emp, hours, is_billable, project in self.filtered_time_logs:
|
||||
self.stats_by_employee.setdefault(
|
||||
emp, frappe._dict()
|
||||
).setdefault('billed_hours', 0.0)
|
||||
self.stats_by_employee.setdefault(emp, frappe._dict()).setdefault("billed_hours", 0.0)
|
||||
|
||||
self.stats_by_employee[emp].setdefault('non_billed_hours', 0.0)
|
||||
self.stats_by_employee[emp].setdefault("non_billed_hours", 0.0)
|
||||
|
||||
if is_billable:
|
||||
self.stats_by_employee[emp]['billed_hours'] += flt(hours, 2)
|
||||
self.stats_by_employee[emp]["billed_hours"] += flt(hours, 2)
|
||||
else:
|
||||
self.stats_by_employee[emp]['non_billed_hours'] += flt(hours, 2)
|
||||
self.stats_by_employee[emp]["non_billed_hours"] += flt(hours, 2)
|
||||
|
||||
def set_employee_department_and_name(self):
|
||||
for emp in self.stats_by_employee:
|
||||
emp_name = frappe.db.get_value(
|
||||
'Employee', emp, 'employee_name'
|
||||
)
|
||||
emp_dept = frappe.db.get_value(
|
||||
'Employee', emp, 'department'
|
||||
)
|
||||
emp_name = frappe.db.get_value("Employee", emp, "employee_name")
|
||||
emp_dept = frappe.db.get_value("Employee", emp, "department")
|
||||
|
||||
self.stats_by_employee[emp]['department'] = emp_dept
|
||||
self.stats_by_employee[emp]['employee_name'] = emp_name
|
||||
self.stats_by_employee[emp]["department"] = emp_dept
|
||||
self.stats_by_employee[emp]["employee_name"] = emp_name
|
||||
|
||||
def calculate_utilizations(self):
|
||||
TOTAL_HOURS = flt(self.standard_working_hours * self.day_span, 2)
|
||||
for emp, data in self.stats_by_employee.items():
|
||||
data['total_hours'] = TOTAL_HOURS
|
||||
data['untracked_hours'] = flt(TOTAL_HOURS - data['billed_hours'] - data['non_billed_hours'], 2)
|
||||
data["total_hours"] = TOTAL_HOURS
|
||||
data["untracked_hours"] = flt(TOTAL_HOURS - data["billed_hours"] - data["non_billed_hours"], 2)
|
||||
|
||||
# To handle overtime edge-case
|
||||
if data['untracked_hours'] < 0:
|
||||
data['untracked_hours'] = 0.0
|
||||
if data["untracked_hours"] < 0:
|
||||
data["untracked_hours"] = 0.0
|
||||
|
||||
data['per_util'] = flt(((data['billed_hours'] + data['non_billed_hours']) / TOTAL_HOURS) * 100, 2)
|
||||
data['per_util_billed_only'] = flt((data['billed_hours'] / TOTAL_HOURS) * 100, 2)
|
||||
data["per_util"] = flt(
|
||||
((data["billed_hours"] + data["non_billed_hours"]) / TOTAL_HOURS) * 100, 2
|
||||
)
|
||||
data["per_util_billed_only"] = flt((data["billed_hours"] / TOTAL_HOURS) * 100, 2)
|
||||
|
||||
def generate_report_summary(self):
|
||||
self.report_summary = []
|
||||
@@ -202,11 +203,11 @@ class EmployeeHoursReport:
|
||||
total_untracked = 0.0
|
||||
|
||||
for row in self.data:
|
||||
avg_utilization += row['per_util']
|
||||
avg_utilization_billed_only += row['per_util_billed_only']
|
||||
total_billed += row['billed_hours']
|
||||
total_non_billed += row['non_billed_hours']
|
||||
total_untracked += row['untracked_hours']
|
||||
avg_utilization += row["per_util"]
|
||||
avg_utilization_billed_only += row["per_util_billed_only"]
|
||||
total_billed += row["billed_hours"]
|
||||
total_non_billed += row["non_billed_hours"]
|
||||
total_untracked += row["untracked_hours"]
|
||||
|
||||
avg_utilization /= len(self.data)
|
||||
avg_utilization = flt(avg_utilization, 2)
|
||||
@@ -217,27 +218,19 @@ class EmployeeHoursReport:
|
||||
THRESHOLD_PERCENTAGE = 70.0
|
||||
self.report_summary = [
|
||||
{
|
||||
'value': f'{avg_utilization}%',
|
||||
'indicator': 'Red' if avg_utilization < THRESHOLD_PERCENTAGE else 'Green',
|
||||
'label': _('Avg Utilization'),
|
||||
'datatype': 'Percentage'
|
||||
"value": f"{avg_utilization}%",
|
||||
"indicator": "Red" if avg_utilization < THRESHOLD_PERCENTAGE else "Green",
|
||||
"label": _("Avg Utilization"),
|
||||
"datatype": "Percentage",
|
||||
},
|
||||
{
|
||||
'value': f'{avg_utilization_billed_only}%',
|
||||
'indicator': 'Red' if avg_utilization_billed_only < THRESHOLD_PERCENTAGE else 'Green',
|
||||
'label': _('Avg Utilization (Billed Only)'),
|
||||
'datatype': 'Percentage'
|
||||
"value": f"{avg_utilization_billed_only}%",
|
||||
"indicator": "Red" if avg_utilization_billed_only < THRESHOLD_PERCENTAGE else "Green",
|
||||
"label": _("Avg Utilization (Billed Only)"),
|
||||
"datatype": "Percentage",
|
||||
},
|
||||
{
|
||||
'value': total_billed,
|
||||
'label': _('Total Billed Hours'),
|
||||
'datatype': 'Float'
|
||||
},
|
||||
{
|
||||
'value': total_non_billed,
|
||||
'label': _('Total Non-Billed Hours'),
|
||||
'datatype': 'Float'
|
||||
}
|
||||
{"value": total_billed, "label": _("Total Billed Hours"), "datatype": "Float"},
|
||||
{"value": total_non_billed, "label": _("Total Non-Billed Hours"), "datatype": "Float"},
|
||||
]
|
||||
|
||||
def generate_chart_data(self):
|
||||
@@ -248,33 +241,21 @@ class EmployeeHoursReport:
|
||||
non_billed_hours = []
|
||||
untracked_hours = []
|
||||
|
||||
|
||||
for row in self.data:
|
||||
labels.append(row.get('employee_name'))
|
||||
billed_hours.append(row.get('billed_hours'))
|
||||
non_billed_hours.append(row.get('non_billed_hours'))
|
||||
untracked_hours.append(row.get('untracked_hours'))
|
||||
labels.append(row.get("employee_name"))
|
||||
billed_hours.append(row.get("billed_hours"))
|
||||
non_billed_hours.append(row.get("non_billed_hours"))
|
||||
untracked_hours.append(row.get("untracked_hours"))
|
||||
|
||||
self.chart = {
|
||||
'data': {
|
||||
'labels': labels[:30],
|
||||
'datasets': [
|
||||
{
|
||||
'name': _('Billed Hours'),
|
||||
'values': billed_hours[:30]
|
||||
},
|
||||
{
|
||||
'name': _('Non-Billed Hours'),
|
||||
'values': non_billed_hours[:30]
|
||||
},
|
||||
{
|
||||
'name': _('Untracked Hours'),
|
||||
'values': untracked_hours[:30]
|
||||
}
|
||||
]
|
||||
"data": {
|
||||
"labels": labels[:30],
|
||||
"datasets": [
|
||||
{"name": _("Billed Hours"), "values": billed_hours[:30]},
|
||||
{"name": _("Non-Billed Hours"), "values": non_billed_hours[:30]},
|
||||
{"name": _("Untracked Hours"), "values": untracked_hours[:30]},
|
||||
],
|
||||
},
|
||||
'type': 'bar',
|
||||
'barOptions': {
|
||||
'stacked': True
|
||||
}
|
||||
"type": "bar",
|
||||
"barOptions": {"stacked": True},
|
||||
}
|
||||
|
||||
@@ -11,191 +11,189 @@ from erpnext.projects.report.employee_hours_utilization_based_on_timesheet.emplo
|
||||
|
||||
|
||||
class TestEmployeeUtilization(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
# Create test employee
|
||||
cls.test_emp1 = make_employee("test1@employeeutil.com", "_Test Company")
|
||||
cls.test_emp2 = make_employee("test2@employeeutil.com", "_Test Company")
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
# Create test employee
|
||||
cls.test_emp1 = make_employee("test1@employeeutil.com", "_Test Company")
|
||||
cls.test_emp2 = make_employee("test2@employeeutil.com", "_Test Company")
|
||||
|
||||
# Create test project
|
||||
cls.test_project = make_project({"project_name": "_Test Project"})
|
||||
# Create test project
|
||||
cls.test_project = make_project({"project_name": "_Test Project"})
|
||||
|
||||
# Create test timesheets
|
||||
cls.create_test_timesheets()
|
||||
# Create test timesheets
|
||||
cls.create_test_timesheets()
|
||||
|
||||
frappe.db.set_value("HR Settings", "HR Settings", "standard_working_hours", 9)
|
||||
frappe.db.set_value("HR Settings", "HR Settings", "standard_working_hours", 9)
|
||||
|
||||
@classmethod
|
||||
def create_test_timesheets(cls):
|
||||
timesheet1 = frappe.new_doc("Timesheet")
|
||||
timesheet1.employee = cls.test_emp1
|
||||
timesheet1.company = '_Test Company'
|
||||
@classmethod
|
||||
def create_test_timesheets(cls):
|
||||
timesheet1 = frappe.new_doc("Timesheet")
|
||||
timesheet1.employee = cls.test_emp1
|
||||
timesheet1.company = "_Test Company"
|
||||
|
||||
timesheet1.append("time_logs", {
|
||||
"activity_type": get_random("Activity Type"),
|
||||
"hours": 5,
|
||||
"is_billable": 1,
|
||||
"from_time": '2021-04-01 13:30:00.000000',
|
||||
"to_time": '2021-04-01 18:30:00.000000'
|
||||
})
|
||||
timesheet1.append(
|
||||
"time_logs",
|
||||
{
|
||||
"activity_type": get_random("Activity Type"),
|
||||
"hours": 5,
|
||||
"is_billable": 1,
|
||||
"from_time": "2021-04-01 13:30:00.000000",
|
||||
"to_time": "2021-04-01 18:30:00.000000",
|
||||
},
|
||||
)
|
||||
|
||||
timesheet1.save()
|
||||
timesheet1.submit()
|
||||
timesheet1.save()
|
||||
timesheet1.submit()
|
||||
|
||||
timesheet2 = frappe.new_doc("Timesheet")
|
||||
timesheet2.employee = cls.test_emp2
|
||||
timesheet2.company = '_Test Company'
|
||||
timesheet2 = frappe.new_doc("Timesheet")
|
||||
timesheet2.employee = cls.test_emp2
|
||||
timesheet2.company = "_Test Company"
|
||||
|
||||
timesheet2.append("time_logs", {
|
||||
"activity_type": get_random("Activity Type"),
|
||||
"hours": 10,
|
||||
"is_billable": 0,
|
||||
"from_time": '2021-04-01 13:30:00.000000',
|
||||
"to_time": '2021-04-01 23:30:00.000000',
|
||||
"project": cls.test_project.name
|
||||
})
|
||||
timesheet2.append(
|
||||
"time_logs",
|
||||
{
|
||||
"activity_type": get_random("Activity Type"),
|
||||
"hours": 10,
|
||||
"is_billable": 0,
|
||||
"from_time": "2021-04-01 13:30:00.000000",
|
||||
"to_time": "2021-04-01 23:30:00.000000",
|
||||
"project": cls.test_project.name,
|
||||
},
|
||||
)
|
||||
|
||||
timesheet2.save()
|
||||
timesheet2.submit()
|
||||
timesheet2.save()
|
||||
timesheet2.submit()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
# Delete time logs
|
||||
frappe.db.sql("""
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
# Delete time logs
|
||||
frappe.db.sql(
|
||||
"""
|
||||
DELETE FROM `tabTimesheet Detail`
|
||||
WHERE parent IN (
|
||||
SELECT name
|
||||
FROM `tabTimesheet`
|
||||
WHERE company = '_Test Company'
|
||||
)
|
||||
""")
|
||||
"""
|
||||
)
|
||||
|
||||
frappe.db.sql("DELETE FROM `tabTimesheet` WHERE company='_Test Company'")
|
||||
frappe.db.sql(f"DELETE FROM `tabProject` WHERE name='{cls.test_project.name}'")
|
||||
frappe.db.sql("DELETE FROM `tabTimesheet` WHERE company='_Test Company'")
|
||||
frappe.db.sql(f"DELETE FROM `tabProject` WHERE name='{cls.test_project.name}'")
|
||||
|
||||
def test_utilization_report_with_required_filters_only(self):
|
||||
filters = {
|
||||
"company": "_Test Company",
|
||||
"from_date": "2021-04-01",
|
||||
"to_date": "2021-04-03"
|
||||
}
|
||||
def test_utilization_report_with_required_filters_only(self):
|
||||
filters = {"company": "_Test Company", "from_date": "2021-04-01", "to_date": "2021-04-03"}
|
||||
|
||||
report = execute(filters)
|
||||
report = execute(filters)
|
||||
|
||||
expected_data = self.get_expected_data_for_test_employees()
|
||||
self.assertEqual(report[1], expected_data)
|
||||
expected_data = self.get_expected_data_for_test_employees()
|
||||
self.assertEqual(report[1], expected_data)
|
||||
|
||||
def test_utilization_report_for_single_employee(self):
|
||||
filters = {
|
||||
"company": "_Test Company",
|
||||
"from_date": "2021-04-01",
|
||||
"to_date": "2021-04-03",
|
||||
"employee": self.test_emp1
|
||||
}
|
||||
def test_utilization_report_for_single_employee(self):
|
||||
filters = {
|
||||
"company": "_Test Company",
|
||||
"from_date": "2021-04-01",
|
||||
"to_date": "2021-04-03",
|
||||
"employee": self.test_emp1,
|
||||
}
|
||||
|
||||
report = execute(filters)
|
||||
report = execute(filters)
|
||||
|
||||
emp1_data = frappe.get_doc('Employee', self.test_emp1)
|
||||
expected_data = [
|
||||
{
|
||||
'employee': self.test_emp1,
|
||||
'employee_name': 'test1@employeeutil.com',
|
||||
'billed_hours': 5.0,
|
||||
'non_billed_hours': 0.0,
|
||||
'department': emp1_data.department,
|
||||
'total_hours': 18.0,
|
||||
'untracked_hours': 13.0,
|
||||
'per_util': 27.78,
|
||||
'per_util_billed_only': 27.78
|
||||
}
|
||||
]
|
||||
emp1_data = frappe.get_doc("Employee", self.test_emp1)
|
||||
expected_data = [
|
||||
{
|
||||
"employee": self.test_emp1,
|
||||
"employee_name": "test1@employeeutil.com",
|
||||
"billed_hours": 5.0,
|
||||
"non_billed_hours": 0.0,
|
||||
"department": emp1_data.department,
|
||||
"total_hours": 18.0,
|
||||
"untracked_hours": 13.0,
|
||||
"per_util": 27.78,
|
||||
"per_util_billed_only": 27.78,
|
||||
}
|
||||
]
|
||||
|
||||
self.assertEqual(report[1], expected_data)
|
||||
self.assertEqual(report[1], expected_data)
|
||||
|
||||
def test_utilization_report_for_project(self):
|
||||
filters = {
|
||||
"company": "_Test Company",
|
||||
"from_date": "2021-04-01",
|
||||
"to_date": "2021-04-03",
|
||||
"project": self.test_project.name
|
||||
}
|
||||
def test_utilization_report_for_project(self):
|
||||
filters = {
|
||||
"company": "_Test Company",
|
||||
"from_date": "2021-04-01",
|
||||
"to_date": "2021-04-03",
|
||||
"project": self.test_project.name,
|
||||
}
|
||||
|
||||
report = execute(filters)
|
||||
report = execute(filters)
|
||||
|
||||
emp2_data = frappe.get_doc('Employee', self.test_emp2)
|
||||
expected_data = [
|
||||
{
|
||||
'employee': self.test_emp2,
|
||||
'employee_name': 'test2@employeeutil.com',
|
||||
'billed_hours': 0.0,
|
||||
'non_billed_hours': 10.0,
|
||||
'department': emp2_data.department,
|
||||
'total_hours': 18.0,
|
||||
'untracked_hours': 8.0,
|
||||
'per_util': 55.56,
|
||||
'per_util_billed_only': 0.0
|
||||
}
|
||||
]
|
||||
emp2_data = frappe.get_doc("Employee", self.test_emp2)
|
||||
expected_data = [
|
||||
{
|
||||
"employee": self.test_emp2,
|
||||
"employee_name": "test2@employeeutil.com",
|
||||
"billed_hours": 0.0,
|
||||
"non_billed_hours": 10.0,
|
||||
"department": emp2_data.department,
|
||||
"total_hours": 18.0,
|
||||
"untracked_hours": 8.0,
|
||||
"per_util": 55.56,
|
||||
"per_util_billed_only": 0.0,
|
||||
}
|
||||
]
|
||||
|
||||
self.assertEqual(report[1], expected_data)
|
||||
self.assertEqual(report[1], expected_data)
|
||||
|
||||
def test_utilization_report_for_department(self):
|
||||
emp1_data = frappe.get_doc('Employee', self.test_emp1)
|
||||
filters = {
|
||||
"company": "_Test Company",
|
||||
"from_date": "2021-04-01",
|
||||
"to_date": "2021-04-03",
|
||||
"department": emp1_data.department
|
||||
}
|
||||
def test_utilization_report_for_department(self):
|
||||
emp1_data = frappe.get_doc("Employee", self.test_emp1)
|
||||
filters = {
|
||||
"company": "_Test Company",
|
||||
"from_date": "2021-04-01",
|
||||
"to_date": "2021-04-03",
|
||||
"department": emp1_data.department,
|
||||
}
|
||||
|
||||
report = execute(filters)
|
||||
report = execute(filters)
|
||||
|
||||
expected_data = self.get_expected_data_for_test_employees()
|
||||
self.assertEqual(report[1], expected_data)
|
||||
expected_data = self.get_expected_data_for_test_employees()
|
||||
self.assertEqual(report[1], expected_data)
|
||||
|
||||
def test_report_summary_data(self):
|
||||
filters = {
|
||||
"company": "_Test Company",
|
||||
"from_date": "2021-04-01",
|
||||
"to_date": "2021-04-03"
|
||||
}
|
||||
def test_report_summary_data(self):
|
||||
filters = {"company": "_Test Company", "from_date": "2021-04-01", "to_date": "2021-04-03"}
|
||||
|
||||
report = execute(filters)
|
||||
summary = report[4]
|
||||
expected_summary_values = ['41.67%', '13.89%', 5.0, 10.0]
|
||||
report = execute(filters)
|
||||
summary = report[4]
|
||||
expected_summary_values = ["41.67%", "13.89%", 5.0, 10.0]
|
||||
|
||||
self.assertEqual(len(summary), 4)
|
||||
self.assertEqual(len(summary), 4)
|
||||
|
||||
for i in range(4):
|
||||
self.assertEqual(
|
||||
summary[i]['value'], expected_summary_values[i]
|
||||
)
|
||||
for i in range(4):
|
||||
self.assertEqual(summary[i]["value"], expected_summary_values[i])
|
||||
|
||||
def get_expected_data_for_test_employees(self):
|
||||
emp1_data = frappe.get_doc('Employee', self.test_emp1)
|
||||
emp2_data = frappe.get_doc('Employee', self.test_emp2)
|
||||
def get_expected_data_for_test_employees(self):
|
||||
emp1_data = frappe.get_doc("Employee", self.test_emp1)
|
||||
emp2_data = frappe.get_doc("Employee", self.test_emp2)
|
||||
|
||||
return [
|
||||
{
|
||||
'employee': self.test_emp2,
|
||||
'employee_name': 'test2@employeeutil.com',
|
||||
'billed_hours': 0.0,
|
||||
'non_billed_hours': 10.0,
|
||||
'department': emp2_data.department,
|
||||
'total_hours': 18.0,
|
||||
'untracked_hours': 8.0,
|
||||
'per_util': 55.56,
|
||||
'per_util_billed_only': 0.0
|
||||
},
|
||||
{
|
||||
'employee': self.test_emp1,
|
||||
'employee_name': 'test1@employeeutil.com',
|
||||
'billed_hours': 5.0,
|
||||
'non_billed_hours': 0.0,
|
||||
'department': emp1_data.department,
|
||||
'total_hours': 18.0,
|
||||
'untracked_hours': 13.0,
|
||||
'per_util': 27.78,
|
||||
'per_util_billed_only': 27.78
|
||||
}
|
||||
]
|
||||
return [
|
||||
{
|
||||
"employee": self.test_emp2,
|
||||
"employee_name": "test2@employeeutil.com",
|
||||
"billed_hours": 0.0,
|
||||
"non_billed_hours": 10.0,
|
||||
"department": emp2_data.department,
|
||||
"total_hours": 18.0,
|
||||
"untracked_hours": 8.0,
|
||||
"per_util": 55.56,
|
||||
"per_util_billed_only": 0.0,
|
||||
},
|
||||
{
|
||||
"employee": self.test_emp1,
|
||||
"employee_name": "test1@employeeutil.com",
|
||||
"billed_hours": 5.0,
|
||||
"non_billed_hours": 0.0,
|
||||
"department": emp1_data.department,
|
||||
"total_hours": 18.0,
|
||||
"untracked_hours": 13.0,
|
||||
"per_util": 27.78,
|
||||
"per_util_billed_only": 27.78,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -12,17 +12,23 @@ def execute(filters=None):
|
||||
charts = get_chart_data(data)
|
||||
return columns, data, None, charts
|
||||
|
||||
|
||||
def get_data(filters):
|
||||
data = get_rows(filters)
|
||||
data = calculate_cost_and_profit(data)
|
||||
return data
|
||||
|
||||
|
||||
def get_rows(filters):
|
||||
conditions = get_conditions(filters)
|
||||
standard_working_hours = frappe.db.get_single_value("HR Settings", "standard_working_hours")
|
||||
if not standard_working_hours:
|
||||
msg = _("The metrics for this report are calculated based on the Standard Working Hours. Please set {0} in {1}.").format(
|
||||
frappe.bold("Standard Working Hours"), frappe.utils.get_link_to_form("HR Settings", "HR Settings"))
|
||||
msg = _(
|
||||
"The metrics for this report are calculated based on the Standard Working Hours. Please set {0} in {1}."
|
||||
).format(
|
||||
frappe.bold("Standard Working Hours"),
|
||||
frappe.utils.get_link_to_form("HR Settings", "HR Settings"),
|
||||
)
|
||||
|
||||
frappe.msgprint(msg)
|
||||
return []
|
||||
@@ -43,12 +49,17 @@ def get_rows(filters):
|
||||
`tabSalary Slip Timesheet` as sst join `tabTimesheet` on tabTimesheet.name = sst.time_sheet
|
||||
join `tabSales Invoice Timesheet` as sit on sit.time_sheet = tabTimesheet.name
|
||||
join `tabSales Invoice` as si on si.name = sit.parent and si.status != "Cancelled"
|
||||
join `tabSalary Slip` as ss on ss.name = sst.parent and ss.status != "Cancelled" """.format(standard_working_hours)
|
||||
join `tabSalary Slip` as ss on ss.name = sst.parent and ss.status != "Cancelled" """.format(
|
||||
standard_working_hours
|
||||
)
|
||||
if conditions:
|
||||
sql += """
|
||||
WHERE
|
||||
{0}) as t""".format(conditions)
|
||||
return frappe.db.sql(sql,filters, as_dict=True)
|
||||
{0}) as t""".format(
|
||||
conditions
|
||||
)
|
||||
return frappe.db.sql(sql, filters, as_dict=True)
|
||||
|
||||
|
||||
def calculate_cost_and_profit(data):
|
||||
for row in data:
|
||||
@@ -56,6 +67,7 @@ def calculate_cost_and_profit(data):
|
||||
row.profit = flt(row.base_grand_total) - flt(row.base_gross_pay) * flt(row.utilization)
|
||||
return data
|
||||
|
||||
|
||||
def get_conditions(filters):
|
||||
conditions = []
|
||||
|
||||
@@ -75,11 +87,14 @@ def get_conditions(filters):
|
||||
conditions.append("tabTimesheet.employee={0}".format(frappe.db.escape(filters.get("employee"))))
|
||||
|
||||
if filters.get("project"):
|
||||
conditions.append("tabTimesheet.parent_project={0}".format(frappe.db.escape(filters.get("project"))))
|
||||
conditions.append(
|
||||
"tabTimesheet.parent_project={0}".format(frappe.db.escape(filters.get("project")))
|
||||
)
|
||||
|
||||
conditions = " and ".join(conditions)
|
||||
return conditions
|
||||
|
||||
|
||||
def get_chart_data(data):
|
||||
if not data:
|
||||
return None
|
||||
@@ -92,20 +107,13 @@ def get_chart_data(data):
|
||||
utilization.append(entry.get("utilization"))
|
||||
|
||||
charts = {
|
||||
"data": {
|
||||
"labels": labels,
|
||||
"datasets": [
|
||||
{
|
||||
"name": "Utilization",
|
||||
"values": utilization
|
||||
}
|
||||
]
|
||||
},
|
||||
"data": {"labels": labels, "datasets": [{"name": "Utilization", "values": utilization}]},
|
||||
"type": "bar",
|
||||
"colors": ["#84BDD5"]
|
||||
"colors": ["#84BDD5"],
|
||||
}
|
||||
return charts
|
||||
|
||||
|
||||
def get_columns():
|
||||
return [
|
||||
{
|
||||
@@ -113,98 +121,78 @@ def get_columns():
|
||||
"label": _("Customer"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Customer",
|
||||
"width": 150
|
||||
"width": 150,
|
||||
},
|
||||
{
|
||||
"fieldname": "employee",
|
||||
"label": _("Employee"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Employee",
|
||||
"width": 130
|
||||
},
|
||||
{
|
||||
"fieldname": "employee_name",
|
||||
"label": _("Employee Name"),
|
||||
"fieldtype": "Data",
|
||||
"width": 120
|
||||
"width": 130,
|
||||
},
|
||||
{"fieldname": "employee_name", "label": _("Employee Name"), "fieldtype": "Data", "width": 120},
|
||||
{
|
||||
"fieldname": "voucher_no",
|
||||
"label": _("Sales Invoice"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Sales Invoice",
|
||||
"width": 120
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"fieldname": "timesheet",
|
||||
"label": _("Timesheet"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Timesheet",
|
||||
"width": 120
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
"label": _("Project"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Project",
|
||||
"width": 100
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"fieldname": "base_grand_total",
|
||||
"label": _("Bill Amount"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 100
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"fieldname": "base_gross_pay",
|
||||
"label": _("Cost"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 100
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"fieldname": "profit",
|
||||
"label": _("Profit"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 100
|
||||
},
|
||||
{
|
||||
"fieldname": "utilization",
|
||||
"label": _("Utilization"),
|
||||
"fieldtype": "Percentage",
|
||||
"width": 100
|
||||
"width": 100,
|
||||
},
|
||||
{"fieldname": "utilization", "label": _("Utilization"), "fieldtype": "Percentage", "width": 100},
|
||||
{
|
||||
"fieldname": "fractional_cost",
|
||||
"label": _("Fractional Cost"),
|
||||
"fieldtype": "Int",
|
||||
"width": 120
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"fieldname": "total_billed_hours",
|
||||
"label": _("Total Billed Hours"),
|
||||
"fieldtype": "Int",
|
||||
"width": 150
|
||||
},
|
||||
{
|
||||
"fieldname": "start_date",
|
||||
"label": _("Start Date"),
|
||||
"fieldtype": "Date",
|
||||
"width": 100
|
||||
},
|
||||
{
|
||||
"fieldname": "end_date",
|
||||
"label": _("End Date"),
|
||||
"fieldtype": "Date",
|
||||
"width": 100
|
||||
"width": 150,
|
||||
},
|
||||
{"fieldname": "start_date", "label": _("Start Date"), "fieldtype": "Date", "width": 100},
|
||||
{"fieldname": "end_date", "label": _("End Date"), "fieldtype": "Date", "width": 100},
|
||||
{
|
||||
"label": _("Currency"),
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Link",
|
||||
"options": "Currency",
|
||||
"width": 80
|
||||
}
|
||||
"width": 80,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -13,13 +13,15 @@ from erpnext.projects.report.project_profitability.project_profitability import
|
||||
|
||||
class TestProjectProfitability(FrappeTestCase):
|
||||
def setUp(self):
|
||||
frappe.db.sql('delete from `tabTimesheet`')
|
||||
emp = make_employee('test_employee_9@salary.com', company='_Test Company')
|
||||
frappe.db.sql("delete from `tabTimesheet`")
|
||||
emp = make_employee("test_employee_9@salary.com", company="_Test Company")
|
||||
|
||||
if not frappe.db.exists('Salary Component', 'Timesheet Component'):
|
||||
frappe.get_doc({'doctype': 'Salary Component', 'salary_component': 'Timesheet Component'}).insert()
|
||||
if not frappe.db.exists("Salary Component", "Timesheet Component"):
|
||||
frappe.get_doc(
|
||||
{"doctype": "Salary Component", "salary_component": "Timesheet Component"}
|
||||
).insert()
|
||||
|
||||
make_salary_structure_for_timesheet(emp, company='_Test Company')
|
||||
make_salary_structure_for_timesheet(emp, company="_Test Company")
|
||||
date = getdate()
|
||||
|
||||
self.timesheet = make_timesheet(emp, is_billable=1)
|
||||
@@ -28,21 +30,21 @@ class TestProjectProfitability(FrappeTestCase):
|
||||
|
||||
holidays = self.salary_slip.get_holidays_for_employee(date, date)
|
||||
if holidays:
|
||||
frappe.db.set_value('Payroll Settings', None, 'include_holidays_in_total_working_days', 1)
|
||||
frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 1)
|
||||
|
||||
self.salary_slip.submit()
|
||||
self.sales_invoice = make_sales_invoice(self.timesheet.name, '_Test Item', '_Test Customer')
|
||||
self.sales_invoice = make_sales_invoice(self.timesheet.name, "_Test Item", "_Test Customer")
|
||||
self.sales_invoice.due_date = date
|
||||
self.sales_invoice.submit()
|
||||
|
||||
frappe.db.set_value('HR Settings', None, 'standard_working_hours', 8)
|
||||
frappe.db.set_value('Payroll Settings', None, 'include_holidays_in_total_working_days', 0)
|
||||
frappe.db.set_value("HR Settings", None, "standard_working_hours", 8)
|
||||
frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0)
|
||||
|
||||
def test_project_profitability(self):
|
||||
filters = {
|
||||
'company': '_Test Company',
|
||||
'start_date': add_days(self.timesheet.start_date, -3),
|
||||
'end_date': self.timesheet.start_date
|
||||
"company": "_Test Company",
|
||||
"start_date": add_days(self.timesheet.start_date, -3),
|
||||
"end_date": self.timesheet.start_date,
|
||||
}
|
||||
|
||||
report = execute(filters)
|
||||
@@ -58,7 +60,9 @@ class TestProjectProfitability(FrappeTestCase):
|
||||
self.assertEqual(self.salary_slip.total_working_days, row.total_working_days)
|
||||
|
||||
standard_working_hours = frappe.db.get_single_value("HR Settings", "standard_working_hours")
|
||||
utilization = timesheet.total_billed_hours/(self.salary_slip.total_working_days * standard_working_hours)
|
||||
utilization = timesheet.total_billed_hours / (
|
||||
self.salary_slip.total_working_days * standard_working_hours
|
||||
)
|
||||
self.assertEqual(utilization, row.utilization)
|
||||
|
||||
profit = self.sales_invoice.base_grand_total - self.salary_slip.base_gross_pay * utilization
|
||||
|
||||
@@ -10,18 +10,35 @@ def execute(filters=None):
|
||||
columns = get_columns()
|
||||
data = []
|
||||
|
||||
data = frappe.db.get_all("Project", filters=filters, fields=["name", 'status', "percent_complete", "expected_start_date", "expected_end_date", "project_type"], order_by="expected_end_date")
|
||||
data = frappe.db.get_all(
|
||||
"Project",
|
||||
filters=filters,
|
||||
fields=[
|
||||
"name",
|
||||
"status",
|
||||
"percent_complete",
|
||||
"expected_start_date",
|
||||
"expected_end_date",
|
||||
"project_type",
|
||||
],
|
||||
order_by="expected_end_date",
|
||||
)
|
||||
|
||||
for project in data:
|
||||
project["total_tasks"] = frappe.db.count("Task", filters={"project": project.name})
|
||||
project["completed_tasks"] = frappe.db.count("Task", filters={"project": project.name, "status": "Completed"})
|
||||
project["overdue_tasks"] = frappe.db.count("Task", filters={"project": project.name, "status": "Overdue"})
|
||||
project["completed_tasks"] = frappe.db.count(
|
||||
"Task", filters={"project": project.name, "status": "Completed"}
|
||||
)
|
||||
project["overdue_tasks"] = frappe.db.count(
|
||||
"Task", filters={"project": project.name, "status": "Overdue"}
|
||||
)
|
||||
|
||||
chart = get_chart_data(data)
|
||||
report_summary = get_report_summary(data)
|
||||
|
||||
return columns, data, None, chart, report_summary
|
||||
|
||||
|
||||
def get_columns():
|
||||
return [
|
||||
{
|
||||
@@ -29,59 +46,35 @@ def get_columns():
|
||||
"label": _("Project"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Project",
|
||||
"width": 200
|
||||
"width": 200,
|
||||
},
|
||||
{
|
||||
"fieldname": "project_type",
|
||||
"label": _("Type"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Project Type",
|
||||
"width": 120
|
||||
},
|
||||
{
|
||||
"fieldname": "status",
|
||||
"label": _("Status"),
|
||||
"fieldtype": "Data",
|
||||
"width": 120
|
||||
},
|
||||
{
|
||||
"fieldname": "total_tasks",
|
||||
"label": _("Total Tasks"),
|
||||
"fieldtype": "Data",
|
||||
"width": 120
|
||||
"width": 120,
|
||||
},
|
||||
{"fieldname": "status", "label": _("Status"), "fieldtype": "Data", "width": 120},
|
||||
{"fieldname": "total_tasks", "label": _("Total Tasks"), "fieldtype": "Data", "width": 120},
|
||||
{
|
||||
"fieldname": "completed_tasks",
|
||||
"label": _("Tasks Completed"),
|
||||
"fieldtype": "Data",
|
||||
"width": 120
|
||||
},
|
||||
{
|
||||
"fieldname": "overdue_tasks",
|
||||
"label": _("Tasks Overdue"),
|
||||
"fieldtype": "Data",
|
||||
"width": 120
|
||||
},
|
||||
{
|
||||
"fieldname": "percent_complete",
|
||||
"label": _("Completion"),
|
||||
"fieldtype": "Data",
|
||||
"width": 120
|
||||
"width": 120,
|
||||
},
|
||||
{"fieldname": "overdue_tasks", "label": _("Tasks Overdue"), "fieldtype": "Data", "width": 120},
|
||||
{"fieldname": "percent_complete", "label": _("Completion"), "fieldtype": "Data", "width": 120},
|
||||
{
|
||||
"fieldname": "expected_start_date",
|
||||
"label": _("Start Date"),
|
||||
"fieldtype": "Date",
|
||||
"width": 120
|
||||
},
|
||||
{
|
||||
"fieldname": "expected_end_date",
|
||||
"label": _("End Date"),
|
||||
"fieldtype": "Date",
|
||||
"width": 120
|
||||
"width": 120,
|
||||
},
|
||||
{"fieldname": "expected_end_date", "label": _("End Date"), "fieldtype": "Date", "width": 120},
|
||||
]
|
||||
|
||||
|
||||
def get_chart_data(data):
|
||||
labels = []
|
||||
total = []
|
||||
@@ -96,29 +89,19 @@ def get_chart_data(data):
|
||||
|
||||
return {
|
||||
"data": {
|
||||
'labels': labels[:30],
|
||||
'datasets': [
|
||||
{
|
||||
"name": "Overdue",
|
||||
"values": overdue[:30]
|
||||
},
|
||||
{
|
||||
"name": "Completed",
|
||||
"values": completed[:30]
|
||||
},
|
||||
{
|
||||
"name": "Total Tasks",
|
||||
"values": total[:30]
|
||||
},
|
||||
]
|
||||
"labels": labels[:30],
|
||||
"datasets": [
|
||||
{"name": "Overdue", "values": overdue[:30]},
|
||||
{"name": "Completed", "values": completed[:30]},
|
||||
{"name": "Total Tasks", "values": total[:30]},
|
||||
],
|
||||
},
|
||||
"type": "bar",
|
||||
"colors": ["#fc4f51", "#78d6ff", "#7575ff"],
|
||||
"barOptions": {
|
||||
"stacked": True
|
||||
}
|
||||
"barOptions": {"stacked": True},
|
||||
}
|
||||
|
||||
|
||||
def get_report_summary(data):
|
||||
if not data:
|
||||
return None
|
||||
@@ -152,5 +135,5 @@ def get_report_summary(data):
|
||||
"indicator": "Green" if total_overdue == 0 else "Red",
|
||||
"label": _("Overdue Tasks"),
|
||||
"datatype": "Int",
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
@@ -14,29 +14,56 @@ def execute(filters=None):
|
||||
|
||||
data = []
|
||||
for project in proj_details:
|
||||
data.append([project.name, pr_item_map.get(project.name, 0),
|
||||
se_item_map.get(project.name, 0), dn_item_map.get(project.name, 0),
|
||||
project.project_name, project.status, project.company,
|
||||
project.customer, project.estimated_costing, project.expected_start_date,
|
||||
project.expected_end_date])
|
||||
data.append(
|
||||
[
|
||||
project.name,
|
||||
pr_item_map.get(project.name, 0),
|
||||
se_item_map.get(project.name, 0),
|
||||
dn_item_map.get(project.name, 0),
|
||||
project.project_name,
|
||||
project.status,
|
||||
project.company,
|
||||
project.customer,
|
||||
project.estimated_costing,
|
||||
project.expected_start_date,
|
||||
project.expected_end_date,
|
||||
]
|
||||
)
|
||||
|
||||
return columns, data
|
||||
|
||||
|
||||
def get_columns():
|
||||
return [_("Project Id") + ":Link/Project:140", _("Cost of Purchased Items") + ":Currency:160",
|
||||
_("Cost of Issued Items") + ":Currency:160", _("Cost of Delivered Items") + ":Currency:160",
|
||||
_("Project Name") + "::120", _("Project Status") + "::120", _("Company") + ":Link/Company:100",
|
||||
_("Customer") + ":Link/Customer:140", _("Project Value") + ":Currency:120",
|
||||
_("Project Start Date") + ":Date:120", _("Completion Date") + ":Date:120"]
|
||||
return [
|
||||
_("Project Id") + ":Link/Project:140",
|
||||
_("Cost of Purchased Items") + ":Currency:160",
|
||||
_("Cost of Issued Items") + ":Currency:160",
|
||||
_("Cost of Delivered Items") + ":Currency:160",
|
||||
_("Project Name") + "::120",
|
||||
_("Project Status") + "::120",
|
||||
_("Company") + ":Link/Company:100",
|
||||
_("Customer") + ":Link/Customer:140",
|
||||
_("Project Value") + ":Currency:120",
|
||||
_("Project Start Date") + ":Date:120",
|
||||
_("Completion Date") + ":Date:120",
|
||||
]
|
||||
|
||||
|
||||
def get_project_details():
|
||||
return frappe.db.sql(""" select name, project_name, status, company, customer, estimated_costing,
|
||||
expected_start_date, expected_end_date from tabProject where docstatus < 2""", as_dict=1)
|
||||
return frappe.db.sql(
|
||||
""" select name, project_name, status, company, customer, estimated_costing,
|
||||
expected_start_date, expected_end_date from tabProject where docstatus < 2""",
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
|
||||
def get_purchased_items_cost():
|
||||
pr_items = frappe.db.sql("""select project, sum(base_net_amount) as amount
|
||||
pr_items = frappe.db.sql(
|
||||
"""select project, sum(base_net_amount) as amount
|
||||
from `tabPurchase Receipt Item` where ifnull(project, '') != ''
|
||||
and docstatus = 1 group by project""", as_dict=1)
|
||||
and docstatus = 1 group by project""",
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
pr_item_map = {}
|
||||
for item in pr_items:
|
||||
@@ -44,11 +71,15 @@ def get_purchased_items_cost():
|
||||
|
||||
return pr_item_map
|
||||
|
||||
|
||||
def get_issued_items_cost():
|
||||
se_items = frappe.db.sql("""select se.project, sum(se_item.amount) as amount
|
||||
se_items = frappe.db.sql(
|
||||
"""select se.project, sum(se_item.amount) as amount
|
||||
from `tabStock Entry` se, `tabStock Entry Detail` se_item
|
||||
where se.name = se_item.parent and se.docstatus = 1 and ifnull(se_item.t_warehouse, '') = ''
|
||||
and ifnull(se.project, '') != '' group by se.project""", as_dict=1)
|
||||
and ifnull(se.project, '') != '' group by se.project""",
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
se_item_map = {}
|
||||
for item in se_items:
|
||||
@@ -56,18 +87,24 @@ def get_issued_items_cost():
|
||||
|
||||
return se_item_map
|
||||
|
||||
|
||||
def get_delivered_items_cost():
|
||||
dn_items = frappe.db.sql("""select dn.project, sum(dn_item.base_net_amount) as amount
|
||||
dn_items = frappe.db.sql(
|
||||
"""select dn.project, sum(dn_item.base_net_amount) as amount
|
||||
from `tabDelivery Note` dn, `tabDelivery Note Item` dn_item
|
||||
where dn.name = dn_item.parent and dn.docstatus = 1 and ifnull(dn.project, '') != ''
|
||||
group by dn.project""", as_dict=1)
|
||||
group by dn.project""",
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
si_items = frappe.db.sql("""select si.project, sum(si_item.base_net_amount) as amount
|
||||
si_items = frappe.db.sql(
|
||||
"""select si.project, sum(si_item.base_net_amount) as amount
|
||||
from `tabSales Invoice` si, `tabSales Invoice Item` si_item
|
||||
where si.name = si_item.parent and si.docstatus = 1 and si.update_stock = 1
|
||||
and si.is_pos = 1 and ifnull(si.project, '') != ''
|
||||
group by si.project""", as_dict=1)
|
||||
|
||||
group by si.project""",
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
dn_item_map = {}
|
||||
for item in dn_items:
|
||||
|
||||
Reference in New Issue
Block a user