mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-24 15:39:20 +00:00
style: format code with black
This commit is contained in:
@@ -7,7 +7,9 @@ from frappe import _
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class DuplicationError(frappe.ValidationError): pass
|
||||
class DuplicationError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class ActivityCost(Document):
|
||||
def validate(self):
|
||||
@@ -24,12 +26,22 @@ class ActivityCost(Document):
|
||||
|
||||
def check_unique(self):
|
||||
if self.employee:
|
||||
if frappe.db.sql("""select name from `tabActivity Cost` where employee_name= %s and activity_type= %s and name != %s""",
|
||||
(self.employee_name, self.activity_type, self.name)):
|
||||
frappe.throw(_("Activity Cost exists for Employee {0} against Activity Type - {1}")
|
||||
.format(self.employee, self.activity_type), DuplicationError)
|
||||
if frappe.db.sql(
|
||||
"""select name from `tabActivity Cost` where employee_name= %s and activity_type= %s and name != %s""",
|
||||
(self.employee_name, self.activity_type, self.name),
|
||||
):
|
||||
frappe.throw(
|
||||
_("Activity Cost exists for Employee {0} against Activity Type - {1}").format(
|
||||
self.employee, self.activity_type
|
||||
),
|
||||
DuplicationError,
|
||||
)
|
||||
else:
|
||||
if frappe.db.sql("""select name from `tabActivity Cost` where ifnull(employee, '')='' and activity_type= %s and name != %s""",
|
||||
(self.activity_type, self.name)):
|
||||
frappe.throw(_("Default Activity Cost exists for Activity Type - {0}")
|
||||
.format(self.activity_type), DuplicationError)
|
||||
if frappe.db.sql(
|
||||
"""select name from `tabActivity Cost` where ifnull(employee, '')='' and activity_type= %s and name != %s""",
|
||||
(self.activity_type, self.name),
|
||||
):
|
||||
frappe.throw(
|
||||
_("Default Activity Cost exists for Activity Type - {0}").format(self.activity_type),
|
||||
DuplicationError,
|
||||
)
|
||||
|
||||
@@ -11,15 +11,17 @@ from erpnext.projects.doctype.activity_cost.activity_cost import DuplicationErro
|
||||
class TestActivityCost(unittest.TestCase):
|
||||
def test_duplication(self):
|
||||
frappe.db.sql("delete from `tabActivity Cost`")
|
||||
activity_cost1 = frappe.new_doc('Activity Cost')
|
||||
activity_cost1.update({
|
||||
"employee": "_T-Employee-00001",
|
||||
"employee_name": "_Test Employee",
|
||||
"activity_type": "_Test Activity Type 1",
|
||||
"billing_rate": 100,
|
||||
"costing_rate": 50
|
||||
})
|
||||
activity_cost1 = frappe.new_doc("Activity Cost")
|
||||
activity_cost1.update(
|
||||
{
|
||||
"employee": "_T-Employee-00001",
|
||||
"employee_name": "_Test Employee",
|
||||
"activity_type": "_Test Activity Type 1",
|
||||
"billing_rate": 100,
|
||||
"costing_rate": 50,
|
||||
}
|
||||
)
|
||||
activity_cost1.insert()
|
||||
activity_cost2 = frappe.copy_doc(activity_cost1)
|
||||
self.assertRaises(DuplicationError, activity_cost2.insert )
|
||||
self.assertRaises(DuplicationError, activity_cost2.insert)
|
||||
frappe.db.sql("delete from `tabActivity Cost`")
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
|
||||
import frappe
|
||||
|
||||
test_records = frappe.get_test_records('Activity Type')
|
||||
test_records = frappe.get_test_records("Activity Type")
|
||||
|
||||
@@ -18,20 +18,26 @@ from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
|
||||
|
||||
class Project(Document):
|
||||
def get_feed(self):
|
||||
return '{0}: {1}'.format(_(self.status), frappe.safe_decode(self.project_name))
|
||||
return "{0}: {1}".format(_(self.status), frappe.safe_decode(self.project_name))
|
||||
|
||||
def onload(self):
|
||||
self.set_onload('activity_summary', frappe.db.sql('''select activity_type,
|
||||
self.set_onload(
|
||||
"activity_summary",
|
||||
frappe.db.sql(
|
||||
"""select activity_type,
|
||||
sum(hours) as total_hours
|
||||
from `tabTimesheet Detail` where project=%s and docstatus < 2 group by activity_type
|
||||
order by total_hours desc''', self.name, as_dict=True))
|
||||
order by total_hours desc""",
|
||||
self.name,
|
||||
as_dict=True,
|
||||
),
|
||||
)
|
||||
|
||||
self.update_costing()
|
||||
|
||||
def before_print(self, settings=None):
|
||||
self.onload()
|
||||
|
||||
|
||||
def validate(self):
|
||||
if not self.is_new():
|
||||
self.copy_from_template()
|
||||
@@ -41,17 +47,17 @@ class Project(Document):
|
||||
update_employee_boarding_status(self)
|
||||
|
||||
def copy_from_template(self):
|
||||
'''
|
||||
"""
|
||||
Copy tasks from template
|
||||
'''
|
||||
if self.project_template and not frappe.db.get_all('Task', dict(project = self.name), limit=1):
|
||||
"""
|
||||
if self.project_template and not frappe.db.get_all("Task", dict(project=self.name), limit=1):
|
||||
|
||||
# has a template, and no loaded tasks, so lets create
|
||||
if not self.expected_start_date:
|
||||
# project starts today
|
||||
self.expected_start_date = today()
|
||||
|
||||
template = frappe.get_doc('Project Template', self.project_template)
|
||||
template = frappe.get_doc("Project Template", self.project_template)
|
||||
|
||||
if not self.project_type:
|
||||
self.project_type = template.project_type
|
||||
@@ -67,19 +73,21 @@ class Project(Document):
|
||||
self.dependency_mapping(tmp_task_details, project_tasks)
|
||||
|
||||
def create_task_from_template(self, task_details):
|
||||
return frappe.get_doc(dict(
|
||||
doctype = 'Task',
|
||||
subject = task_details.subject,
|
||||
project = self.name,
|
||||
status = 'Open',
|
||||
exp_start_date = self.calculate_start_date(task_details),
|
||||
exp_end_date = self.calculate_end_date(task_details),
|
||||
description = task_details.description,
|
||||
task_weight = task_details.task_weight,
|
||||
type = task_details.type,
|
||||
issue = task_details.issue,
|
||||
is_group = task_details.is_group
|
||||
)).insert()
|
||||
return frappe.get_doc(
|
||||
dict(
|
||||
doctype="Task",
|
||||
subject=task_details.subject,
|
||||
project=self.name,
|
||||
status="Open",
|
||||
exp_start_date=self.calculate_start_date(task_details),
|
||||
exp_end_date=self.calculate_end_date(task_details),
|
||||
description=task_details.description,
|
||||
task_weight=task_details.task_weight,
|
||||
type=task_details.type,
|
||||
issue=task_details.issue,
|
||||
is_group=task_details.is_group,
|
||||
)
|
||||
).insert()
|
||||
|
||||
def calculate_start_date(self, task_details):
|
||||
self.start_date = add_days(self.expected_start_date, task_details.start)
|
||||
@@ -107,23 +115,26 @@ class Project(Document):
|
||||
if template_task.get("depends_on") and not project_task.get("depends_on"):
|
||||
for child_task in template_task.get("depends_on"):
|
||||
child_task_subject = frappe.db.get_value("Task", child_task.task, "subject")
|
||||
corresponding_project_task = list(filter(lambda x: x.subject == child_task_subject, project_tasks))
|
||||
corresponding_project_task = list(
|
||||
filter(lambda x: x.subject == child_task_subject, project_tasks)
|
||||
)
|
||||
if len(corresponding_project_task):
|
||||
project_task.append("depends_on",{
|
||||
"task": corresponding_project_task[0].name
|
||||
})
|
||||
project_task.append("depends_on", {"task": corresponding_project_task[0].name})
|
||||
project_task.save()
|
||||
|
||||
def check_for_parent_tasks(self, template_task, project_task, project_tasks):
|
||||
if template_task.get("parent_task") and not project_task.get("parent_task"):
|
||||
parent_task_subject = frappe.db.get_value("Task", template_task.get("parent_task"), "subject")
|
||||
corresponding_project_task = list(filter(lambda x: x.subject == parent_task_subject, project_tasks))
|
||||
corresponding_project_task = list(
|
||||
filter(lambda x: x.subject == parent_task_subject, project_tasks)
|
||||
)
|
||||
if len(corresponding_project_task):
|
||||
project_task.parent_task = corresponding_project_task[0].name
|
||||
project_task.save()
|
||||
|
||||
def is_row_updated(self, row, existing_task_data, fields):
|
||||
if self.get("__islocal") or not existing_task_data: return True
|
||||
if self.get("__islocal") or not existing_task_data:
|
||||
return True
|
||||
|
||||
d = existing_task_data.get(row.task_id, {})
|
||||
|
||||
@@ -132,7 +143,7 @@ class Project(Document):
|
||||
return True
|
||||
|
||||
def update_project(self):
|
||||
'''Called externally by Task'''
|
||||
"""Called externally by Task"""
|
||||
self.update_percent_complete()
|
||||
update_employee_boarding_status(self)
|
||||
self.update_costing()
|
||||
@@ -152,52 +163,74 @@ class Project(Document):
|
||||
self.percent_complete = 100
|
||||
return
|
||||
|
||||
total = frappe.db.count('Task', dict(project=self.name))
|
||||
total = frappe.db.count("Task", dict(project=self.name))
|
||||
|
||||
if not total:
|
||||
self.percent_complete = 0
|
||||
else:
|
||||
if (self.percent_complete_method == "Task Completion" and total > 0) or (
|
||||
not self.percent_complete_method and total > 0):
|
||||
completed = frappe.db.sql("""select count(name) from tabTask where
|
||||
project=%s and status in ('Cancelled', 'Completed')""", self.name)[0][0]
|
||||
not self.percent_complete_method and total > 0
|
||||
):
|
||||
completed = frappe.db.sql(
|
||||
"""select count(name) from tabTask where
|
||||
project=%s and status in ('Cancelled', 'Completed')""",
|
||||
self.name,
|
||||
)[0][0]
|
||||
self.percent_complete = flt(flt(completed) / total * 100, 2)
|
||||
|
||||
if (self.percent_complete_method == "Task Progress" and total > 0):
|
||||
progress = frappe.db.sql("""select sum(progress) from tabTask where
|
||||
project=%s""", self.name)[0][0]
|
||||
if self.percent_complete_method == "Task Progress" and total > 0:
|
||||
progress = frappe.db.sql(
|
||||
"""select sum(progress) from tabTask where
|
||||
project=%s""",
|
||||
self.name,
|
||||
)[0][0]
|
||||
self.percent_complete = flt(flt(progress) / total, 2)
|
||||
|
||||
if (self.percent_complete_method == "Task Weight" and total > 0):
|
||||
weight_sum = frappe.db.sql("""select sum(task_weight) from tabTask where
|
||||
project=%s""", self.name)[0][0]
|
||||
weighted_progress = frappe.db.sql("""select progress, task_weight from tabTask where
|
||||
project=%s""", self.name, as_dict=1)
|
||||
if self.percent_complete_method == "Task Weight" and total > 0:
|
||||
weight_sum = frappe.db.sql(
|
||||
"""select sum(task_weight) from tabTask where
|
||||
project=%s""",
|
||||
self.name,
|
||||
)[0][0]
|
||||
weighted_progress = frappe.db.sql(
|
||||
"""select progress, task_weight from tabTask where
|
||||
project=%s""",
|
||||
self.name,
|
||||
as_dict=1,
|
||||
)
|
||||
pct_complete = 0
|
||||
for row in weighted_progress:
|
||||
pct_complete += row["progress"] * frappe.utils.safe_div(row["task_weight"], weight_sum)
|
||||
self.percent_complete = flt(flt(pct_complete), 2)
|
||||
|
||||
# don't update status if it is cancelled
|
||||
if self.status == 'Cancelled':
|
||||
if self.status == "Cancelled":
|
||||
return
|
||||
|
||||
if self.percent_complete == 100:
|
||||
self.status = "Completed"
|
||||
|
||||
def update_costing(self):
|
||||
from_time_sheet = frappe.db.sql("""select
|
||||
from_time_sheet = frappe.db.sql(
|
||||
"""select
|
||||
sum(costing_amount) as costing_amount,
|
||||
sum(billing_amount) as billing_amount,
|
||||
min(from_time) as start_date,
|
||||
max(to_time) as end_date,
|
||||
sum(hours) as time
|
||||
from `tabTimesheet Detail` where project = %s and docstatus = 1""", self.name, as_dict=1)[0]
|
||||
from `tabTimesheet Detail` where project = %s and docstatus = 1""",
|
||||
self.name,
|
||||
as_dict=1,
|
||||
)[0]
|
||||
|
||||
from_expense_claim = frappe.db.sql("""select
|
||||
from_expense_claim = frappe.db.sql(
|
||||
"""select
|
||||
sum(total_sanctioned_amount) as total_sanctioned_amount
|
||||
from `tabExpense Claim` where project = %s
|
||||
and docstatus = 1""", self.name, as_dict=1)[0]
|
||||
and docstatus = 1""",
|
||||
self.name,
|
||||
as_dict=1,
|
||||
)[0]
|
||||
|
||||
self.actual_start_date = from_time_sheet.start_date
|
||||
self.actual_end_date = from_time_sheet.end_date
|
||||
@@ -213,41 +246,54 @@ class Project(Document):
|
||||
self.calculate_gross_margin()
|
||||
|
||||
def calculate_gross_margin(self):
|
||||
expense_amount = (flt(self.total_costing_amount) + flt(self.total_expense_claim)
|
||||
+ flt(self.total_purchase_cost) + flt(self.get('total_consumed_material_cost', 0)))
|
||||
expense_amount = (
|
||||
flt(self.total_costing_amount)
|
||||
+ flt(self.total_expense_claim)
|
||||
+ flt(self.total_purchase_cost)
|
||||
+ flt(self.get("total_consumed_material_cost", 0))
|
||||
)
|
||||
|
||||
self.gross_margin = flt(self.total_billed_amount) - expense_amount
|
||||
if self.total_billed_amount:
|
||||
self.per_gross_margin = (self.gross_margin / flt(self.total_billed_amount)) * 100
|
||||
|
||||
def update_purchase_costing(self):
|
||||
total_purchase_cost = frappe.db.sql("""select sum(base_net_amount)
|
||||
from `tabPurchase Invoice Item` where project = %s and docstatus=1""", self.name)
|
||||
total_purchase_cost = frappe.db.sql(
|
||||
"""select sum(base_net_amount)
|
||||
from `tabPurchase Invoice Item` where project = %s and docstatus=1""",
|
||||
self.name,
|
||||
)
|
||||
|
||||
self.total_purchase_cost = total_purchase_cost and total_purchase_cost[0][0] or 0
|
||||
|
||||
def update_sales_amount(self):
|
||||
total_sales_amount = frappe.db.sql("""select sum(base_net_total)
|
||||
from `tabSales Order` where project = %s and docstatus=1""", self.name)
|
||||
total_sales_amount = frappe.db.sql(
|
||||
"""select sum(base_net_total)
|
||||
from `tabSales Order` where project = %s and docstatus=1""",
|
||||
self.name,
|
||||
)
|
||||
|
||||
self.total_sales_amount = total_sales_amount and total_sales_amount[0][0] or 0
|
||||
|
||||
def update_billed_amount(self):
|
||||
total_billed_amount = frappe.db.sql("""select sum(base_net_total)
|
||||
from `tabSales Invoice` where project = %s and docstatus=1""", self.name)
|
||||
total_billed_amount = frappe.db.sql(
|
||||
"""select sum(base_net_total)
|
||||
from `tabSales Invoice` where project = %s and docstatus=1""",
|
||||
self.name,
|
||||
)
|
||||
|
||||
self.total_billed_amount = total_billed_amount and total_billed_amount[0][0] or 0
|
||||
|
||||
def after_rename(self, old_name, new_name, merge=False):
|
||||
if old_name == self.copied_from:
|
||||
frappe.db.set_value('Project', new_name, 'copied_from', new_name)
|
||||
frappe.db.set_value("Project", new_name, "copied_from", new_name)
|
||||
|
||||
def send_welcome_email(self):
|
||||
url = get_url("/project/?name={0}".format(self.name))
|
||||
messages = (
|
||||
_("You have been invited to collaborate on the project: {0}").format(self.name),
|
||||
url,
|
||||
_("Join")
|
||||
_("Join"),
|
||||
)
|
||||
|
||||
content = """
|
||||
@@ -257,21 +303,31 @@ class Project(Document):
|
||||
|
||||
for user in self.users:
|
||||
if user.welcome_email_sent == 0:
|
||||
frappe.sendmail(user.user, subject=_("Project Collaboration Invitation"),
|
||||
content=content.format(*messages))
|
||||
frappe.sendmail(
|
||||
user.user, subject=_("Project Collaboration Invitation"), content=content.format(*messages)
|
||||
)
|
||||
user.welcome_email_sent = 1
|
||||
|
||||
|
||||
def get_timeline_data(doctype, name):
|
||||
'''Return timeline for attendance'''
|
||||
return dict(frappe.db.sql('''select unix_timestamp(from_time), count(*)
|
||||
"""Return timeline for attendance"""
|
||||
return dict(
|
||||
frappe.db.sql(
|
||||
"""select unix_timestamp(from_time), count(*)
|
||||
from `tabTimesheet Detail` where project=%s
|
||||
and from_time > date_sub(curdate(), interval 1 year)
|
||||
and docstatus < 2
|
||||
group by date(from_time)''', name))
|
||||
group by date(from_time)""",
|
||||
name,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_project_list(doctype, txt, filters, limit_start, limit_page_length=20, order_by="modified"):
|
||||
return frappe.db.sql('''select distinct project.*
|
||||
def get_project_list(
|
||||
doctype, txt, filters, limit_start, limit_page_length=20, order_by="modified"
|
||||
):
|
||||
return frappe.db.sql(
|
||||
"""select distinct project.*
|
||||
from tabProject project, `tabProject User` project_user
|
||||
where
|
||||
(project_user.user = %(user)s
|
||||
@@ -279,27 +335,32 @@ def get_project_list(doctype, txt, filters, limit_start, limit_page_length=20, o
|
||||
or project.owner = %(user)s
|
||||
order by project.modified desc
|
||||
limit {0}, {1}
|
||||
'''.format(limit_start, limit_page_length),
|
||||
{'user': frappe.session.user},
|
||||
as_dict=True,
|
||||
update={'doctype': 'Project'})
|
||||
""".format(
|
||||
limit_start, limit_page_length
|
||||
),
|
||||
{"user": frappe.session.user},
|
||||
as_dict=True,
|
||||
update={"doctype": "Project"},
|
||||
)
|
||||
|
||||
|
||||
def get_list_context(context=None):
|
||||
return {
|
||||
"show_sidebar": True,
|
||||
"show_search": True,
|
||||
'no_breadcrumbs': True,
|
||||
"no_breadcrumbs": True,
|
||||
"title": _("Projects"),
|
||||
"get_list": get_project_list,
|
||||
"row_template": "templates/includes/projects/project_row.html"
|
||||
"row_template": "templates/includes/projects/project_row.html",
|
||||
}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_users_for_project(doctype, txt, searchfield, start, page_len, filters):
|
||||
conditions = []
|
||||
return frappe.db.sql("""select name, concat_ws(' ', first_name, middle_name, last_name)
|
||||
return frappe.db.sql(
|
||||
"""select name, concat_ws(' ', first_name, middle_name, last_name)
|
||||
from `tabUser`
|
||||
where enabled=1
|
||||
and name not in ("Guest", "Administrator")
|
||||
@@ -311,47 +372,51 @@ def get_users_for_project(doctype, txt, searchfield, start, page_len, filters):
|
||||
if(locate(%(_txt)s, full_name), locate(%(_txt)s, full_name), 99999),
|
||||
idx desc,
|
||||
name, full_name
|
||||
limit %(start)s, %(page_len)s""".format(**{
|
||||
'key': searchfield,
|
||||
'fcond': get_filters_cond(doctype, filters, conditions),
|
||||
'mcond': get_match_cond(doctype)
|
||||
}), {
|
||||
'txt': "%%%s%%" % txt,
|
||||
'_txt': txt.replace("%", ""),
|
||||
'start': start,
|
||||
'page_len': page_len
|
||||
})
|
||||
limit %(start)s, %(page_len)s""".format(
|
||||
**{
|
||||
"key": searchfield,
|
||||
"fcond": get_filters_cond(doctype, filters, conditions),
|
||||
"mcond": get_match_cond(doctype),
|
||||
}
|
||||
),
|
||||
{"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len},
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_cost_center_name(project):
|
||||
return frappe.db.get_value("Project", project, "cost_center")
|
||||
|
||||
|
||||
def hourly_reminder():
|
||||
fields = ["from_time", "to_time"]
|
||||
projects = get_projects_for_collect_progress("Hourly", fields)
|
||||
|
||||
for project in projects:
|
||||
if (get_time(nowtime()) >= get_time(project.from_time) or
|
||||
get_time(nowtime()) <= get_time(project.to_time)):
|
||||
if get_time(nowtime()) >= get_time(project.from_time) or get_time(nowtime()) <= get_time(
|
||||
project.to_time
|
||||
):
|
||||
send_project_update_email_to_users(project.name)
|
||||
|
||||
|
||||
def project_status_update_reminder():
|
||||
daily_reminder()
|
||||
twice_daily_reminder()
|
||||
weekly_reminder()
|
||||
|
||||
|
||||
def daily_reminder():
|
||||
fields = ["daily_time_to_send"]
|
||||
projects = get_projects_for_collect_progress("Daily", fields)
|
||||
projects = get_projects_for_collect_progress("Daily", fields)
|
||||
|
||||
for project in projects:
|
||||
if allow_to_make_project_update(project.name, project.get("daily_time_to_send"), "Daily"):
|
||||
send_project_update_email_to_users(project.name)
|
||||
|
||||
|
||||
def twice_daily_reminder():
|
||||
fields = ["first_email", "second_email"]
|
||||
projects = get_projects_for_collect_progress("Twice Daily", fields)
|
||||
projects = get_projects_for_collect_progress("Twice Daily", fields)
|
||||
fields.remove("name")
|
||||
|
||||
for project in projects:
|
||||
@@ -359,9 +424,10 @@ def twice_daily_reminder():
|
||||
if allow_to_make_project_update(project.name, project.get(d), "Twicely"):
|
||||
send_project_update_email_to_users(project.name)
|
||||
|
||||
|
||||
def weekly_reminder():
|
||||
fields = ["day_to_send", "weekly_time_to_send"]
|
||||
projects = get_projects_for_collect_progress("Weekly", fields)
|
||||
projects = get_projects_for_collect_progress("Weekly", fields)
|
||||
|
||||
current_day = get_datetime().strftime("%A")
|
||||
for project in projects:
|
||||
@@ -371,12 +437,16 @@ def weekly_reminder():
|
||||
if allow_to_make_project_update(project.name, project.get("weekly_time_to_send"), "Weekly"):
|
||||
send_project_update_email_to_users(project.name)
|
||||
|
||||
|
||||
def allow_to_make_project_update(project, time, frequency):
|
||||
data = frappe.db.sql(""" SELECT name from `tabProject Update`
|
||||
WHERE project = %s and date = %s """, (project, today()))
|
||||
data = frappe.db.sql(
|
||||
""" SELECT name from `tabProject Update`
|
||||
WHERE project = %s and date = %s """,
|
||||
(project, today()),
|
||||
)
|
||||
|
||||
# len(data) > 1 condition is checked for twicely frequency
|
||||
if data and (frequency in ['Daily', 'Weekly'] or len(data) > 1):
|
||||
if data and (frequency in ["Daily", "Weekly"] or len(data) > 1):
|
||||
return False
|
||||
|
||||
if get_time(nowtime()) >= get_time(time):
|
||||
@@ -385,138 +455,162 @@ def allow_to_make_project_update(project, time, frequency):
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_duplicate_project(prev_doc, project_name):
|
||||
''' Create duplicate project based on the old project '''
|
||||
"""Create duplicate project based on the old project"""
|
||||
import json
|
||||
|
||||
prev_doc = json.loads(prev_doc)
|
||||
|
||||
if project_name == prev_doc.get('name'):
|
||||
if project_name == prev_doc.get("name"):
|
||||
frappe.throw(_("Use a name that is different from previous project name"))
|
||||
|
||||
# change the copied doc name to new project name
|
||||
project = frappe.copy_doc(prev_doc)
|
||||
project.name = project_name
|
||||
project.project_template = ''
|
||||
project.project_template = ""
|
||||
project.project_name = project_name
|
||||
project.insert()
|
||||
|
||||
# fetch all the task linked with the old project
|
||||
task_list = frappe.get_all("Task", filters={
|
||||
'project': prev_doc.get('name')
|
||||
}, fields=['name'])
|
||||
task_list = frappe.get_all("Task", filters={"project": prev_doc.get("name")}, fields=["name"])
|
||||
|
||||
# Create duplicate task for all the task
|
||||
for task in task_list:
|
||||
task = frappe.get_doc('Task', task)
|
||||
task = frappe.get_doc("Task", task)
|
||||
new_task = frappe.copy_doc(task)
|
||||
new_task.project = project.name
|
||||
new_task.insert()
|
||||
|
||||
project.db_set('project_template', prev_doc.get('project_template'))
|
||||
project.db_set("project_template", prev_doc.get("project_template"))
|
||||
|
||||
|
||||
def get_projects_for_collect_progress(frequency, fields):
|
||||
fields.extend(["name"])
|
||||
|
||||
return frappe.get_all("Project", fields = fields,
|
||||
filters = {'collect_progress': 1, 'frequency': frequency, 'status': 'Open'})
|
||||
return frappe.get_all(
|
||||
"Project",
|
||||
fields=fields,
|
||||
filters={"collect_progress": 1, "frequency": frequency, "status": "Open"},
|
||||
)
|
||||
|
||||
|
||||
def send_project_update_email_to_users(project):
|
||||
doc = frappe.get_doc('Project', project)
|
||||
doc = frappe.get_doc("Project", project)
|
||||
|
||||
if is_holiday(doc.holiday_list) or not doc.users: return
|
||||
if is_holiday(doc.holiday_list) or not doc.users:
|
||||
return
|
||||
|
||||
project_update = frappe.get_doc({
|
||||
"doctype" : "Project Update",
|
||||
"project" : project,
|
||||
"sent": 0,
|
||||
"date": today(),
|
||||
"time": nowtime(),
|
||||
"naming_series": "UPDATE-.project.-.YY.MM.DD.-",
|
||||
}).insert()
|
||||
project_update = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Project Update",
|
||||
"project": project,
|
||||
"sent": 0,
|
||||
"date": today(),
|
||||
"time": nowtime(),
|
||||
"naming_series": "UPDATE-.project.-.YY.MM.DD.-",
|
||||
}
|
||||
).insert()
|
||||
|
||||
subject = "For project %s, update your status" % (project)
|
||||
|
||||
incoming_email_account = frappe.db.get_value('Email Account',
|
||||
dict(enable_incoming=1, default_incoming=1), 'email_id')
|
||||
incoming_email_account = frappe.db.get_value(
|
||||
"Email Account", dict(enable_incoming=1, default_incoming=1), "email_id"
|
||||
)
|
||||
|
||||
frappe.sendmail(recipients=get_users_email(doc),
|
||||
frappe.sendmail(
|
||||
recipients=get_users_email(doc),
|
||||
message=doc.message,
|
||||
subject=_(subject),
|
||||
reference_doctype=project_update.doctype,
|
||||
reference_name=project_update.name,
|
||||
reply_to=incoming_email_account
|
||||
reply_to=incoming_email_account,
|
||||
)
|
||||
|
||||
|
||||
def collect_project_status():
|
||||
for data in frappe.get_all("Project Update",
|
||||
{'date': today(), 'sent': 0}):
|
||||
replies = frappe.get_all('Communication',
|
||||
fields=['content', 'text_content', 'sender'],
|
||||
filters=dict(reference_doctype="Project Update",
|
||||
for data in frappe.get_all("Project Update", {"date": today(), "sent": 0}):
|
||||
replies = frappe.get_all(
|
||||
"Communication",
|
||||
fields=["content", "text_content", "sender"],
|
||||
filters=dict(
|
||||
reference_doctype="Project Update",
|
||||
reference_name=data.name,
|
||||
communication_type='Communication',
|
||||
sent_or_received='Received'),
|
||||
order_by='creation asc')
|
||||
communication_type="Communication",
|
||||
sent_or_received="Received",
|
||||
),
|
||||
order_by="creation asc",
|
||||
)
|
||||
|
||||
for d in replies:
|
||||
doc = frappe.get_doc("Project Update", data.name)
|
||||
user_data = frappe.db.get_values("User", {"email": d.sender},
|
||||
["full_name", "user_image", "name"], as_dict=True)[0]
|
||||
user_data = frappe.db.get_values(
|
||||
"User", {"email": d.sender}, ["full_name", "user_image", "name"], as_dict=True
|
||||
)[0]
|
||||
|
||||
doc.append("users", {
|
||||
'user': user_data.name,
|
||||
'full_name': user_data.full_name,
|
||||
'image': user_data.user_image,
|
||||
'project_status': frappe.utils.md_to_html(
|
||||
EmailReplyParser.parse_reply(d.text_content) or d.content
|
||||
)
|
||||
})
|
||||
doc.append(
|
||||
"users",
|
||||
{
|
||||
"user": user_data.name,
|
||||
"full_name": user_data.full_name,
|
||||
"image": user_data.user_image,
|
||||
"project_status": frappe.utils.md_to_html(
|
||||
EmailReplyParser.parse_reply(d.text_content) or d.content
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
doc.save(ignore_permissions=True)
|
||||
|
||||
|
||||
def send_project_status_email_to_users():
|
||||
yesterday = add_days(today(), -1)
|
||||
|
||||
for d in frappe.get_all("Project Update",
|
||||
{'date': yesterday, 'sent': 0}):
|
||||
for d in frappe.get_all("Project Update", {"date": yesterday, "sent": 0}):
|
||||
doc = frappe.get_doc("Project Update", d.name)
|
||||
|
||||
project_doc = frappe.get_doc('Project', doc.project)
|
||||
project_doc = frappe.get_doc("Project", doc.project)
|
||||
|
||||
args = {
|
||||
"users": doc.users,
|
||||
"title": _("Project Summary for {0}").format(yesterday)
|
||||
}
|
||||
args = {"users": doc.users, "title": _("Project Summary for {0}").format(yesterday)}
|
||||
|
||||
frappe.sendmail(recipients=get_users_email(project_doc),
|
||||
template='daily_project_summary',
|
||||
frappe.sendmail(
|
||||
recipients=get_users_email(project_doc),
|
||||
template="daily_project_summary",
|
||||
args=args,
|
||||
subject=_("Daily Project Summary for {0}").format(d.name),
|
||||
reference_doctype="Project Update",
|
||||
reference_name=d.name)
|
||||
reference_name=d.name,
|
||||
)
|
||||
|
||||
doc.db_set("sent", 1)
|
||||
|
||||
doc.db_set('sent', 1)
|
||||
|
||||
def update_project_sales_billing():
|
||||
sales_update_frequency = frappe.db.get_single_value("Selling Settings", "sales_update_frequency")
|
||||
if sales_update_frequency == "Each Transaction":
|
||||
return
|
||||
elif (sales_update_frequency == "Monthly" and frappe.utils.now_datetime().day != 1):
|
||||
elif sales_update_frequency == "Monthly" and frappe.utils.now_datetime().day != 1:
|
||||
return
|
||||
|
||||
#Else simply fallback to Daily
|
||||
exists_query = '(SELECT 1 from `tab{doctype}` where docstatus = 1 and project = `tabProject`.name)'
|
||||
# Else simply fallback to Daily
|
||||
exists_query = (
|
||||
"(SELECT 1 from `tab{doctype}` where docstatus = 1 and project = `tabProject`.name)"
|
||||
)
|
||||
project_map = {}
|
||||
for project_details in frappe.db.sql('''
|
||||
for project_details in frappe.db.sql(
|
||||
"""
|
||||
SELECT name, 1 as order_exists, null as invoice_exists from `tabProject` where
|
||||
exists {order_exists}
|
||||
union
|
||||
SELECT name, null as order_exists, 1 as invoice_exists from `tabProject` where
|
||||
exists {invoice_exists}
|
||||
'''.format(
|
||||
""".format(
|
||||
order_exists=exists_query.format(doctype="Sales Order"),
|
||||
invoice_exists=exists_query.format(doctype="Sales Invoice"),
|
||||
), as_dict=True):
|
||||
project = project_map.setdefault(project_details.name, frappe.get_doc('Project', project_details.name))
|
||||
),
|
||||
as_dict=True,
|
||||
):
|
||||
project = project_map.setdefault(
|
||||
project_details.name, frappe.get_doc("Project", project_details.name)
|
||||
)
|
||||
if project_details.order_exists:
|
||||
project.update_sales_amount()
|
||||
if project_details.invoice_exists:
|
||||
@@ -525,29 +619,31 @@ def update_project_sales_billing():
|
||||
for project in project_map.values():
|
||||
project.save()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_kanban_board_if_not_exists(project):
|
||||
from frappe.desk.doctype.kanban_board.kanban_board import quick_kanban_board
|
||||
|
||||
project = frappe.get_doc('Project', project)
|
||||
if not frappe.db.exists('Kanban Board', project.project_name):
|
||||
quick_kanban_board('Task', project.project_name, 'status', project.name)
|
||||
project = frappe.get_doc("Project", project)
|
||||
if not frappe.db.exists("Kanban Board", project.project_name):
|
||||
quick_kanban_board("Task", project.project_name, "status", project.name)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def set_project_status(project, status):
|
||||
'''
|
||||
"""
|
||||
set status for project and all related tasks
|
||||
'''
|
||||
if not status in ('Completed', 'Cancelled'):
|
||||
frappe.throw(_('Status must be Cancelled or Completed'))
|
||||
"""
|
||||
if not status in ("Completed", "Cancelled"):
|
||||
frappe.throw(_("Status must be Cancelled or Completed"))
|
||||
|
||||
project = frappe.get_doc('Project', project)
|
||||
frappe.has_permission(doc = project, throw = True)
|
||||
project = frappe.get_doc("Project", project)
|
||||
frappe.has_permission(doc=project, throw=True)
|
||||
|
||||
for task in frappe.get_all('Task', dict(project = project.name)):
|
||||
frappe.db.set_value('Task', task.name, 'status', status)
|
||||
for task in frappe.get_all("Task", dict(project=project.name)):
|
||||
frappe.db.set_value("Task", task.name, "status", status)
|
||||
|
||||
project.status = status
|
||||
project.save()
|
||||
|
||||
@@ -3,25 +3,16 @@ from frappe import _
|
||||
|
||||
def get_data():
|
||||
return {
|
||||
'heatmap': True,
|
||||
'heatmap_message': _('This is based on the Time Sheets created against this project'),
|
||||
'fieldname': 'project',
|
||||
'transactions': [
|
||||
"heatmap": True,
|
||||
"heatmap_message": _("This is based on the Time Sheets created against this project"),
|
||||
"fieldname": "project",
|
||||
"transactions": [
|
||||
{
|
||||
'label': _('Project'),
|
||||
'items': ['Task', 'Timesheet', 'Expense Claim', 'Issue' , 'Project Update']
|
||||
"label": _("Project"),
|
||||
"items": ["Task", "Timesheet", "Expense Claim", "Issue", "Project Update"],
|
||||
},
|
||||
{
|
||||
'label': _('Material'),
|
||||
'items': ['Material Request', 'BOM', 'Stock Entry']
|
||||
},
|
||||
{
|
||||
'label': _('Sales'),
|
||||
'items': ['Sales Order', 'Delivery Note', 'Sales Invoice']
|
||||
},
|
||||
{
|
||||
'label': _('Purchase'),
|
||||
'items': ['Purchase Order', 'Purchase Receipt', 'Purchase Invoice']
|
||||
},
|
||||
]
|
||||
{"label": _("Material"), "items": ["Material Request", "BOM", "Stock Entry"]},
|
||||
{"label": _("Sales"), "items": ["Sales Order", "Delivery Note", "Sales Invoice"]},
|
||||
{"label": _("Purchase"), "items": ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ from erpnext.projects.doctype.task.test_task import create_task
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_project as make_project_from_so
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
|
||||
test_records = frappe.get_test_records('Project')
|
||||
test_records = frappe.get_test_records("Project")
|
||||
test_ignore = ["Sales Order"]
|
||||
|
||||
|
||||
@@ -19,53 +19,83 @@ class TestProject(unittest.TestCase):
|
||||
def test_project_with_template_having_no_parent_and_depend_tasks(self):
|
||||
project_name = "Test Project with Template - No Parent and Dependend Tasks"
|
||||
frappe.db.sql(""" delete from tabTask where project = %s """, project_name)
|
||||
frappe.delete_doc('Project', project_name)
|
||||
frappe.delete_doc("Project", project_name)
|
||||
|
||||
task1 = task_exists("Test Template Task with No Parent and Dependency")
|
||||
if not task1:
|
||||
task1 = create_task(subject="Test Template Task with No Parent and Dependency", is_template=1, begin=5, duration=3)
|
||||
task1 = create_task(
|
||||
subject="Test Template Task with No Parent and Dependency", is_template=1, begin=5, duration=3
|
||||
)
|
||||
|
||||
template = make_project_template("Test Project Template - No Parent and Dependend Tasks", [task1])
|
||||
template = make_project_template(
|
||||
"Test Project Template - No Parent and Dependend Tasks", [task1]
|
||||
)
|
||||
project = get_project(project_name, template)
|
||||
tasks = frappe.get_all('Task', ['subject','exp_end_date','depends_on_tasks'], dict(project=project.name), order_by='creation asc')
|
||||
tasks = frappe.get_all(
|
||||
"Task",
|
||||
["subject", "exp_end_date", "depends_on_tasks"],
|
||||
dict(project=project.name),
|
||||
order_by="creation asc",
|
||||
)
|
||||
|
||||
self.assertEqual(tasks[0].subject, 'Test Template Task with No Parent and Dependency')
|
||||
self.assertEqual(tasks[0].subject, "Test Template Task with No Parent and Dependency")
|
||||
self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 5, 3))
|
||||
self.assertEqual(len(tasks), 1)
|
||||
|
||||
def test_project_template_having_parent_child_tasks(self):
|
||||
project_name = "Test Project with Template - Tasks with Parent-Child Relation"
|
||||
|
||||
if frappe.db.get_value('Project', {'project_name': project_name}, 'name'):
|
||||
project_name = frappe.db.get_value('Project', {'project_name': project_name}, 'name')
|
||||
if frappe.db.get_value("Project", {"project_name": project_name}, "name"):
|
||||
project_name = frappe.db.get_value("Project", {"project_name": project_name}, "name")
|
||||
|
||||
frappe.db.sql(""" delete from tabTask where project = %s """, project_name)
|
||||
frappe.delete_doc('Project', project_name)
|
||||
frappe.delete_doc("Project", project_name)
|
||||
|
||||
task1 = task_exists("Test Template Task Parent")
|
||||
if not task1:
|
||||
task1 = create_task(subject="Test Template Task Parent", is_group=1, is_template=1, begin=1, duration=10)
|
||||
task1 = create_task(
|
||||
subject="Test Template Task Parent", is_group=1, is_template=1, begin=1, duration=10
|
||||
)
|
||||
|
||||
task2 = task_exists("Test Template Task Child 1")
|
||||
if not task2:
|
||||
task2 = create_task(subject="Test Template Task Child 1", parent_task=task1.name, is_template=1, begin=1, duration=3)
|
||||
task2 = create_task(
|
||||
subject="Test Template Task Child 1",
|
||||
parent_task=task1.name,
|
||||
is_template=1,
|
||||
begin=1,
|
||||
duration=3,
|
||||
)
|
||||
|
||||
task3 = task_exists("Test Template Task Child 2")
|
||||
if not task3:
|
||||
task3 = create_task(subject="Test Template Task Child 2", parent_task=task1.name, is_template=1, begin=2, duration=3)
|
||||
task3 = create_task(
|
||||
subject="Test Template Task Child 2",
|
||||
parent_task=task1.name,
|
||||
is_template=1,
|
||||
begin=2,
|
||||
duration=3,
|
||||
)
|
||||
|
||||
template = make_project_template("Test Project Template - Tasks with Parent-Child Relation", [task1, task2, task3])
|
||||
template = make_project_template(
|
||||
"Test Project Template - Tasks with Parent-Child Relation", [task1, task2, task3]
|
||||
)
|
||||
project = get_project(project_name, template)
|
||||
tasks = frappe.get_all('Task', ['subject','exp_end_date','depends_on_tasks', 'name', 'parent_task'], dict(project=project.name), order_by='creation asc')
|
||||
tasks = frappe.get_all(
|
||||
"Task",
|
||||
["subject", "exp_end_date", "depends_on_tasks", "name", "parent_task"],
|
||||
dict(project=project.name),
|
||||
order_by="creation asc",
|
||||
)
|
||||
|
||||
self.assertEqual(tasks[0].subject, 'Test Template Task Parent')
|
||||
self.assertEqual(tasks[0].subject, "Test Template Task Parent")
|
||||
self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 1, 10))
|
||||
|
||||
self.assertEqual(tasks[1].subject, 'Test Template Task Child 1')
|
||||
self.assertEqual(tasks[1].subject, "Test Template Task Child 1")
|
||||
self.assertEqual(getdate(tasks[1].exp_end_date), calculate_end_date(project, 1, 3))
|
||||
self.assertEqual(tasks[1].parent_task, tasks[0].name)
|
||||
|
||||
self.assertEqual(tasks[2].subject, 'Test Template Task Child 2')
|
||||
self.assertEqual(tasks[2].subject, "Test Template Task Child 2")
|
||||
self.assertEqual(getdate(tasks[2].exp_end_date), calculate_end_date(project, 2, 3))
|
||||
self.assertEqual(tasks[2].parent_task, tasks[0].name)
|
||||
|
||||
@@ -74,26 +104,39 @@ class TestProject(unittest.TestCase):
|
||||
def test_project_template_having_dependent_tasks(self):
|
||||
project_name = "Test Project with Template - Dependent Tasks"
|
||||
frappe.db.sql(""" delete from tabTask where project = %s """, project_name)
|
||||
frappe.delete_doc('Project', project_name)
|
||||
frappe.delete_doc("Project", project_name)
|
||||
|
||||
task1 = task_exists("Test Template Task for Dependency")
|
||||
if not task1:
|
||||
task1 = create_task(subject="Test Template Task for Dependency", is_template=1, begin=3, duration=1)
|
||||
task1 = create_task(
|
||||
subject="Test Template Task for Dependency", is_template=1, begin=3, duration=1
|
||||
)
|
||||
|
||||
task2 = task_exists("Test Template Task with Dependency")
|
||||
if not task2:
|
||||
task2 = create_task(subject="Test Template Task with Dependency", depends_on=task1.name, is_template=1, begin=2, duration=2)
|
||||
task2 = create_task(
|
||||
subject="Test Template Task with Dependency",
|
||||
depends_on=task1.name,
|
||||
is_template=1,
|
||||
begin=2,
|
||||
duration=2,
|
||||
)
|
||||
|
||||
template = make_project_template("Test Project with Template - Dependent Tasks", [task1, task2])
|
||||
project = get_project(project_name, template)
|
||||
tasks = frappe.get_all('Task', ['subject','exp_end_date','depends_on_tasks', 'name'], dict(project=project.name), order_by='creation asc')
|
||||
tasks = frappe.get_all(
|
||||
"Task",
|
||||
["subject", "exp_end_date", "depends_on_tasks", "name"],
|
||||
dict(project=project.name),
|
||||
order_by="creation asc",
|
||||
)
|
||||
|
||||
self.assertEqual(tasks[1].subject, 'Test Template Task with Dependency')
|
||||
self.assertEqual(tasks[1].subject, "Test Template Task with Dependency")
|
||||
self.assertEqual(getdate(tasks[1].exp_end_date), calculate_end_date(project, 2, 2))
|
||||
self.assertTrue(tasks[1].depends_on_tasks.find(tasks[0].name) >= 0 )
|
||||
self.assertTrue(tasks[1].depends_on_tasks.find(tasks[0].name) >= 0)
|
||||
|
||||
self.assertEqual(tasks[0].subject, 'Test Template Task for Dependency')
|
||||
self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 3, 1) )
|
||||
self.assertEqual(tasks[0].subject, "Test Template Task for Dependency")
|
||||
self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 3, 1))
|
||||
|
||||
self.assertEqual(len(tasks), 2)
|
||||
|
||||
@@ -112,32 +155,38 @@ class TestProject(unittest.TestCase):
|
||||
so.reload()
|
||||
self.assertFalse(so.project)
|
||||
|
||||
|
||||
def get_project(name, template):
|
||||
|
||||
project = frappe.get_doc(dict(
|
||||
doctype = 'Project',
|
||||
project_name = name,
|
||||
status = 'Open',
|
||||
project_template = template.name,
|
||||
expected_start_date = nowdate(),
|
||||
company="_Test Company"
|
||||
)).insert()
|
||||
project = frappe.get_doc(
|
||||
dict(
|
||||
doctype="Project",
|
||||
project_name=name,
|
||||
status="Open",
|
||||
project_template=template.name,
|
||||
expected_start_date=nowdate(),
|
||||
company="_Test Company",
|
||||
)
|
||||
).insert()
|
||||
|
||||
return project
|
||||
|
||||
|
||||
def make_project(args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
if args.project_name and frappe.db.exists("Project", {"project_name": args.project_name}):
|
||||
return frappe.get_doc("Project", {"project_name": args.project_name})
|
||||
|
||||
project = frappe.get_doc(dict(
|
||||
doctype = 'Project',
|
||||
project_name = args.project_name,
|
||||
status = 'Open',
|
||||
expected_start_date = args.start_date,
|
||||
company= args.company or '_Test Company'
|
||||
))
|
||||
project = frappe.get_doc(
|
||||
dict(
|
||||
doctype="Project",
|
||||
project_name=args.project_name,
|
||||
status="Open",
|
||||
expected_start_date=args.start_date,
|
||||
company=args.company or "_Test Company",
|
||||
)
|
||||
)
|
||||
|
||||
if args.project_template_name:
|
||||
template = make_project_template(args.project_template_name)
|
||||
@@ -147,12 +196,14 @@ def make_project(args):
|
||||
|
||||
return project
|
||||
|
||||
|
||||
def task_exists(subject):
|
||||
result = frappe.db.get_list("Task", filters={"subject": subject},fields=["name"])
|
||||
result = frappe.db.get_list("Task", filters={"subject": subject}, fields=["name"])
|
||||
if not len(result):
|
||||
return False
|
||||
return frappe.get_doc("Task", result[0].name)
|
||||
|
||||
|
||||
def calculate_end_date(project, start, duration):
|
||||
start = add_days(project.expected_start_date, start)
|
||||
start = project.update_if_holiday(start)
|
||||
|
||||
@@ -9,7 +9,6 @@ from frappe.utils import get_link_to_form
|
||||
|
||||
|
||||
class ProjectTemplate(Document):
|
||||
|
||||
def validate(self):
|
||||
self.validate_dependencies()
|
||||
|
||||
@@ -19,9 +18,13 @@ class ProjectTemplate(Document):
|
||||
if task_details.depends_on:
|
||||
for dependency_task in task_details.depends_on:
|
||||
if not self.check_dependent_task_presence(dependency_task.task):
|
||||
task_details_format = get_link_to_form("Task",task_details.name)
|
||||
task_details_format = get_link_to_form("Task", task_details.name)
|
||||
dependency_task_format = get_link_to_form("Task", dependency_task.task)
|
||||
frappe.throw(_("Task {0} depends on Task {1}. Please add Task {1} to the Tasks list.").format(frappe.bold(task_details_format), frappe.bold(dependency_task_format)))
|
||||
frappe.throw(
|
||||
_("Task {0} depends on Task {1}. Please add Task {1} to the Tasks list.").format(
|
||||
frappe.bold(task_details_format), frappe.bold(dependency_task_format)
|
||||
)
|
||||
)
|
||||
|
||||
def check_dependent_task_presence(self, task):
|
||||
for task_details in self.tasks:
|
||||
|
||||
@@ -1,9 +1,2 @@
|
||||
def get_data():
|
||||
return {
|
||||
'fieldname': 'project_template',
|
||||
'transactions': [
|
||||
{
|
||||
'items': ['Project']
|
||||
}
|
||||
]
|
||||
}
|
||||
return {"fieldname": "project_template", "transactions": [{"items": ["Project"]}]}
|
||||
|
||||
@@ -11,20 +11,16 @@ from erpnext.projects.doctype.task.test_task import create_task
|
||||
class TestProjectTemplate(unittest.TestCase):
|
||||
pass
|
||||
|
||||
|
||||
def make_project_template(project_template_name, project_tasks=[]):
|
||||
if not frappe.db.exists('Project Template', project_template_name):
|
||||
if not frappe.db.exists("Project Template", project_template_name):
|
||||
project_tasks = project_tasks or [
|
||||
create_task(subject="_Test Template Task 1", is_template=1, begin=0, duration=3),
|
||||
create_task(subject="_Test Template Task 2", is_template=1, begin=0, duration=2),
|
||||
]
|
||||
doc = frappe.get_doc(dict(
|
||||
doctype = 'Project Template',
|
||||
name = project_template_name
|
||||
))
|
||||
create_task(subject="_Test Template Task 1", is_template=1, begin=0, duration=3),
|
||||
create_task(subject="_Test Template Task 2", is_template=1, begin=0, duration=2),
|
||||
]
|
||||
doc = frappe.get_doc(dict(doctype="Project Template", name=project_template_name))
|
||||
for task in project_tasks:
|
||||
doc.append("tasks",{
|
||||
"task": task.name
|
||||
})
|
||||
doc.append("tasks", {"task": task.name})
|
||||
doc.insert()
|
||||
|
||||
return frappe.get_doc('Project Template', project_template_name)
|
||||
return frappe.get_doc("Project Template", project_template_name)
|
||||
|
||||
@@ -7,36 +7,88 @@ from frappe.model.document import Document
|
||||
|
||||
|
||||
class ProjectUpdate(Document):
|
||||
pass
|
||||
pass
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def daily_reminder():
|
||||
project = frappe.db.sql("""SELECT `tabProject`.project_name,`tabProject`.frequency,`tabProject`.expected_start_date,`tabProject`.expected_end_date,`tabProject`.percent_complete FROM `tabProject`;""")
|
||||
for projects in project:
|
||||
project_name = projects[0]
|
||||
frequency = projects[1]
|
||||
date_start = projects[2]
|
||||
date_end = projects [3]
|
||||
progress = projects [4]
|
||||
draft = frappe.db.sql("""SELECT count(docstatus) from `tabProject Update` WHERE `tabProject Update`.project = %s AND `tabProject Update`.docstatus = 0;""",project_name)
|
||||
for drafts in draft:
|
||||
number_of_drafts = drafts[0]
|
||||
update = frappe.db.sql("""SELECT name,date,time,progress,progress_details FROM `tabProject Update` WHERE `tabProject Update`.project = %s AND date = DATE_ADD(CURDATE(), INTERVAL -1 DAY);""",project_name)
|
||||
email_sending(project_name,frequency,date_start,date_end,progress,number_of_drafts,update)
|
||||
project = frappe.db.sql(
|
||||
"""SELECT `tabProject`.project_name,`tabProject`.frequency,`tabProject`.expected_start_date,`tabProject`.expected_end_date,`tabProject`.percent_complete FROM `tabProject`;"""
|
||||
)
|
||||
for projects in project:
|
||||
project_name = projects[0]
|
||||
frequency = projects[1]
|
||||
date_start = projects[2]
|
||||
date_end = projects[3]
|
||||
progress = projects[4]
|
||||
draft = frappe.db.sql(
|
||||
"""SELECT count(docstatus) from `tabProject Update` WHERE `tabProject Update`.project = %s AND `tabProject Update`.docstatus = 0;""",
|
||||
project_name,
|
||||
)
|
||||
for drafts in draft:
|
||||
number_of_drafts = drafts[0]
|
||||
update = frappe.db.sql(
|
||||
"""SELECT name,date,time,progress,progress_details FROM `tabProject Update` WHERE `tabProject Update`.project = %s AND date = DATE_ADD(CURDATE(), INTERVAL -1 DAY);""",
|
||||
project_name,
|
||||
)
|
||||
email_sending(project_name, frequency, date_start, date_end, progress, number_of_drafts, update)
|
||||
|
||||
def email_sending(project_name,frequency,date_start,date_end,progress,number_of_drafts,update):
|
||||
|
||||
holiday = frappe.db.sql("""SELECT holiday_date FROM `tabHoliday` where holiday_date = CURDATE();""")
|
||||
msg = "<p>Project Name: " + project_name + "</p><p>Frequency: " + " " + frequency + "</p><p>Update Reminder:" + " " + str(date_start) + "</p><p>Expected Date End:" + " " + str(date_end) + "</p><p>Percent Progress:" + " " + str(progress) + "</p><p>Number of Updates:" + " " + str(len(update)) + "</p>" + "</p><p>Number of drafts:" + " " + str(number_of_drafts) + "</p>"
|
||||
msg += """</u></b></p><table class='table table-bordered'><tr>
|
||||
def email_sending(
|
||||
project_name, frequency, date_start, date_end, progress, number_of_drafts, update
|
||||
):
|
||||
|
||||
holiday = frappe.db.sql(
|
||||
"""SELECT holiday_date FROM `tabHoliday` where holiday_date = CURDATE();"""
|
||||
)
|
||||
msg = (
|
||||
"<p>Project Name: "
|
||||
+ project_name
|
||||
+ "</p><p>Frequency: "
|
||||
+ " "
|
||||
+ frequency
|
||||
+ "</p><p>Update Reminder:"
|
||||
+ " "
|
||||
+ str(date_start)
|
||||
+ "</p><p>Expected Date End:"
|
||||
+ " "
|
||||
+ str(date_end)
|
||||
+ "</p><p>Percent Progress:"
|
||||
+ " "
|
||||
+ str(progress)
|
||||
+ "</p><p>Number of Updates:"
|
||||
+ " "
|
||||
+ str(len(update))
|
||||
+ "</p>"
|
||||
+ "</p><p>Number of drafts:"
|
||||
+ " "
|
||||
+ str(number_of_drafts)
|
||||
+ "</p>"
|
||||
)
|
||||
msg += """</u></b></p><table class='table table-bordered'><tr>
|
||||
<th>Project ID</th><th>Date Updated</th><th>Time Updated</th><th>Project Status</th><th>Notes</th>"""
|
||||
for updates in update:
|
||||
msg += "<tr><td>" + str(updates[0]) + "</td><td>" + str(updates[1]) + "</td><td>" + str(updates[2]) + "</td><td>" + str(updates[3]) + "</td>" + "</td><td>" + str(updates[4]) + "</td></tr>"
|
||||
for updates in update:
|
||||
msg += (
|
||||
"<tr><td>"
|
||||
+ str(updates[0])
|
||||
+ "</td><td>"
|
||||
+ str(updates[1])
|
||||
+ "</td><td>"
|
||||
+ str(updates[2])
|
||||
+ "</td><td>"
|
||||
+ str(updates[3])
|
||||
+ "</td>"
|
||||
+ "</td><td>"
|
||||
+ str(updates[4])
|
||||
+ "</td></tr>"
|
||||
)
|
||||
|
||||
msg += "</table>"
|
||||
if len(holiday) == 0:
|
||||
email = frappe.db.sql("""SELECT user from `tabProject User` WHERE parent = %s;""", project_name)
|
||||
for emails in email:
|
||||
frappe.sendmail(recipients=emails,subject=frappe._(project_name + ' ' + 'Summary'),message = msg)
|
||||
else:
|
||||
pass
|
||||
msg += "</table>"
|
||||
if len(holiday) == 0:
|
||||
email = frappe.db.sql("""SELECT user from `tabProject User` WHERE parent = %s;""", project_name)
|
||||
for emails in email:
|
||||
frappe.sendmail(
|
||||
recipients=emails, subject=frappe._(project_name + " " + "Summary"), message=msg
|
||||
)
|
||||
else:
|
||||
pass
|
||||
|
||||
@@ -9,5 +9,6 @@ import frappe
|
||||
class TestProjectUpdate(unittest.TestCase):
|
||||
pass
|
||||
|
||||
test_records = frappe.get_test_records('Project Update')
|
||||
|
||||
test_records = frappe.get_test_records("Project Update")
|
||||
test_ignore = ["Sales Order"]
|
||||
|
||||
@@ -12,19 +12,24 @@ from frappe.utils import add_days, cstr, date_diff, flt, get_link_to_form, getda
|
||||
from frappe.utils.nestedset import NestedSet
|
||||
|
||||
|
||||
class CircularReferenceError(frappe.ValidationError): pass
|
||||
class EndDateCannotBeGreaterThanProjectEndDateError(frappe.ValidationError): pass
|
||||
class CircularReferenceError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class EndDateCannotBeGreaterThanProjectEndDateError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class Task(NestedSet):
|
||||
nsm_parent_field = 'parent_task'
|
||||
nsm_parent_field = "parent_task"
|
||||
|
||||
def get_feed(self):
|
||||
return '{0}: {1}'.format(_(self.status), self.subject)
|
||||
return "{0}: {1}".format(_(self.status), self.subject)
|
||||
|
||||
def get_customer_details(self):
|
||||
cust = frappe.db.sql("select customer_name from `tabCustomer` where name=%s", self.customer)
|
||||
if cust:
|
||||
ret = {'customer_name': cust and cust[0][0] or ''}
|
||||
ret = {"customer_name": cust and cust[0][0] or ""}
|
||||
return ret
|
||||
|
||||
def validate(self):
|
||||
@@ -38,19 +43,37 @@ class Task(NestedSet):
|
||||
self.validate_completed_on()
|
||||
|
||||
def validate_dates(self):
|
||||
if self.exp_start_date and self.exp_end_date and getdate(self.exp_start_date) > getdate(self.exp_end_date):
|
||||
frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Expected Start Date"), \
|
||||
frappe.bold("Expected End Date")))
|
||||
if (
|
||||
self.exp_start_date
|
||||
and self.exp_end_date
|
||||
and getdate(self.exp_start_date) > getdate(self.exp_end_date)
|
||||
):
|
||||
frappe.throw(
|
||||
_("{0} can not be greater than {1}").format(
|
||||
frappe.bold("Expected Start Date"), frappe.bold("Expected End Date")
|
||||
)
|
||||
)
|
||||
|
||||
if self.act_start_date and self.act_end_date and getdate(self.act_start_date) > getdate(self.act_end_date):
|
||||
frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Actual Start Date"), \
|
||||
frappe.bold("Actual End Date")))
|
||||
if (
|
||||
self.act_start_date
|
||||
and self.act_end_date
|
||||
and getdate(self.act_start_date) > getdate(self.act_end_date)
|
||||
):
|
||||
frappe.throw(
|
||||
_("{0} can not be greater than {1}").format(
|
||||
frappe.bold("Actual Start Date"), frappe.bold("Actual End Date")
|
||||
)
|
||||
)
|
||||
|
||||
def validate_parent_expected_end_date(self):
|
||||
if self.parent_task:
|
||||
parent_exp_end_date = frappe.db.get_value("Task", self.parent_task, "exp_end_date")
|
||||
if parent_exp_end_date and getdate(self.get("exp_end_date")) > getdate(parent_exp_end_date):
|
||||
frappe.throw(_("Expected End Date should be less than or equal to parent task's Expected End Date {0}.").format(getdate(parent_exp_end_date)))
|
||||
frappe.throw(
|
||||
_(
|
||||
"Expected End Date should be less than or equal to parent task's Expected End Date {0}."
|
||||
).format(getdate(parent_exp_end_date))
|
||||
)
|
||||
|
||||
def validate_parent_project_dates(self):
|
||||
if not self.project or frappe.flags.in_test:
|
||||
@@ -59,16 +82,24 @@ class Task(NestedSet):
|
||||
expected_end_date = frappe.db.get_value("Project", self.project, "expected_end_date")
|
||||
|
||||
if expected_end_date:
|
||||
validate_project_dates(getdate(expected_end_date), self, "exp_start_date", "exp_end_date", "Expected")
|
||||
validate_project_dates(getdate(expected_end_date), self, "act_start_date", "act_end_date", "Actual")
|
||||
validate_project_dates(
|
||||
getdate(expected_end_date), self, "exp_start_date", "exp_end_date", "Expected"
|
||||
)
|
||||
validate_project_dates(
|
||||
getdate(expected_end_date), self, "act_start_date", "act_end_date", "Actual"
|
||||
)
|
||||
|
||||
def validate_status(self):
|
||||
if self.is_template and self.status != "Template":
|
||||
self.status = "Template"
|
||||
if self.status!=self.get_db_value("status") and self.status == "Completed":
|
||||
if self.status != self.get_db_value("status") and self.status == "Completed":
|
||||
for d in self.depends_on:
|
||||
if frappe.db.get_value("Task", d.task, "status") not in ("Completed", "Cancelled"):
|
||||
frappe.throw(_("Cannot complete task {0} as its dependant task {1} are not ccompleted / cancelled.").format(frappe.bold(self.name), frappe.bold(d.task)))
|
||||
frappe.throw(
|
||||
_(
|
||||
"Cannot complete task {0} as its dependant task {1} are not ccompleted / cancelled."
|
||||
).format(frappe.bold(self.name), frappe.bold(d.task))
|
||||
)
|
||||
|
||||
close_all_assignments(self.doctype, self.name)
|
||||
|
||||
@@ -76,7 +107,7 @@ class Task(NestedSet):
|
||||
if flt(self.progress or 0) > 100:
|
||||
frappe.throw(_("Progress % for a task cannot be more than 100."))
|
||||
|
||||
if self.status == 'Completed':
|
||||
if self.status == "Completed":
|
||||
self.progress = 100
|
||||
|
||||
def validate_dependencies_for_template_task(self):
|
||||
@@ -126,34 +157,43 @@ class Task(NestedSet):
|
||||
clear(self.doctype, self.name)
|
||||
|
||||
def update_total_expense_claim(self):
|
||||
self.total_expense_claim = frappe.db.sql("""select sum(total_sanctioned_amount) from `tabExpense Claim`
|
||||
where project = %s and task = %s and docstatus=1""",(self.project, self.name))[0][0]
|
||||
self.total_expense_claim = frappe.db.sql(
|
||||
"""select sum(total_sanctioned_amount) from `tabExpense Claim`
|
||||
where project = %s and task = %s and docstatus=1""",
|
||||
(self.project, self.name),
|
||||
)[0][0]
|
||||
|
||||
def update_time_and_costing(self):
|
||||
tl = frappe.db.sql("""select min(from_time) as start_date, max(to_time) as end_date,
|
||||
tl = frappe.db.sql(
|
||||
"""select min(from_time) as start_date, max(to_time) as end_date,
|
||||
sum(billing_amount) as total_billing_amount, sum(costing_amount) as total_costing_amount,
|
||||
sum(hours) as time from `tabTimesheet Detail` where task = %s and docstatus=1"""
|
||||
,self.name, as_dict=1)[0]
|
||||
sum(hours) as time from `tabTimesheet Detail` where task = %s and docstatus=1""",
|
||||
self.name,
|
||||
as_dict=1,
|
||||
)[0]
|
||||
if self.status == "Open":
|
||||
self.status = "Working"
|
||||
self.total_costing_amount= tl.total_costing_amount
|
||||
self.total_billing_amount= tl.total_billing_amount
|
||||
self.actual_time= tl.time
|
||||
self.act_start_date= tl.start_date
|
||||
self.act_end_date= tl.end_date
|
||||
self.total_costing_amount = tl.total_costing_amount
|
||||
self.total_billing_amount = tl.total_billing_amount
|
||||
self.actual_time = tl.time
|
||||
self.act_start_date = tl.start_date
|
||||
self.act_end_date = tl.end_date
|
||||
|
||||
def update_project(self):
|
||||
if self.project and not self.flags.from_project:
|
||||
frappe.get_cached_doc("Project", self.project).update_project()
|
||||
|
||||
def check_recursion(self):
|
||||
if self.flags.ignore_recursion_check: return
|
||||
check_list = [['task', 'parent'], ['parent', 'task']]
|
||||
if self.flags.ignore_recursion_check:
|
||||
return
|
||||
check_list = [["task", "parent"], ["parent", "task"]]
|
||||
for d in check_list:
|
||||
task_list, count = [self.name], 0
|
||||
while (len(task_list) > count ):
|
||||
tasks = frappe.db.sql(" select %s from `tabTask Depends On` where %s = %s " %
|
||||
(d[0], d[1], '%s'), cstr(task_list[count]))
|
||||
while len(task_list) > count:
|
||||
tasks = frappe.db.sql(
|
||||
" select %s from `tabTask Depends On` where %s = %s " % (d[0], d[1], "%s"),
|
||||
cstr(task_list[count]),
|
||||
)
|
||||
count = count + 1
|
||||
for b in tasks:
|
||||
if b[0] == self.name:
|
||||
@@ -167,15 +207,24 @@ class Task(NestedSet):
|
||||
def reschedule_dependent_tasks(self):
|
||||
end_date = self.exp_end_date or self.act_end_date
|
||||
if end_date:
|
||||
for task_name in frappe.db.sql("""
|
||||
for task_name in frappe.db.sql(
|
||||
"""
|
||||
select name from `tabTask` as parent
|
||||
where parent.project = %(project)s
|
||||
and parent.name in (
|
||||
select parent from `tabTask Depends On` as child
|
||||
where child.task = %(task)s and child.project = %(project)s)
|
||||
""", {'project': self.project, 'task':self.name }, as_dict=1):
|
||||
""",
|
||||
{"project": self.project, "task": self.name},
|
||||
as_dict=1,
|
||||
):
|
||||
task = frappe.get_doc("Task", task_name.name)
|
||||
if task.exp_start_date and task.exp_end_date and task.exp_start_date < getdate(end_date) and task.status == "Open":
|
||||
if (
|
||||
task.exp_start_date
|
||||
and task.exp_end_date
|
||||
and task.exp_start_date < getdate(end_date)
|
||||
and task.status == "Open"
|
||||
):
|
||||
task_duration = date_diff(task.exp_end_date, task.exp_start_date)
|
||||
task.exp_start_date = add_days(end_date, 1)
|
||||
task.exp_end_date = add_days(task.exp_start_date, task_duration)
|
||||
@@ -183,19 +232,19 @@ class Task(NestedSet):
|
||||
task.save()
|
||||
|
||||
def has_webform_permission(self):
|
||||
project_user = frappe.db.get_value("Project User", {"parent": self.project, "user":frappe.session.user} , "user")
|
||||
project_user = frappe.db.get_value(
|
||||
"Project User", {"parent": self.project, "user": frappe.session.user}, "user"
|
||||
)
|
||||
if project_user:
|
||||
return True
|
||||
|
||||
def populate_depends_on(self):
|
||||
if self.parent_task:
|
||||
parent = frappe.get_doc('Task', self.parent_task)
|
||||
parent = frappe.get_doc("Task", self.parent_task)
|
||||
if self.name not in [row.task for row in parent.depends_on]:
|
||||
parent.append("depends_on", {
|
||||
"doctype": "Task Depends On",
|
||||
"task": self.name,
|
||||
"subject": self.subject
|
||||
})
|
||||
parent.append(
|
||||
"depends_on", {"doctype": "Task Depends On", "task": self.name, "subject": self.subject}
|
||||
)
|
||||
parent.save()
|
||||
|
||||
def on_trash(self):
|
||||
@@ -208,12 +257,14 @@ class Task(NestedSet):
|
||||
self.update_project()
|
||||
|
||||
def update_status(self):
|
||||
if self.status not in ('Cancelled', 'Completed') and self.exp_end_date:
|
||||
if self.status not in ("Cancelled", "Completed") and self.exp_end_date:
|
||||
from datetime import datetime
|
||||
|
||||
if self.exp_end_date < datetime.now().date():
|
||||
self.db_set('status', 'Overdue', update_modified=False)
|
||||
self.db_set("status", "Overdue", update_modified=False)
|
||||
self.update_project()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def check_if_child_exists(name):
|
||||
child_tasks = frappe.get_all("Task", filters={"parent_task": name})
|
||||
@@ -225,24 +276,29 @@ def check_if_child_exists(name):
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_project(doctype, txt, searchfield, start, page_len, filters):
|
||||
from erpnext.controllers.queries import get_match_cond
|
||||
|
||||
meta = frappe.get_meta(doctype)
|
||||
searchfields = meta.get_search_fields()
|
||||
search_columns = ", " + ", ".join(searchfields) if searchfields else ''
|
||||
search_columns = ", " + ", ".join(searchfields) if searchfields else ""
|
||||
search_cond = " or " + " or ".join(field + " like %(txt)s" for field in searchfields)
|
||||
|
||||
return frappe.db.sql(""" select name {search_columns} from `tabProject`
|
||||
return frappe.db.sql(
|
||||
""" select name {search_columns} from `tabProject`
|
||||
where %(key)s like %(txt)s
|
||||
%(mcond)s
|
||||
{search_condition}
|
||||
order by name
|
||||
limit %(start)s, %(page_len)s""".format(search_columns = search_columns,
|
||||
search_condition=search_cond), {
|
||||
'key': searchfield,
|
||||
'txt': '%' + txt + '%',
|
||||
'mcond':get_match_cond(doctype),
|
||||
'start': start,
|
||||
'page_len': page_len
|
||||
})
|
||||
limit %(start)s, %(page_len)s""".format(
|
||||
search_columns=search_columns, search_condition=search_cond
|
||||
),
|
||||
{
|
||||
"key": searchfield,
|
||||
"txt": "%" + txt + "%",
|
||||
"mcond": get_match_cond(doctype),
|
||||
"start": start,
|
||||
"page_len": page_len,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -253,8 +309,13 @@ def set_multiple_status(names, status):
|
||||
task.status = status
|
||||
task.save()
|
||||
|
||||
|
||||
def set_tasks_as_overdue():
|
||||
tasks = frappe.get_all("Task", filters={"status": ["not in", ["Cancelled", "Completed"]]}, fields=["name", "status", "review_date"])
|
||||
tasks = frappe.get_all(
|
||||
"Task",
|
||||
filters={"status": ["not in", ["Cancelled", "Completed"]]},
|
||||
fields=["name", "status", "review_date"],
|
||||
)
|
||||
for task in tasks:
|
||||
if task.status == "Pending Review":
|
||||
if getdate(task.review_date) > getdate(today()):
|
||||
@@ -265,18 +326,24 @@ def set_tasks_as_overdue():
|
||||
@frappe.whitelist()
|
||||
def make_timesheet(source_name, target_doc=None, ignore_permissions=False):
|
||||
def set_missing_values(source, target):
|
||||
target.append("time_logs", {
|
||||
"hours": source.actual_time,
|
||||
"completed": source.status == "Completed",
|
||||
"project": source.project,
|
||||
"task": source.name
|
||||
})
|
||||
target.append(
|
||||
"time_logs",
|
||||
{
|
||||
"hours": source.actual_time,
|
||||
"completed": source.status == "Completed",
|
||||
"project": source.project,
|
||||
"task": source.name,
|
||||
},
|
||||
)
|
||||
|
||||
doclist = get_mapped_doc("Task", source_name, {
|
||||
"Task": {
|
||||
"doctype": "Timesheet"
|
||||
}
|
||||
}, target_doc, postprocess=set_missing_values, ignore_permissions=ignore_permissions)
|
||||
doclist = get_mapped_doc(
|
||||
"Task",
|
||||
source_name,
|
||||
{"Task": {"doctype": "Timesheet"}},
|
||||
target_doc,
|
||||
postprocess=set_missing_values,
|
||||
ignore_permissions=ignore_permissions,
|
||||
)
|
||||
|
||||
return doclist
|
||||
|
||||
@@ -284,60 +351,69 @@ def make_timesheet(source_name, target_doc=None, ignore_permissions=False):
|
||||
@frappe.whitelist()
|
||||
def get_children(doctype, parent, task=None, project=None, is_root=False):
|
||||
|
||||
filters = [['docstatus', '<', '2']]
|
||||
filters = [["docstatus", "<", "2"]]
|
||||
|
||||
if task:
|
||||
filters.append(['parent_task', '=', task])
|
||||
filters.append(["parent_task", "=", task])
|
||||
elif parent and not is_root:
|
||||
# via expand child
|
||||
filters.append(['parent_task', '=', parent])
|
||||
filters.append(["parent_task", "=", parent])
|
||||
else:
|
||||
filters.append(['ifnull(`parent_task`, "")', '=', ''])
|
||||
filters.append(['ifnull(`parent_task`, "")', "=", ""])
|
||||
|
||||
if project:
|
||||
filters.append(['project', '=', project])
|
||||
filters.append(["project", "=", project])
|
||||
|
||||
tasks = frappe.get_list(doctype, fields=[
|
||||
'name as value',
|
||||
'subject as title',
|
||||
'is_group as expandable'
|
||||
], filters=filters, order_by='name')
|
||||
tasks = frappe.get_list(
|
||||
doctype,
|
||||
fields=["name as value", "subject as title", "is_group as expandable"],
|
||||
filters=filters,
|
||||
order_by="name",
|
||||
)
|
||||
|
||||
# return tasks
|
||||
return tasks
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_node():
|
||||
from frappe.desk.treeview import make_tree_args
|
||||
|
||||
args = frappe.form_dict
|
||||
args.update({
|
||||
"name_field": "subject"
|
||||
})
|
||||
args.update({"name_field": "subject"})
|
||||
args = make_tree_args(**args)
|
||||
|
||||
if args.parent_task == 'All Tasks' or args.parent_task == args.project:
|
||||
if args.parent_task == "All Tasks" or args.parent_task == args.project:
|
||||
args.parent_task = None
|
||||
|
||||
frappe.get_doc(args).insert()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_multiple_tasks(data, parent):
|
||||
data = json.loads(data)
|
||||
new_doc = {'doctype': 'Task', 'parent_task': parent if parent!="All Tasks" else ""}
|
||||
new_doc['project'] = frappe.db.get_value('Task', {"name": parent}, 'project') or ""
|
||||
new_doc = {"doctype": "Task", "parent_task": parent if parent != "All Tasks" else ""}
|
||||
new_doc["project"] = frappe.db.get_value("Task", {"name": parent}, "project") or ""
|
||||
|
||||
for d in data:
|
||||
if not d.get("subject"): continue
|
||||
new_doc['subject'] = d.get("subject")
|
||||
if not d.get("subject"):
|
||||
continue
|
||||
new_doc["subject"] = d.get("subject")
|
||||
new_task = frappe.get_doc(new_doc)
|
||||
new_task.insert()
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_index("Task", ["lft", "rgt"])
|
||||
|
||||
|
||||
def validate_project_dates(project_end_date, task, task_start, task_end, actual_or_expected_date):
|
||||
if task.get(task_start) and date_diff(project_end_date, getdate(task.get(task_start))) < 0:
|
||||
frappe.throw(_("Task's {0} Start Date cannot be after Project's End Date.").format(actual_or_expected_date))
|
||||
frappe.throw(
|
||||
_("Task's {0} Start Date cannot be after Project's End Date.").format(actual_or_expected_date)
|
||||
)
|
||||
|
||||
if task.get(task_end) and date_diff(project_end_date, getdate(task.get(task_end))) < 0:
|
||||
frappe.throw(_("Task's {0} End Date cannot be after Project's End Date.").format(actual_or_expected_date))
|
||||
frappe.throw(
|
||||
_("Task's {0} End Date cannot be after Project's End Date.").format(actual_or_expected_date)
|
||||
)
|
||||
|
||||
@@ -3,15 +3,9 @@ from frappe import _
|
||||
|
||||
def get_data():
|
||||
return {
|
||||
'fieldname': 'task',
|
||||
'transactions': [
|
||||
{
|
||||
'label': _('Activity'),
|
||||
'items': ['Timesheet']
|
||||
},
|
||||
{
|
||||
'label': _('Accounting'),
|
||||
'items': ['Expense Claim']
|
||||
}
|
||||
]
|
||||
"fieldname": "task",
|
||||
"transactions": [
|
||||
{"label": _("Activity"), "items": ["Timesheet"]},
|
||||
{"label": _("Accounting"), "items": ["Expense Claim"]},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -16,9 +16,7 @@ class TestTask(unittest.TestCase):
|
||||
task3 = create_task("_Test Task 3", add_days(nowdate(), 11), add_days(nowdate(), 15), task2.name)
|
||||
|
||||
task1.reload()
|
||||
task1.append("depends_on", {
|
||||
"task": task3.name
|
||||
})
|
||||
task1.append("depends_on", {"task": task3.name})
|
||||
|
||||
self.assertRaises(CircularReferenceError, task1.save)
|
||||
|
||||
@@ -27,9 +25,7 @@ class TestTask(unittest.TestCase):
|
||||
|
||||
task4 = create_task("_Test Task 4", nowdate(), add_days(nowdate(), 15), task1.name)
|
||||
|
||||
task3.append("depends_on", {
|
||||
"task": task4.name
|
||||
})
|
||||
task3.append("depends_on", {"task": task4.name})
|
||||
|
||||
def test_reschedule_dependent_task(self):
|
||||
project = frappe.get_value("Project", {"project_name": "_Test Project"})
|
||||
@@ -44,20 +40,22 @@ class TestTask(unittest.TestCase):
|
||||
task3.get("depends_on")[0].project = project
|
||||
task3.save()
|
||||
|
||||
task1.update({
|
||||
"exp_end_date": add_days(nowdate(), 20)
|
||||
})
|
||||
task1.update({"exp_end_date": add_days(nowdate(), 20)})
|
||||
task1.save()
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Task", task2.name, "exp_start_date"),
|
||||
getdate(add_days(nowdate(), 21)))
|
||||
self.assertEqual(frappe.db.get_value("Task", task2.name, "exp_end_date"),
|
||||
getdate(add_days(nowdate(), 25)))
|
||||
self.assertEqual(
|
||||
frappe.db.get_value("Task", task2.name, "exp_start_date"), getdate(add_days(nowdate(), 21))
|
||||
)
|
||||
self.assertEqual(
|
||||
frappe.db.get_value("Task", task2.name, "exp_end_date"), getdate(add_days(nowdate(), 25))
|
||||
)
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Task", task3.name, "exp_start_date"),
|
||||
getdate(add_days(nowdate(), 26)))
|
||||
self.assertEqual(frappe.db.get_value("Task", task3.name, "exp_end_date"),
|
||||
getdate(add_days(nowdate(), 30)))
|
||||
self.assertEqual(
|
||||
frappe.db.get_value("Task", task3.name, "exp_start_date"), getdate(add_days(nowdate(), 26))
|
||||
)
|
||||
self.assertEqual(
|
||||
frappe.db.get_value("Task", task3.name, "exp_end_date"), getdate(add_days(nowdate(), 30))
|
||||
)
|
||||
|
||||
def test_close_assignment(self):
|
||||
if not frappe.db.exists("Task", "Test Close Assignment"):
|
||||
@@ -67,18 +65,27 @@ class TestTask(unittest.TestCase):
|
||||
|
||||
def assign():
|
||||
from frappe.desk.form import assign_to
|
||||
assign_to.add({
|
||||
"assign_to": ["test@example.com"],
|
||||
"doctype": task.doctype,
|
||||
"name": task.name,
|
||||
"description": "Close this task"
|
||||
})
|
||||
|
||||
assign_to.add(
|
||||
{
|
||||
"assign_to": ["test@example.com"],
|
||||
"doctype": task.doctype,
|
||||
"name": task.name,
|
||||
"description": "Close this task",
|
||||
}
|
||||
)
|
||||
|
||||
def get_owner_and_status():
|
||||
return frappe.db.get_value("ToDo",
|
||||
filters={"reference_type": task.doctype, "reference_name": task.name,
|
||||
"description": "Close this task"},
|
||||
fieldname=("allocated_to", "status"), as_dict=True)
|
||||
return frappe.db.get_value(
|
||||
"ToDo",
|
||||
filters={
|
||||
"reference_type": task.doctype,
|
||||
"reference_name": task.name,
|
||||
"description": "Close this task",
|
||||
},
|
||||
fieldname=("allocated_to", "status"),
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
assign()
|
||||
todo = get_owner_and_status()
|
||||
@@ -97,18 +104,36 @@ class TestTask(unittest.TestCase):
|
||||
task = create_task("Testing Overdue", add_days(nowdate(), -10), add_days(nowdate(), -5))
|
||||
|
||||
from erpnext.projects.doctype.task.task import set_tasks_as_overdue
|
||||
|
||||
set_tasks_as_overdue()
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Task", task.name, "status"), "Overdue")
|
||||
|
||||
def create_task(subject, start=None, end=None, depends_on=None, project=None, parent_task=None, is_group=0, is_template=0, begin=0, duration=0, save=True):
|
||||
|
||||
def create_task(
|
||||
subject,
|
||||
start=None,
|
||||
end=None,
|
||||
depends_on=None,
|
||||
project=None,
|
||||
parent_task=None,
|
||||
is_group=0,
|
||||
is_template=0,
|
||||
begin=0,
|
||||
duration=0,
|
||||
save=True,
|
||||
):
|
||||
if not frappe.db.exists("Task", subject):
|
||||
task = frappe.new_doc('Task')
|
||||
task = frappe.new_doc("Task")
|
||||
task.status = "Open"
|
||||
task.subject = subject
|
||||
task.exp_start_date = start or nowdate()
|
||||
task.exp_end_date = end or nowdate()
|
||||
task.project = project or None if is_template else frappe.get_value("Project", {"project_name": "_Test Project"})
|
||||
task.project = (
|
||||
project or None
|
||||
if is_template
|
||||
else frappe.get_value("Project", {"project_name": "_Test Project"})
|
||||
)
|
||||
task.is_template = is_template
|
||||
task.start = begin
|
||||
task.duration = duration
|
||||
@@ -120,9 +145,7 @@ def create_task(subject, start=None, end=None, depends_on=None, project=None, pa
|
||||
task = frappe.get_doc("Task", subject)
|
||||
|
||||
if depends_on:
|
||||
task.append("depends_on", {
|
||||
"task": depends_on
|
||||
})
|
||||
task.append("depends_on", {"task": depends_on})
|
||||
if save:
|
||||
task.save()
|
||||
return task
|
||||
|
||||
@@ -27,8 +27,8 @@ from erpnext.projects.doctype.timesheet.timesheet import (
|
||||
class TestTimesheet(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
make_earning_salary_component(setup=True, company_list=['_Test Company'])
|
||||
make_deduction_salary_component(setup=True, company_list=['_Test Company'])
|
||||
make_earning_salary_component(setup=True, company_list=["_Test Company"])
|
||||
make_deduction_salary_component(setup=True, company_list=["_Test Company"])
|
||||
|
||||
def setUp(self):
|
||||
for dt in ["Salary Slip", "Salary Structure", "Salary Structure Assignment", "Timesheet"]:
|
||||
@@ -62,7 +62,7 @@ class TestTimesheet(unittest.TestCase):
|
||||
emp = make_employee("test_employee_6@salary.com", company="_Test Company")
|
||||
|
||||
salary_structure = make_salary_structure_for_timesheet(emp)
|
||||
timesheet = make_timesheet(emp, simulate = True, is_billable=1)
|
||||
timesheet = make_timesheet(emp, simulate=True, is_billable=1)
|
||||
salary_slip = make_salary_slip(timesheet.name)
|
||||
salary_slip.submit()
|
||||
|
||||
@@ -73,27 +73,27 @@ class TestTimesheet(unittest.TestCase):
|
||||
self.assertEqual(salary_slip.timesheets[0].time_sheet, timesheet.name)
|
||||
self.assertEqual(salary_slip.timesheets[0].working_hours, 2)
|
||||
|
||||
timesheet = frappe.get_doc('Timesheet', timesheet.name)
|
||||
self.assertEqual(timesheet.status, 'Payslip')
|
||||
timesheet = frappe.get_doc("Timesheet", timesheet.name)
|
||||
self.assertEqual(timesheet.status, "Payslip")
|
||||
salary_slip.cancel()
|
||||
|
||||
timesheet = frappe.get_doc('Timesheet', timesheet.name)
|
||||
self.assertEqual(timesheet.status, 'Submitted')
|
||||
timesheet = frappe.get_doc("Timesheet", timesheet.name)
|
||||
self.assertEqual(timesheet.status, "Submitted")
|
||||
|
||||
def test_sales_invoice_from_timesheet(self):
|
||||
emp = make_employee("test_employee_6@salary.com")
|
||||
|
||||
timesheet = make_timesheet(emp, simulate=True, is_billable=1)
|
||||
sales_invoice = make_sales_invoice(timesheet.name, '_Test Item', '_Test Customer')
|
||||
sales_invoice = make_sales_invoice(timesheet.name, "_Test Item", "_Test Customer")
|
||||
sales_invoice.due_date = nowdate()
|
||||
sales_invoice.submit()
|
||||
timesheet = frappe.get_doc('Timesheet', timesheet.name)
|
||||
timesheet = frappe.get_doc("Timesheet", timesheet.name)
|
||||
self.assertEqual(sales_invoice.total_billing_amount, 100)
|
||||
self.assertEqual(timesheet.status, 'Billed')
|
||||
self.assertEqual(sales_invoice.customer, '_Test Customer')
|
||||
self.assertEqual(timesheet.status, "Billed")
|
||||
self.assertEqual(sales_invoice.customer, "_Test Customer")
|
||||
|
||||
item = sales_invoice.items[0]
|
||||
self.assertEqual(item.item_code, '_Test Item')
|
||||
self.assertEqual(item.item_code, "_Test Item")
|
||||
self.assertEqual(item.qty, 2.00)
|
||||
self.assertEqual(item.rate, 50.00)
|
||||
|
||||
@@ -101,19 +101,21 @@ class TestTimesheet(unittest.TestCase):
|
||||
emp = make_employee("test_employee_6@salary.com")
|
||||
project = frappe.get_value("Project", {"project_name": "_Test Project"})
|
||||
|
||||
timesheet = make_timesheet(emp, simulate=True, is_billable=1, project=project, company='_Test Company')
|
||||
timesheet = make_timesheet(
|
||||
emp, simulate=True, is_billable=1, project=project, company="_Test Company"
|
||||
)
|
||||
sales_invoice = create_sales_invoice(do_not_save=True)
|
||||
sales_invoice.project = project
|
||||
sales_invoice.submit()
|
||||
|
||||
ts = frappe.get_doc('Timesheet', timesheet.name)
|
||||
ts = frappe.get_doc("Timesheet", timesheet.name)
|
||||
self.assertEqual(ts.per_billed, 100)
|
||||
self.assertEqual(ts.time_logs[0].sales_invoice, sales_invoice.name)
|
||||
|
||||
def test_timesheet_time_overlap(self):
|
||||
emp = make_employee("test_employee_6@salary.com")
|
||||
|
||||
settings = frappe.get_single('Projects Settings')
|
||||
settings = frappe.get_single("Projects Settings")
|
||||
initial_setting = settings.ignore_employee_time_overlap
|
||||
settings.ignore_employee_time_overlap = 0
|
||||
settings.save()
|
||||
@@ -122,24 +124,24 @@ class TestTimesheet(unittest.TestCase):
|
||||
timesheet = frappe.new_doc("Timesheet")
|
||||
timesheet.employee = emp
|
||||
timesheet.append(
|
||||
'time_logs',
|
||||
"time_logs",
|
||||
{
|
||||
"billable": 1,
|
||||
"activity_type": "_Test Activity Type",
|
||||
"from_time": now_datetime(),
|
||||
"to_time": now_datetime() + datetime.timedelta(hours=3),
|
||||
"company": "_Test Company"
|
||||
}
|
||||
"company": "_Test Company",
|
||||
},
|
||||
)
|
||||
timesheet.append(
|
||||
'time_logs',
|
||||
"time_logs",
|
||||
{
|
||||
"billable": 1,
|
||||
"activity_type": "_Test Activity Type",
|
||||
"from_time": now_datetime(),
|
||||
"to_time": now_datetime() + datetime.timedelta(hours=3),
|
||||
"company": "_Test Company"
|
||||
}
|
||||
"company": "_Test Company",
|
||||
},
|
||||
)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, timesheet.save)
|
||||
@@ -158,27 +160,27 @@ class TestTimesheet(unittest.TestCase):
|
||||
timesheet = frappe.new_doc("Timesheet")
|
||||
timesheet.employee = emp
|
||||
timesheet.append(
|
||||
'time_logs',
|
||||
"time_logs",
|
||||
{
|
||||
"billable": 1,
|
||||
"activity_type": "_Test Activity Type",
|
||||
"from_time": now_datetime(),
|
||||
"to_time": now_datetime() + datetime.timedelta(hours=3),
|
||||
"company": "_Test Company"
|
||||
}
|
||||
"company": "_Test Company",
|
||||
},
|
||||
)
|
||||
timesheet.append(
|
||||
'time_logs',
|
||||
"time_logs",
|
||||
{
|
||||
"billable": 1,
|
||||
"activity_type": "_Test Activity Type",
|
||||
"from_time": now_datetime() + datetime.timedelta(hours=3),
|
||||
"to_time": now_datetime() + datetime.timedelta(hours=4),
|
||||
"company": "_Test Company"
|
||||
}
|
||||
"company": "_Test Company",
|
||||
},
|
||||
)
|
||||
|
||||
timesheet.save() # should not throw an error
|
||||
timesheet.save() # should not throw an error
|
||||
|
||||
def test_to_time(self):
|
||||
emp = make_employee("test_employee_6@salary.com")
|
||||
@@ -187,14 +189,14 @@ class TestTimesheet(unittest.TestCase):
|
||||
timesheet = frappe.new_doc("Timesheet")
|
||||
timesheet.employee = emp
|
||||
timesheet.append(
|
||||
'time_logs',
|
||||
"time_logs",
|
||||
{
|
||||
"billable": 1,
|
||||
"activity_type": "_Test Activity Type",
|
||||
"from_time": from_time,
|
||||
"hours": 2,
|
||||
"company": "_Test Company"
|
||||
}
|
||||
"company": "_Test Company",
|
||||
},
|
||||
)
|
||||
timesheet.save()
|
||||
|
||||
@@ -207,39 +209,51 @@ def make_salary_structure_for_timesheet(employee, company=None):
|
||||
frequency = "Monthly"
|
||||
|
||||
if not frappe.db.exists("Salary Component", "Timesheet Component"):
|
||||
frappe.get_doc({"doctype": "Salary Component", "salary_component": "Timesheet Component"}).insert()
|
||||
frappe.get_doc(
|
||||
{"doctype": "Salary Component", "salary_component": "Timesheet Component"}
|
||||
).insert()
|
||||
|
||||
salary_structure = make_salary_structure(salary_structure_name, frequency, company=company, dont_submit=True)
|
||||
salary_structure = make_salary_structure(
|
||||
salary_structure_name, frequency, company=company, dont_submit=True
|
||||
)
|
||||
salary_structure.salary_component = "Timesheet Component"
|
||||
salary_structure.salary_slip_based_on_timesheet = 1
|
||||
salary_structure.hour_rate = 50.0
|
||||
salary_structure.save()
|
||||
salary_structure.submit()
|
||||
|
||||
if not frappe.db.get_value("Salary Structure Assignment",
|
||||
{'employee':employee, 'docstatus': 1}):
|
||||
frappe.db.set_value('Employee', employee, 'date_of_joining',
|
||||
add_months(nowdate(), -5))
|
||||
create_salary_structure_assignment(employee, salary_structure.name)
|
||||
if not frappe.db.get_value("Salary Structure Assignment", {"employee": employee, "docstatus": 1}):
|
||||
frappe.db.set_value("Employee", employee, "date_of_joining", add_months(nowdate(), -5))
|
||||
create_salary_structure_assignment(employee, salary_structure.name)
|
||||
|
||||
return salary_structure
|
||||
|
||||
|
||||
def make_timesheet(employee, simulate=False, is_billable = 0, activity_type="_Test Activity Type", project=None, task=None, company=None):
|
||||
def make_timesheet(
|
||||
employee,
|
||||
simulate=False,
|
||||
is_billable=0,
|
||||
activity_type="_Test Activity Type",
|
||||
project=None,
|
||||
task=None,
|
||||
company=None,
|
||||
):
|
||||
update_activity_type(activity_type)
|
||||
timesheet = frappe.new_doc("Timesheet")
|
||||
timesheet.employee = employee
|
||||
timesheet.company = company or '_Test Company'
|
||||
timesheet_detail = timesheet.append('time_logs', {})
|
||||
timesheet.company = company or "_Test Company"
|
||||
timesheet_detail = timesheet.append("time_logs", {})
|
||||
timesheet_detail.is_billable = is_billable
|
||||
timesheet_detail.activity_type = activity_type
|
||||
timesheet_detail.from_time = now_datetime()
|
||||
timesheet_detail.hours = 2
|
||||
timesheet_detail.to_time = timesheet_detail.from_time + datetime.timedelta(hours= timesheet_detail.hours)
|
||||
timesheet_detail.to_time = timesheet_detail.from_time + datetime.timedelta(
|
||||
hours=timesheet_detail.hours
|
||||
)
|
||||
timesheet_detail.project = project
|
||||
timesheet_detail.task = task
|
||||
|
||||
for data in timesheet.get('time_logs'):
|
||||
for data in timesheet.get("time_logs"):
|
||||
if simulate:
|
||||
while True:
|
||||
try:
|
||||
@@ -247,7 +261,7 @@ def make_timesheet(employee, simulate=False, is_billable = 0, activity_type="_Te
|
||||
break
|
||||
except OverlapError:
|
||||
data.from_time = data.from_time + datetime.timedelta(minutes=10)
|
||||
data.to_time = data.from_time + datetime.timedelta(hours= data.hours)
|
||||
data.to_time = data.from_time + datetime.timedelta(hours=data.hours)
|
||||
else:
|
||||
timesheet.save(ignore_permissions=True)
|
||||
|
||||
@@ -255,7 +269,8 @@ def make_timesheet(employee, simulate=False, is_billable = 0, activity_type="_Te
|
||||
|
||||
return timesheet
|
||||
|
||||
|
||||
def update_activity_type(activity_type):
|
||||
activity_type = frappe.get_doc('Activity Type',activity_type)
|
||||
activity_type = frappe.get_doc("Activity Type", activity_type)
|
||||
activity_type.billing_rate = 50.0
|
||||
activity_type.save(ignore_permissions=True)
|
||||
|
||||
@@ -14,8 +14,13 @@ from erpnext.hr.utils import validate_active_employee
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
|
||||
class OverlapError(frappe.ValidationError): pass
|
||||
class OverWorkLoggedError(frappe.ValidationError): pass
|
||||
class OverlapError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class OverWorkLoggedError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class Timesheet(Document):
|
||||
def validate(self):
|
||||
@@ -32,7 +37,7 @@ class Timesheet(Document):
|
||||
|
||||
def set_employee_name(self):
|
||||
if self.employee and not self.employee_name:
|
||||
self.employee_name = frappe.db.get_value('Employee', self.employee, 'employee_name')
|
||||
self.employee_name = frappe.db.get_value("Employee", self.employee, "employee_name")
|
||||
|
||||
def calculate_total_amounts(self):
|
||||
self.total_hours = 0.0
|
||||
@@ -70,11 +75,7 @@ class Timesheet(Document):
|
||||
args.billing_hours = 0
|
||||
|
||||
def set_status(self):
|
||||
self.status = {
|
||||
"0": "Draft",
|
||||
"1": "Submitted",
|
||||
"2": "Cancelled"
|
||||
}[str(self.docstatus or 0)]
|
||||
self.status = {"0": "Draft", "1": "Submitted", "2": "Cancelled"}[str(self.docstatus or 0)]
|
||||
|
||||
if self.per_billed == 100:
|
||||
self.status = "Billed"
|
||||
@@ -135,7 +136,7 @@ class Timesheet(Document):
|
||||
frappe.throw(_("To date cannot be before from date"))
|
||||
|
||||
def validate_time_logs(self):
|
||||
for data in self.get('time_logs'):
|
||||
for data in self.get("time_logs"):
|
||||
self.set_to_time(data)
|
||||
self.validate_overlap(data)
|
||||
self.set_project(data)
|
||||
@@ -150,7 +151,7 @@ class Timesheet(Document):
|
||||
data.to_time = _to_time
|
||||
|
||||
def validate_overlap(self, data):
|
||||
settings = frappe.get_single('Projects Settings')
|
||||
settings = frappe.get_single("Projects Settings")
|
||||
self.validate_overlap_for("user", data, self.user, settings.ignore_user_time_overlap)
|
||||
self.validate_overlap_for("employee", data, self.employee, settings.ignore_employee_time_overlap)
|
||||
|
||||
@@ -159,7 +160,11 @@ class Timesheet(Document):
|
||||
|
||||
def validate_project(self, data):
|
||||
if self.parent_project and self.parent_project != data.project:
|
||||
frappe.throw(_("Row {0}: Project must be same as the one set in the Timesheet: {1}.").format(data.idx, self.parent_project))
|
||||
frappe.throw(
|
||||
_("Row {0}: Project must be same as the one set in the Timesheet: {1}.").format(
|
||||
data.idx, self.parent_project
|
||||
)
|
||||
)
|
||||
|
||||
def validate_overlap_for(self, fieldname, args, value, ignore_validation=False):
|
||||
if not value or ignore_validation:
|
||||
@@ -167,8 +172,12 @@ class Timesheet(Document):
|
||||
|
||||
existing = self.get_overlap_for(fieldname, args, value)
|
||||
if existing:
|
||||
frappe.throw(_("Row {0}: From Time and To Time of {1} is overlapping with {2}")
|
||||
.format(args.idx, self.name, existing.name), OverlapError)
|
||||
frappe.throw(
|
||||
_("Row {0}: From Time and To Time of {1} is overlapping with {2}").format(
|
||||
args.idx, self.name, existing.name
|
||||
),
|
||||
OverlapError,
|
||||
)
|
||||
|
||||
def get_overlap_for(self, fieldname, args, value):
|
||||
timesheet = frappe.qb.DocType("Timesheet")
|
||||
@@ -179,20 +188,22 @@ class Timesheet(Document):
|
||||
|
||||
existing = (
|
||||
frappe.qb.from_(timesheet)
|
||||
.join(timelog)
|
||||
.on(timelog.parent == timesheet.name)
|
||||
.select(timesheet.name.as_('name'), timelog.from_time.as_('from_time'), timelog.to_time.as_('to_time'))
|
||||
.where(
|
||||
(timelog.name != (args.name or "No Name"))
|
||||
& (timesheet.name != (args.parent or "No Name"))
|
||||
& (timesheet.docstatus < 2)
|
||||
& (timesheet[fieldname] == value)
|
||||
& (
|
||||
((from_time > timelog.from_time) & (from_time < timelog.to_time))
|
||||
| ((to_time > timelog.from_time) & (to_time < timelog.to_time))
|
||||
| ((from_time <= timelog.from_time) & (to_time >= timelog.to_time))
|
||||
)
|
||||
.join(timelog)
|
||||
.on(timelog.parent == timesheet.name)
|
||||
.select(
|
||||
timesheet.name.as_("name"), timelog.from_time.as_("from_time"), timelog.to_time.as_("to_time")
|
||||
)
|
||||
.where(
|
||||
(timelog.name != (args.name or "No Name"))
|
||||
& (timesheet.name != (args.parent or "No Name"))
|
||||
& (timesheet.docstatus < 2)
|
||||
& (timesheet[fieldname] == value)
|
||||
& (
|
||||
((from_time > timelog.from_time) & (from_time < timelog.to_time))
|
||||
| ((to_time > timelog.from_time) & (to_time < timelog.to_time))
|
||||
| ((from_time <= timelog.from_time) & (to_time >= timelog.to_time))
|
||||
)
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
if self.check_internal_overlap(fieldname, args):
|
||||
@@ -202,8 +213,7 @@ class Timesheet(Document):
|
||||
|
||||
def check_internal_overlap(self, fieldname, args):
|
||||
for time_log in self.time_logs:
|
||||
if not (time_log.from_time and time_log.to_time
|
||||
and args.from_time and args.to_time):
|
||||
if not (time_log.from_time and time_log.to_time and args.from_time and args.to_time):
|
||||
continue
|
||||
|
||||
from_time = get_datetime(time_log.from_time)
|
||||
@@ -211,10 +221,14 @@ class Timesheet(Document):
|
||||
args_from_time = get_datetime(args.from_time)
|
||||
args_to_time = get_datetime(args.to_time)
|
||||
|
||||
if (args.get(fieldname) == time_log.get(fieldname)) and (args.idx != time_log.idx) and (
|
||||
(args_from_time > from_time and args_from_time < to_time)
|
||||
or (args_to_time > from_time and args_to_time < to_time)
|
||||
or (args_from_time <= from_time and args_to_time >= to_time)
|
||||
if (
|
||||
(args.get(fieldname) == time_log.get(fieldname))
|
||||
and (args.idx != time_log.idx)
|
||||
and (
|
||||
(args_from_time > from_time and args_from_time < to_time)
|
||||
or (args_to_time > from_time and args_to_time < to_time)
|
||||
or (args_from_time <= from_time and args_to_time >= to_time)
|
||||
)
|
||||
):
|
||||
return True
|
||||
return False
|
||||
@@ -226,8 +240,12 @@ class Timesheet(Document):
|
||||
hours = data.billing_hours or 0
|
||||
costing_hours = data.billing_hours or data.hours or 0
|
||||
if rate:
|
||||
data.billing_rate = flt(rate.get('billing_rate')) if flt(data.billing_rate) == 0 else data.billing_rate
|
||||
data.costing_rate = flt(rate.get('costing_rate')) if flt(data.costing_rate) == 0 else data.costing_rate
|
||||
data.billing_rate = (
|
||||
flt(rate.get("billing_rate")) if flt(data.billing_rate) == 0 else data.billing_rate
|
||||
)
|
||||
data.costing_rate = (
|
||||
flt(rate.get("costing_rate")) if flt(data.costing_rate) == 0 else data.costing_rate
|
||||
)
|
||||
data.billing_amount = data.billing_rate * hours
|
||||
data.costing_amount = data.costing_rate * costing_hours
|
||||
|
||||
@@ -235,6 +253,7 @@ class Timesheet(Document):
|
||||
if not ts_detail.is_billable:
|
||||
ts_detail.billing_rate = 0.0
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_projectwise_timesheet_data(project=None, parent=None, from_time=None, to_time=None):
|
||||
condition = ""
|
||||
@@ -269,22 +288,22 @@ def get_projectwise_timesheet_data(project=None, parent=None, from_time=None, to
|
||||
ORDER BY tsd.from_time ASC
|
||||
"""
|
||||
|
||||
filters = {
|
||||
"project": project,
|
||||
"parent": parent,
|
||||
"from_time": from_time,
|
||||
"to_time": to_time
|
||||
}
|
||||
filters = {"project": project, "parent": parent, "from_time": from_time, "to_time": to_time}
|
||||
|
||||
return frappe.db.sql(query, filters, as_dict=1)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_timesheet_detail_rate(timelog, currency):
|
||||
timelog_detail = frappe.db.sql("""SELECT tsd.billing_amount as billing_amount,
|
||||
timelog_detail = frappe.db.sql(
|
||||
"""SELECT tsd.billing_amount as billing_amount,
|
||||
ts.currency as currency FROM `tabTimesheet Detail` tsd
|
||||
INNER JOIN `tabTimesheet` ts ON ts.name=tsd.parent
|
||||
WHERE tsd.name = '{0}'""".format(timelog), as_dict = 1)[0]
|
||||
WHERE tsd.name = '{0}'""".format(
|
||||
timelog
|
||||
),
|
||||
as_dict=1,
|
||||
)[0]
|
||||
|
||||
if timelog_detail.currency:
|
||||
exchange_rate = get_exchange_rate(timelog_detail.currency, currency)
|
||||
@@ -292,44 +311,60 @@ def get_timesheet_detail_rate(timelog, currency):
|
||||
return timelog_detail.billing_amount * exchange_rate
|
||||
return timelog_detail.billing_amount
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_timesheet(doctype, txt, searchfield, start, page_len, filters):
|
||||
if not filters: filters = {}
|
||||
if not filters:
|
||||
filters = {}
|
||||
|
||||
condition = ""
|
||||
if filters.get("project"):
|
||||
condition = "and tsd.project = %(project)s"
|
||||
|
||||
return frappe.db.sql("""select distinct tsd.parent from `tabTimesheet Detail` tsd,
|
||||
return frappe.db.sql(
|
||||
"""select distinct tsd.parent from `tabTimesheet Detail` tsd,
|
||||
`tabTimesheet` ts where
|
||||
ts.status in ('Submitted', 'Payslip') and tsd.parent = ts.name and
|
||||
tsd.docstatus = 1 and ts.total_billable_amount > 0
|
||||
and tsd.parent LIKE %(txt)s {condition}
|
||||
order by tsd.parent limit %(start)s, %(page_len)s"""
|
||||
.format(condition=condition), {
|
||||
'txt': '%' + txt + '%',
|
||||
"start": start, "page_len": page_len, 'project': filters.get("project")
|
||||
})
|
||||
order by tsd.parent limit %(start)s, %(page_len)s""".format(
|
||||
condition=condition
|
||||
),
|
||||
{
|
||||
"txt": "%" + txt + "%",
|
||||
"start": start,
|
||||
"page_len": page_len,
|
||||
"project": filters.get("project"),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_timesheet_data(name, project):
|
||||
data = None
|
||||
if project and project!='':
|
||||
if project and project != "":
|
||||
data = get_projectwise_timesheet_data(project, name)
|
||||
else:
|
||||
data = frappe.get_all('Timesheet',
|
||||
fields = ["(total_billable_amount - total_billed_amount) as billing_amt", "total_billable_hours as billing_hours"], filters = {'name': name})
|
||||
data = frappe.get_all(
|
||||
"Timesheet",
|
||||
fields=[
|
||||
"(total_billable_amount - total_billed_amount) as billing_amt",
|
||||
"total_billable_hours as billing_hours",
|
||||
],
|
||||
filters={"name": name},
|
||||
)
|
||||
return {
|
||||
'billing_hours': data[0].billing_hours if data else None,
|
||||
'billing_amount': data[0].billing_amt if data else None,
|
||||
'timesheet_detail': data[0].name if data and project and project!= '' else None
|
||||
"billing_hours": data[0].billing_hours if data else None,
|
||||
"billing_amount": data[0].billing_amt if data else None,
|
||||
"timesheet_detail": data[0].name if data and project and project != "" else None,
|
||||
}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_sales_invoice(source_name, item_code=None, customer=None, currency=None):
|
||||
target = frappe.new_doc("Sales Invoice")
|
||||
timesheet = frappe.get_doc('Timesheet', source_name)
|
||||
timesheet = frappe.get_doc("Timesheet", source_name)
|
||||
|
||||
if not timesheet.total_billable_hours:
|
||||
frappe.throw(_("Invoice can't be made for zero billing hour"))
|
||||
@@ -349,28 +384,28 @@ def make_sales_invoice(source_name, item_code=None, customer=None, currency=None
|
||||
target.currency = currency
|
||||
|
||||
if item_code:
|
||||
target.append('items', {
|
||||
'item_code': item_code,
|
||||
'qty': hours,
|
||||
'rate': billing_rate
|
||||
})
|
||||
target.append("items", {"item_code": item_code, "qty": hours, "rate": billing_rate})
|
||||
|
||||
for time_log in timesheet.time_logs:
|
||||
if time_log.is_billable:
|
||||
target.append('timesheets', {
|
||||
'time_sheet': timesheet.name,
|
||||
'billing_hours': time_log.billing_hours,
|
||||
'billing_amount': time_log.billing_amount,
|
||||
'timesheet_detail': time_log.name,
|
||||
'activity_type': time_log.activity_type,
|
||||
'description': time_log.description
|
||||
})
|
||||
target.append(
|
||||
"timesheets",
|
||||
{
|
||||
"time_sheet": timesheet.name,
|
||||
"billing_hours": time_log.billing_hours,
|
||||
"billing_amount": time_log.billing_amount,
|
||||
"timesheet_detail": time_log.name,
|
||||
"activity_type": time_log.activity_type,
|
||||
"description": time_log.description,
|
||||
},
|
||||
)
|
||||
|
||||
target.run_method("calculate_billing_amount_for_timesheet")
|
||||
target.run_method("set_missing_values")
|
||||
|
||||
return target
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_salary_slip(source_name, target_doc=None):
|
||||
target = frappe.new_doc("Salary Slip")
|
||||
@@ -379,8 +414,9 @@ def make_salary_slip(source_name, target_doc=None):
|
||||
|
||||
return target
|
||||
|
||||
|
||||
def set_missing_values(time_sheet, target):
|
||||
doc = frappe.get_doc('Timesheet', time_sheet)
|
||||
doc = frappe.get_doc("Timesheet", time_sheet)
|
||||
target.employee = doc.employee
|
||||
target.employee_name = doc.employee_name
|
||||
target.salary_slip_based_on_timesheet = 1
|
||||
@@ -388,26 +424,33 @@ def set_missing_values(time_sheet, target):
|
||||
target.end_date = doc.end_date
|
||||
target.posting_date = doc.modified
|
||||
target.total_working_hours = doc.total_hours
|
||||
target.append('timesheets', {
|
||||
'time_sheet': doc.name,
|
||||
'working_hours': doc.total_hours
|
||||
})
|
||||
target.append("timesheets", {"time_sheet": doc.name, "working_hours": doc.total_hours})
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_activity_cost(employee=None, activity_type=None, currency=None):
|
||||
base_currency = frappe.defaults.get_global_default('currency')
|
||||
rate = frappe.db.get_values("Activity Cost", {"employee": employee,
|
||||
"activity_type": activity_type}, ["costing_rate", "billing_rate"], as_dict=True)
|
||||
base_currency = frappe.defaults.get_global_default("currency")
|
||||
rate = frappe.db.get_values(
|
||||
"Activity Cost",
|
||||
{"employee": employee, "activity_type": activity_type},
|
||||
["costing_rate", "billing_rate"],
|
||||
as_dict=True,
|
||||
)
|
||||
if not rate:
|
||||
rate = frappe.db.get_values("Activity Type", {"activity_type": activity_type},
|
||||
["costing_rate", "billing_rate"], as_dict=True)
|
||||
if rate and currency and currency!=base_currency:
|
||||
rate = frappe.db.get_values(
|
||||
"Activity Type",
|
||||
{"activity_type": activity_type},
|
||||
["costing_rate", "billing_rate"],
|
||||
as_dict=True,
|
||||
)
|
||||
if rate and currency and currency != base_currency:
|
||||
exchange_rate = get_exchange_rate(base_currency, currency)
|
||||
rate[0]["costing_rate"] = rate[0]["costing_rate"] * exchange_rate
|
||||
rate[0]["billing_rate"] = rate[0]["billing_rate"] * exchange_rate
|
||||
|
||||
return rate[0] if rate else {}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_events(start, end, filters=None):
|
||||
"""Returns events for Gantt / Calendar view rendering.
|
||||
@@ -417,9 +460,11 @@ def get_events(start, end, filters=None):
|
||||
"""
|
||||
filters = json.loads(filters)
|
||||
from frappe.desk.calendar import get_event_conditions
|
||||
|
||||
conditions = get_event_conditions("Timesheet", filters)
|
||||
|
||||
return frappe.db.sql("""select `tabTimesheet Detail`.name as name,
|
||||
return frappe.db.sql(
|
||||
"""select `tabTimesheet Detail`.name as name,
|
||||
`tabTimesheet Detail`.docstatus as status, `tabTimesheet Detail`.parent as parent,
|
||||
from_time as start_date, hours, activity_type,
|
||||
`tabTimesheet Detail`.project, to_time as end_date,
|
||||
@@ -428,29 +473,37 @@ def get_events(start, end, filters=None):
|
||||
where `tabTimesheet Detail`.parent = `tabTimesheet`.name
|
||||
and `tabTimesheet`.docstatus < 2
|
||||
and (from_time <= %(end)s and to_time >= %(start)s) {conditions} {match_cond}
|
||||
""".format(conditions=conditions, match_cond = get_match_cond('Timesheet')),
|
||||
{
|
||||
"start": start,
|
||||
"end": end
|
||||
}, as_dict=True, update={"allDay": 0})
|
||||
""".format(
|
||||
conditions=conditions, match_cond=get_match_cond("Timesheet")
|
||||
),
|
||||
{"start": start, "end": end},
|
||||
as_dict=True,
|
||||
update={"allDay": 0},
|
||||
)
|
||||
|
||||
def get_timesheets_list(doctype, txt, filters, limit_start, limit_page_length=20, order_by="modified"):
|
||||
|
||||
def get_timesheets_list(
|
||||
doctype, txt, filters, limit_start, limit_page_length=20, order_by="modified"
|
||||
):
|
||||
user = frappe.session.user
|
||||
# find customer name from contact.
|
||||
customer = ''
|
||||
customer = ""
|
||||
timesheets = []
|
||||
|
||||
contact = frappe.db.exists('Contact', {'user': user})
|
||||
contact = frappe.db.exists("Contact", {"user": user})
|
||||
if contact:
|
||||
# find customer
|
||||
contact = frappe.get_doc('Contact', contact)
|
||||
customer = contact.get_link_for('Customer')
|
||||
contact = frappe.get_doc("Contact", contact)
|
||||
customer = contact.get_link_for("Customer")
|
||||
|
||||
if customer:
|
||||
sales_invoices = [d.name for d in frappe.get_all('Sales Invoice', filters={'customer': customer})] or [None]
|
||||
projects = [d.name for d in frappe.get_all('Project', filters={'customer': customer})]
|
||||
sales_invoices = [
|
||||
d.name for d in frappe.get_all("Sales Invoice", filters={"customer": customer})
|
||||
] or [None]
|
||||
projects = [d.name for d in frappe.get_all("Project", filters={"customer": customer})]
|
||||
# Return timesheet related data to web portal.
|
||||
timesheets = frappe.db.sql('''
|
||||
timesheets = frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
ts.name, tsd.activity_type, ts.status, ts.total_billable_hours,
|
||||
COALESCE(ts.sales_invoice, tsd.sales_invoice) AS sales_invoice, tsd.project
|
||||
@@ -463,16 +516,22 @@ def get_timesheets_list(doctype, txt, filters, limit_start, limit_page_length=20
|
||||
)
|
||||
ORDER BY `end_date` ASC
|
||||
LIMIT {0}, {1}
|
||||
'''.format(limit_start, limit_page_length), dict(sales_invoices=sales_invoices, projects=projects), as_dict=True) #nosec
|
||||
""".format(
|
||||
limit_start, limit_page_length
|
||||
),
|
||||
dict(sales_invoices=sales_invoices, projects=projects),
|
||||
as_dict=True,
|
||||
) # nosec
|
||||
|
||||
return timesheets
|
||||
|
||||
|
||||
def get_list_context(context=None):
|
||||
return {
|
||||
"show_sidebar": True,
|
||||
"show_search": True,
|
||||
'no_breadcrumbs': True,
|
||||
"no_breadcrumbs": True,
|
||||
"title": _("Timesheets"),
|
||||
"get_list": get_timesheets_list,
|
||||
"row_template": "templates/includes/timesheet/timesheet_row.html"
|
||||
"row_template": "templates/includes/timesheet/timesheet_row.html",
|
||||
}
|
||||
|
||||
@@ -3,11 +3,6 @@ from frappe import _
|
||||
|
||||
def get_data():
|
||||
return {
|
||||
'fieldname': 'time_sheet',
|
||||
'transactions': [
|
||||
{
|
||||
'label': _('References'),
|
||||
'items': ['Sales Invoice', 'Salary Slip']
|
||||
}
|
||||
]
|
||||
"fieldname": "time_sheet",
|
||||
"transactions": [{"label": _("References"), "items": ["Sales Invoice", "Salary Slip"]}],
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -17,14 +17,15 @@ def query_task(doctype, txt, searchfield, start, page_len, filters):
|
||||
match_conditions = build_match_conditions("Task")
|
||||
match_conditions = ("and" + match_conditions) if match_conditions else ""
|
||||
|
||||
return frappe.db.sql("""select name, subject from `tabTask`
|
||||
return frappe.db.sql(
|
||||
"""select name, subject from `tabTask`
|
||||
where (`%s` like %s or `subject` like %s) %s
|
||||
order by
|
||||
case when `subject` like %s then 0 else 1 end,
|
||||
case when `%s` like %s then 0 else 1 end,
|
||||
`%s`,
|
||||
subject
|
||||
limit %s, %s""" %
|
||||
(searchfield, "%s", "%s", match_conditions, "%s",
|
||||
searchfield, "%s", searchfield, "%s", "%s"),
|
||||
(search_string, search_string, order_by_string, order_by_string, start, page_len))
|
||||
limit %s, %s"""
|
||||
% (searchfield, "%s", "%s", match_conditions, "%s", searchfield, "%s", searchfield, "%s", "%s"),
|
||||
(search_string, search_string, order_by_string, order_by_string, start, page_len),
|
||||
)
|
||||
|
||||
@@ -3,9 +3,13 @@ import frappe
|
||||
|
||||
def get_context(context):
|
||||
if frappe.form_dict.project:
|
||||
context.parents = [{'title': frappe.form_dict.project, 'route': '/projects?project='+ frappe.form_dict.project}]
|
||||
context.parents = [
|
||||
{"title": frappe.form_dict.project, "route": "/projects?project=" + frappe.form_dict.project}
|
||||
]
|
||||
context.success_url = "/projects?project=" + frappe.form_dict.project
|
||||
|
||||
elif context.doc and context.doc.get('project'):
|
||||
context.parents = [{'title': context.doc.project, 'route': '/projects?project='+ context.doc.project}]
|
||||
elif context.doc and context.doc.get("project"):
|
||||
context.parents = [
|
||||
{"title": context.doc.project, "route": "/projects?project=" + context.doc.project}
|
||||
]
|
||||
context.success_url = "/projects?project=" + context.doc.project
|
||||
|
||||
Reference in New Issue
Block a user