style: format code with black

This commit is contained in:
Ankush Menat
2022-03-28 18:52:46 +05:30
parent 21e00da3d6
commit 494bd9ef78
1395 changed files with 91704 additions and 62532 deletions

View File

@@ -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)

View File

@@ -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"):

View File

@@ -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

View File

@@ -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]

View File

@@ -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},
}

View File

@@ -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,
},
]

View File

@@ -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,
},
]

View File

@@ -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

View File

@@ -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",
}
},
]

View File

@@ -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: