From a2068d9b980f25e5e08c7b6d405a6c94c4b32367 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Thu, 18 Mar 2021 15:25:22 +0530 Subject: [PATCH 01/15] feat: profitability report --- .../projects/report/profitability/__init__.py | 0 .../report/profitability/profitability.js | 32 +++++ .../report/profitability/profitability.json | 39 ++++++ .../report/profitability/profitability.py | 119 ++++++++++++++++++ 4 files changed, 190 insertions(+) create mode 100644 erpnext/projects/report/profitability/__init__.py create mode 100644 erpnext/projects/report/profitability/profitability.js create mode 100644 erpnext/projects/report/profitability/profitability.json create mode 100644 erpnext/projects/report/profitability/profitability.py diff --git a/erpnext/projects/report/profitability/__init__.py b/erpnext/projects/report/profitability/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/projects/report/profitability/profitability.js b/erpnext/projects/report/profitability/profitability.js new file mode 100644 index 0000000000..dbf918760f --- /dev/null +++ b/erpnext/projects/report/profitability/profitability.js @@ -0,0 +1,32 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Profitability"] = { + "filters": [ + { + "fieldname": "start_date", + "label": __("Start Date"), + "fieldtype": "Date", + "reqd": 1 + }, + { + "fieldname": "end_date", + "label": __("End Date"), + "fieldtype": "Date", + "reqd": 1 + }, + { + "fieldname": "customer_name", + "label": __("Customer"), + "fieldtype": "Link", + "options": "Customer" + }, + { + "fieldname": "employee", + "label": __("Employee"), + "fieldtype": "Link", + "options": "Employee" + } + ] +}; diff --git a/erpnext/projects/report/profitability/profitability.json b/erpnext/projects/report/profitability/profitability.json new file mode 100644 index 0000000000..4f91accf58 --- /dev/null +++ b/erpnext/projects/report/profitability/profitability.json @@ -0,0 +1,39 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2021-03-18 10:19:40.124932", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "json": "{}", + "modified": "2021-03-18 10:20:15.559305", + "modified_by": "Administrator", + "module": "Projects", + "name": "Profitability", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Timesheet", + "report_name": "Profitability", + "report_type": "Script Report", + "roles": [ + { + "role": "Projects User" + }, + { + "role": "HR User" + }, + { + "role": "Manufacturing User" + }, + { + "role": "Employee" + }, + { + "role": "Accounts User" + } + ] +} \ No newline at end of file diff --git a/erpnext/projects/report/profitability/profitability.py b/erpnext/projects/report/profitability/profitability.py new file mode 100644 index 0000000000..48adf97264 --- /dev/null +++ b/erpnext/projects/report/profitability/profitability.py @@ -0,0 +1,119 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ + +def execute(filters=None): + columns, data = [], [] + data = get_data(filters) + columns = get_columns() + return columns, data + +def get_columns(): + return [ + { + "fieldname": "customer_name", + "label": _("Customer"), + "fieldtype": "Link", + "options": "Customer", + "width": 150 + }, + { + "fieldname": "title", + "label": _("Name"), + "fieldtype": "Data", + "width": 120 + }, + { + "fieldname": "employee", + "label": _("Employee"), + "fieldtype": "Link", + "options": "employee", + "width": 150 + }, + { + "fieldname": "grand_total", + "label": _("Bill Amount"), + "fieldtype": "Currency", + "options": "currency", + "width": 120 + }, + { + "fieldname": "gross_pay", + "label": _("Cost"), + "fieldtype": "Currency", + "options": "currency", + "width": 120 + }, + { + "fieldname": "profit", + "label": _("Profit"), + "fieldtype": "Currency", + "options": "currency", + "width": 120 + }, + { + "fieldname": "end_date", + "label": _("End Date"), + "fieldtype": "Date", + "width": 120 + }, + { + "fieldname": "total_billed_hours", + "label": _("Total Billed Hours"), + "fieldtype": "Int", + "width": 120 + }, + { + "fieldname": "utilization", + "label": _("Utilization"), + "fieldtype": "Percentage", + "width": 120 + }, + { + "fieldname": "fractional_cost", + "label": _("Fractional Cost"), + "fieldtype": "Int", + "width": 100 + } + ] + +def get_data(filters): + conditions = get_conditions(filters) + sql = """ + select + *, + t.gross_pay * t.utilization as fractional_cost, + t.grand_total - t.gross_pay * t.utilization as profit + from + (select + si.customer_name,tabTimesheet.title,tabTimesheet.employee,si.grand_total,si.name as voucher_no, + ss.gross_pay,ss.total_working_days,tabTimesheet.end_date,tabTimesheet.total_billed_hours, + tabTimesheet.total_billed_hours/(ss.total_working_days * 8) as utilization + from + `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 """ + if conditions: + sql += """ + where + %s) as t"""%(conditions) + data = frappe.db.sql(sql,filters, as_dict=True) + + return data + +def get_conditions(filters): + conditions = [] + if filters.get("customer_name"): + conditions.append("si.customer_name='%s'"%filters.get("customer_name")) + if filters.get("start_date"): + conditions.append("tabTimesheet.start_date>='%s'"%filters.get("start_date")) + if filters.get("end_date"): + conditions.append("tabTimesheet.end_date<='%s'"%filters.get("end_date")) + if filters.get("employee"): + conditions.append("tabTimesheet.employee='%s'"%filters.get("employee")) + conditions = " and ".join(conditions) + return conditions \ No newline at end of file From 453e07e32a856039d8306ad6a432757e814c8574 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Tue, 23 Mar 2021 11:18:57 +0530 Subject: [PATCH 02/15] fix: exclude cancelled salary slips --- erpnext/projects/report/profitability/profitability.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/projects/report/profitability/profitability.py b/erpnext/projects/report/profitability/profitability.py index 48adf97264..0edecd8e1b 100644 --- a/erpnext/projects/report/profitability/profitability.py +++ b/erpnext/projects/report/profitability/profitability.py @@ -96,7 +96,7 @@ def get_data(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 """ + join `tabSalary Slip` as ss on ss.name = sst.parent and ss.status != "Cancelled" """ if conditions: sql += """ where From 6597bf7dd76c9e24ff77ec35ee7a0eca994d2349 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Thu, 25 Mar 2021 13:31:43 +0530 Subject: [PATCH 03/15] feat: profitability report default working hours and tests --- .../hr/doctype/hr_settings/hr_settings.json | 9 +- .../doctype/timesheet/test_timesheet.py | 4 +- .../report/profitability/profitability.js | 14 +- .../report/profitability/profitability.py | 128 ++++++++++++------ .../profitability/test_profitability.py | 52 +++++++ .../projects/workspace/projects/projects.json | 13 +- 6 files changed, 172 insertions(+), 48 deletions(-) create mode 100644 erpnext/projects/report/profitability/test_profitability.py diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.json b/erpnext/hr/doctype/hr_settings/hr_settings.json index 09666c5db5..4fa50c4852 100644 --- a/erpnext/hr/doctype/hr_settings/hr_settings.json +++ b/erpnext/hr/doctype/hr_settings/hr_settings.json @@ -10,6 +10,7 @@ "retirement_age", "emp_created_by", "column_break_4", + "default_working_hours", "stop_birthday_reminders", "expense_approver_mandatory_in_expense_claim", "leave_settings", @@ -143,13 +144,19 @@ "fieldname": "send_leave_notification", "fieldtype": "Check", "label": "Send Leave Notification" + }, + { + "default": "8", + "fieldname": "default_working_hours", + "fieldtype": "Int", + "label": "Default Working Hours" } ], "icon": "fa fa-cog", "idx": 1, "issingle": 1, "links": [], - "modified": "2021-03-14 02:04:22.907159", + "modified": "2021-03-25 13:18:21.648077", "modified_by": "Administrator", "module": "HR", "name": "HR Settings", diff --git a/erpnext/projects/doctype/timesheet/test_timesheet.py b/erpnext/projects/doctype/timesheet/test_timesheet.py index f7c764e1bd..d21ac0f2f0 100644 --- a/erpnext/projects/doctype/timesheet/test_timesheet.py +++ b/erpnext/projects/doctype/timesheet/test_timesheet.py @@ -151,11 +151,11 @@ class TestTimesheet(unittest.TestCase): settings.save() -def make_salary_structure_for_timesheet(employee): +def make_salary_structure_for_timesheet(employee, company=None): salary_structure_name = "Timesheet Salary Structure Test" frequency = "Monthly" - salary_structure = make_salary_structure(salary_structure_name, frequency, 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 diff --git a/erpnext/projects/report/profitability/profitability.js b/erpnext/projects/report/profitability/profitability.js index dbf918760f..6cb6e39d34 100644 --- a/erpnext/projects/report/profitability/profitability.js +++ b/erpnext/projects/report/profitability/profitability.js @@ -4,17 +4,27 @@ frappe.query_reports["Profitability"] = { "filters": [ + { + "fieldname": "company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "default": frappe.defaults.get_user_default("Company"), + "reqd": 1 + }, { "fieldname": "start_date", "label": __("Start Date"), "fieldtype": "Date", - "reqd": 1 + "reqd": 1, + "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1) }, { "fieldname": "end_date", "label": __("End Date"), "fieldtype": "Date", - "reqd": 1 + "reqd": 1, + "default": frappe.datetime.now_date() }, { "fieldname": "customer_name", diff --git a/erpnext/projects/report/profitability/profitability.py b/erpnext/projects/report/profitability/profitability.py index 0edecd8e1b..8c052b5e17 100644 --- a/erpnext/projects/report/profitability/profitability.py +++ b/erpnext/projects/report/profitability/profitability.py @@ -4,12 +4,80 @@ from __future__ import unicode_literals import frappe from frappe import _ +from frappe.utils import nowdate, time_diff_in_hours def execute(filters=None): columns, data = [], [] data = get_data(filters) columns = get_columns() - return columns, data + charts = get_chart_data(data) + return columns, data, None, charts + +def get_data(filters): + conditions = get_conditions(filters) + default_working_hours = frappe.db.get_single_value("HR Settings", "default_working_hours") + sql = """ + select + *, + t.gross_pay * t.utilization as fractional_cost, + t.grand_total - t.gross_pay * t.utilization as profit + from + (select + si.customer_name,tabTimesheet.title,tabTimesheet.employee,si.grand_total,si.name as voucher_no, + ss.gross_pay,ss.total_working_days,tabTimesheet.end_date,tabTimesheet.total_billed_hours,tabTimesheet.name as timesheet, + tabTimesheet.total_billed_hours/(ss.total_working_days * %s) as utilization + from + `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" """%(default_working_hours) + if conditions: + sql += """ + where + %s) as t"""%(conditions) + data = frappe.db.sql(sql,filters, as_dict=True) + return data + +def get_conditions(filters): + conditions = [] + if filters.get("company"): + conditions.append("tabTimesheet.company='%s'"%filters.get("company")) + if filters.get("customer_name"): + conditions.append("si.customer_name='%s'"%filters.get("customer_name")) + if filters.get("start_date"): + conditions.append("tabTimesheet.start_date>='%s'"%filters.get("start_date")) + if filters.get("end_date"): + conditions.append("tabTimesheet.end_date<='%s'"%filters.get("end_date")) + if filters.get("employee"): + conditions.append("tabTimesheet.employee='%s'"%filters.get("employee")) + + conditions = " and ".join(conditions) + return conditions + +def get_chart_data(data): + if not data: + return None + + labels = [] + utilization = [] + + for entry in data: + labels.append(entry.get("title") + " - " + str(entry.get("end_date"))) + utilization.append(entry.get("utilization")) + charts = { + 'data': { + 'labels': labels, + 'datasets': [ + { + 'name': 'Utilization', + 'values': utilization + } + ] + }, + 'type': 'bar', + 'colors': ['#84BDD5'] + } + return charts def get_columns(): return [ @@ -30,7 +98,21 @@ def get_columns(): "fieldname": "employee", "label": _("Employee"), "fieldtype": "Link", - "options": "employee", + "options": "Employee", + "width": 150 + }, + { + "fieldname": "voucher_no", + "label": _("Sales Invoice"), + "fieldtype": "Link", + "options": "Sales Invoice", + "width": 200 + }, + { + "fieldname": "timesheet", + "label": _("Timesheet"), + "fieldtype": "Link", + "options": "Timesheet", "width": 150 }, { @@ -64,7 +146,7 @@ def get_columns(): "fieldname": "total_billed_hours", "label": _("Total Billed Hours"), "fieldtype": "Int", - "width": 120 + "width": 100 }, { "fieldname": "utilization", @@ -78,42 +160,4 @@ def get_columns(): "fieldtype": "Int", "width": 100 } - ] - -def get_data(filters): - conditions = get_conditions(filters) - sql = """ - select - *, - t.gross_pay * t.utilization as fractional_cost, - t.grand_total - t.gross_pay * t.utilization as profit - from - (select - si.customer_name,tabTimesheet.title,tabTimesheet.employee,si.grand_total,si.name as voucher_no, - ss.gross_pay,ss.total_working_days,tabTimesheet.end_date,tabTimesheet.total_billed_hours, - tabTimesheet.total_billed_hours/(ss.total_working_days * 8) as utilization - from - `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" """ - if conditions: - sql += """ - where - %s) as t"""%(conditions) - data = frappe.db.sql(sql,filters, as_dict=True) - - return data - -def get_conditions(filters): - conditions = [] - if filters.get("customer_name"): - conditions.append("si.customer_name='%s'"%filters.get("customer_name")) - if filters.get("start_date"): - conditions.append("tabTimesheet.start_date>='%s'"%filters.get("start_date")) - if filters.get("end_date"): - conditions.append("tabTimesheet.end_date<='%s'"%filters.get("end_date")) - if filters.get("employee"): - conditions.append("tabTimesheet.employee='%s'"%filters.get("employee")) - conditions = " and ".join(conditions) - return conditions \ No newline at end of file + ] \ No newline at end of file diff --git a/erpnext/projects/report/profitability/test_profitability.py b/erpnext/projects/report/profitability/test_profitability.py new file mode 100644 index 0000000000..dfdef0dcec --- /dev/null +++ b/erpnext/projects/report/profitability/test_profitability.py @@ -0,0 +1,52 @@ +from __future__ import unicode_literals +import unittest +import frappe +import datetime +from frappe.utils import getdate, nowdate, add_days, add_months +from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.projects.doctype.timesheet.test_timesheet import make_salary_structure_for_timesheet, make_timesheet +from erpnext.projects.doctype.timesheet.timesheet import make_salary_slip, make_sales_invoice +from erpnext.projects.report.profitability.profitability import execute + +class TestProfitability(unittest.TestCase): + @classmethod + def setUp(self): + 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() + make_salary_structure_for_timesheet(emp, company="_Test Company") + self.timesheet = make_timesheet(emp, simulate = True, billable=1) + self.salary_slip = make_salary_slip(self.timesheet.name) + self.salary_slip.submit() + self.sales_invoice = make_sales_invoice(self.timesheet.name, '_Test Item', '_Test Customer') + self.sales_invoice.due_date = nowdate() + self.sales_invoice.submit() + + def test_profitability(self): + filters = { + 'company': '_Test Company', + 'start_date': getdate(), + 'end_date': getdate() + } + + report = execute(filters) + expected_data = [ + { + "customer_name": "_Test Customer", + "title": "test_employee_9@salary.com", + "grand_total": 100.0, + "gross_pay": 78100.0, + "profit": -19425.0, + "total_billed_hours": 2.0, + "utilization": 0.25, + "fractional_cost": 19525.0, + "total_working_days": 1.0 + } + ] + for key in ["customer_name","title","grand_total","gross_pay","profit","total_billed_hours","utilization","fractional_cost","total_working_days"]: + self.assertEqual(expected_data[0].get(key), report[1][0].get(key)) + + def tearDown(self): + frappe.get_doc("Sales Invoice", self.sales_invoice.name).cancel() + frappe.get_doc("Salary Slip", self.salary_slip.name).cancel() + frappe.get_doc("Timesheet", self.timesheet.name).cancel() \ No newline at end of file diff --git a/erpnext/projects/workspace/projects/projects.json b/erpnext/projects/workspace/projects/projects.json index dbbd7e1458..8703ffb756 100644 --- a/erpnext/projects/workspace/projects/projects.json +++ b/erpnext/projects/workspace/projects/projects.json @@ -15,6 +15,7 @@ "hide_custom": 0, "icon": "project", "idx": 0, + "is_default": 0, "is_standard": 1, "label": "Projects", "links": [ @@ -129,6 +130,16 @@ "onboard": 1, "type": "Link" }, + { + "dependencies": "Timesheet, Sales Invoice, Salary Slip", + "hidden": 0, + "is_query_report": 1, + "label": "Profitability", + "link_to": "Profitability", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, { "dependencies": "Project", "hidden": 0, @@ -150,7 +161,7 @@ "type": "Link" } ], - "modified": "2020-12-01 13:38:37.856224", + "modified": "2021-03-25 13:25:17.609608", "modified_by": "Administrator", "module": "Projects", "name": "Projects", From c0b4eea415ca3cb3a242f78f882e7ea0f811a836 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Thu, 25 Mar 2021 14:39:05 +0530 Subject: [PATCH 04/15] fix: sider --- .../report/profitability/profitability.py | 17 ++++++++--------- .../report/profitability/test_profitability.py | 3 +-- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/erpnext/projects/report/profitability/profitability.py b/erpnext/projects/report/profitability/profitability.py index 8c052b5e17..8ce2eb09ee 100644 --- a/erpnext/projects/report/profitability/profitability.py +++ b/erpnext/projects/report/profitability/profitability.py @@ -4,7 +4,6 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import nowdate, time_diff_in_hours def execute(filters=None): columns, data = [], [] @@ -25,31 +24,31 @@ def get_data(filters): (select si.customer_name,tabTimesheet.title,tabTimesheet.employee,si.grand_total,si.name as voucher_no, ss.gross_pay,ss.total_working_days,tabTimesheet.end_date,tabTimesheet.total_billed_hours,tabTimesheet.name as timesheet, - tabTimesheet.total_billed_hours/(ss.total_working_days * %s) as utilization + tabTimesheet.total_billed_hours/(ss.total_working_days * {0}) as utilization from `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" """%(default_working_hours) + join `tabSalary Slip` as ss on ss.name = sst.parent and ss.status != "Cancelled" """.format(default_working_hours) if conditions: sql += """ where - %s) as t"""%(conditions) + {0}) as t""".format(conditions) data = frappe.db.sql(sql,filters, as_dict=True) return data def get_conditions(filters): conditions = [] if filters.get("company"): - conditions.append("tabTimesheet.company='%s'"%filters.get("company")) + conditions.append('tabTimesheet.company="{0}"'.format(filters.get("company"))) if filters.get("customer_name"): - conditions.append("si.customer_name='%s'"%filters.get("customer_name")) + conditions.append("si.customer_name='{0}'".format(filters.get("customer_name"))) if filters.get("start_date"): - conditions.append("tabTimesheet.start_date>='%s'"%filters.get("start_date")) + conditions.append("tabTimesheet.start_date>='{0}'".format(filters.get("start_date"))) if filters.get("end_date"): - conditions.append("tabTimesheet.end_date<='%s'"%filters.get("end_date")) + conditions.append("tabTimesheet.end_date<='{0}'".format(filters.get("end_date"))) if filters.get("employee"): - conditions.append("tabTimesheet.employee='%s'"%filters.get("employee")) + conditions.append("tabTimesheet.employee='{0}'".format(filters.get("employee"))) conditions = " and ".join(conditions) return conditions diff --git a/erpnext/projects/report/profitability/test_profitability.py b/erpnext/projects/report/profitability/test_profitability.py index dfdef0dcec..64ab6787eb 100644 --- a/erpnext/projects/report/profitability/test_profitability.py +++ b/erpnext/projects/report/profitability/test_profitability.py @@ -1,8 +1,7 @@ from __future__ import unicode_literals import unittest import frappe -import datetime -from frappe.utils import getdate, nowdate, add_days, add_months +from frappe.utils import getdate, nowdate from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.projects.doctype.timesheet.test_timesheet import make_salary_structure_for_timesheet, make_timesheet from erpnext.projects.doctype.timesheet.timesheet import make_salary_slip, make_sales_invoice From 3b54b1e97588127d0a673f0a1cc769b78867c597 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Mon, 19 Apr 2021 16:49:28 +0530 Subject: [PATCH 05/15] fix: report name and columns --- .../hr/doctype/hr_settings/hr_settings.json | 8 +- .../report/profitability/profitability.py | 162 ---------------- .../profitability/test_profitability.py | 51 ----- .../__init__.py | 0 .../project_profitability.js} | 8 +- .../project_profitability.json} | 25 ++- .../project_profitability.py | 174 ++++++++++++++++++ .../test_project_profitability.py | 51 +++++ .../projects/workspace/projects/projects.json | 6 +- erpnext/regional/india/utils.py | 2 +- 10 files changed, 255 insertions(+), 232 deletions(-) delete mode 100644 erpnext/projects/report/profitability/profitability.py delete mode 100644 erpnext/projects/report/profitability/test_profitability.py rename erpnext/projects/report/{profitability => project_profitability}/__init__.py (100%) rename erpnext/projects/report/{profitability/profitability.js => project_profitability/project_profitability.js} (84%) rename erpnext/projects/report/{profitability/profitability.json => project_profitability/project_profitability.json} (69%) create mode 100644 erpnext/projects/report/project_profitability/project_profitability.py create mode 100644 erpnext/projects/report/project_profitability/test_project_profitability.py diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.json b/erpnext/hr/doctype/hr_settings/hr_settings.json index 4fa50c4852..35532291a5 100644 --- a/erpnext/hr/doctype/hr_settings/hr_settings.json +++ b/erpnext/hr/doctype/hr_settings/hr_settings.json @@ -10,7 +10,7 @@ "retirement_age", "emp_created_by", "column_break_4", - "default_working_hours", + "standard_working_hours", "stop_birthday_reminders", "expense_approver_mandatory_in_expense_claim", "leave_settings", @@ -147,16 +147,16 @@ }, { "default": "8", - "fieldname": "default_working_hours", + "fieldname": "standard_working_hours", "fieldtype": "Int", - "label": "Default Working Hours" + "label": "Standard Working Hours" } ], "icon": "fa fa-cog", "idx": 1, "issingle": 1, "links": [], - "modified": "2021-03-25 13:18:21.648077", + "modified": "2021-04-16 15:45:18.467699", "modified_by": "Administrator", "module": "HR", "name": "HR Settings", diff --git a/erpnext/projects/report/profitability/profitability.py b/erpnext/projects/report/profitability/profitability.py deleted file mode 100644 index 8ce2eb09ee..0000000000 --- a/erpnext/projects/report/profitability/profitability.py +++ /dev/null @@ -1,162 +0,0 @@ -# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -import frappe -from frappe import _ - -def execute(filters=None): - columns, data = [], [] - data = get_data(filters) - columns = get_columns() - charts = get_chart_data(data) - return columns, data, None, charts - -def get_data(filters): - conditions = get_conditions(filters) - default_working_hours = frappe.db.get_single_value("HR Settings", "default_working_hours") - sql = """ - select - *, - t.gross_pay * t.utilization as fractional_cost, - t.grand_total - t.gross_pay * t.utilization as profit - from - (select - si.customer_name,tabTimesheet.title,tabTimesheet.employee,si.grand_total,si.name as voucher_no, - ss.gross_pay,ss.total_working_days,tabTimesheet.end_date,tabTimesheet.total_billed_hours,tabTimesheet.name as timesheet, - tabTimesheet.total_billed_hours/(ss.total_working_days * {0}) as utilization - from - `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(default_working_hours) - if conditions: - sql += """ - where - {0}) as t""".format(conditions) - data = frappe.db.sql(sql,filters, as_dict=True) - return data - -def get_conditions(filters): - conditions = [] - if filters.get("company"): - conditions.append('tabTimesheet.company="{0}"'.format(filters.get("company"))) - if filters.get("customer_name"): - conditions.append("si.customer_name='{0}'".format(filters.get("customer_name"))) - if filters.get("start_date"): - conditions.append("tabTimesheet.start_date>='{0}'".format(filters.get("start_date"))) - if filters.get("end_date"): - conditions.append("tabTimesheet.end_date<='{0}'".format(filters.get("end_date"))) - if filters.get("employee"): - conditions.append("tabTimesheet.employee='{0}'".format(filters.get("employee"))) - - conditions = " and ".join(conditions) - return conditions - -def get_chart_data(data): - if not data: - return None - - labels = [] - utilization = [] - - for entry in data: - labels.append(entry.get("title") + " - " + str(entry.get("end_date"))) - utilization.append(entry.get("utilization")) - charts = { - 'data': { - 'labels': labels, - 'datasets': [ - { - 'name': 'Utilization', - 'values': utilization - } - ] - }, - 'type': 'bar', - 'colors': ['#84BDD5'] - } - return charts - -def get_columns(): - return [ - { - "fieldname": "customer_name", - "label": _("Customer"), - "fieldtype": "Link", - "options": "Customer", - "width": 150 - }, - { - "fieldname": "title", - "label": _("Name"), - "fieldtype": "Data", - "width": 120 - }, - { - "fieldname": "employee", - "label": _("Employee"), - "fieldtype": "Link", - "options": "Employee", - "width": 150 - }, - { - "fieldname": "voucher_no", - "label": _("Sales Invoice"), - "fieldtype": "Link", - "options": "Sales Invoice", - "width": 200 - }, - { - "fieldname": "timesheet", - "label": _("Timesheet"), - "fieldtype": "Link", - "options": "Timesheet", - "width": 150 - }, - { - "fieldname": "grand_total", - "label": _("Bill Amount"), - "fieldtype": "Currency", - "options": "currency", - "width": 120 - }, - { - "fieldname": "gross_pay", - "label": _("Cost"), - "fieldtype": "Currency", - "options": "currency", - "width": 120 - }, - { - "fieldname": "profit", - "label": _("Profit"), - "fieldtype": "Currency", - "options": "currency", - "width": 120 - }, - { - "fieldname": "end_date", - "label": _("End Date"), - "fieldtype": "Date", - "width": 120 - }, - { - "fieldname": "total_billed_hours", - "label": _("Total Billed Hours"), - "fieldtype": "Int", - "width": 100 - }, - { - "fieldname": "utilization", - "label": _("Utilization"), - "fieldtype": "Percentage", - "width": 120 - }, - { - "fieldname": "fractional_cost", - "label": _("Fractional Cost"), - "fieldtype": "Int", - "width": 100 - } - ] \ No newline at end of file diff --git a/erpnext/projects/report/profitability/test_profitability.py b/erpnext/projects/report/profitability/test_profitability.py deleted file mode 100644 index 64ab6787eb..0000000000 --- a/erpnext/projects/report/profitability/test_profitability.py +++ /dev/null @@ -1,51 +0,0 @@ -from __future__ import unicode_literals -import unittest -import frappe -from frappe.utils import getdate, nowdate -from erpnext.hr.doctype.employee.test_employee import make_employee -from erpnext.projects.doctype.timesheet.test_timesheet import make_salary_structure_for_timesheet, make_timesheet -from erpnext.projects.doctype.timesheet.timesheet import make_salary_slip, make_sales_invoice -from erpnext.projects.report.profitability.profitability import execute - -class TestProfitability(unittest.TestCase): - @classmethod - def setUp(self): - 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() - make_salary_structure_for_timesheet(emp, company="_Test Company") - self.timesheet = make_timesheet(emp, simulate = True, billable=1) - self.salary_slip = make_salary_slip(self.timesheet.name) - self.salary_slip.submit() - self.sales_invoice = make_sales_invoice(self.timesheet.name, '_Test Item', '_Test Customer') - self.sales_invoice.due_date = nowdate() - self.sales_invoice.submit() - - def test_profitability(self): - filters = { - 'company': '_Test Company', - 'start_date': getdate(), - 'end_date': getdate() - } - - report = execute(filters) - expected_data = [ - { - "customer_name": "_Test Customer", - "title": "test_employee_9@salary.com", - "grand_total": 100.0, - "gross_pay": 78100.0, - "profit": -19425.0, - "total_billed_hours": 2.0, - "utilization": 0.25, - "fractional_cost": 19525.0, - "total_working_days": 1.0 - } - ] - for key in ["customer_name","title","grand_total","gross_pay","profit","total_billed_hours","utilization","fractional_cost","total_working_days"]: - self.assertEqual(expected_data[0].get(key), report[1][0].get(key)) - - def tearDown(self): - frappe.get_doc("Sales Invoice", self.sales_invoice.name).cancel() - frappe.get_doc("Salary Slip", self.salary_slip.name).cancel() - frappe.get_doc("Timesheet", self.timesheet.name).cancel() \ No newline at end of file diff --git a/erpnext/projects/report/profitability/__init__.py b/erpnext/projects/report/project_profitability/__init__.py similarity index 100% rename from erpnext/projects/report/profitability/__init__.py rename to erpnext/projects/report/project_profitability/__init__.py diff --git a/erpnext/projects/report/profitability/profitability.js b/erpnext/projects/report/project_profitability/project_profitability.js similarity index 84% rename from erpnext/projects/report/profitability/profitability.js rename to erpnext/projects/report/project_profitability/project_profitability.js index 6cb6e39d34..cdf7bfdc9f 100644 --- a/erpnext/projects/report/profitability/profitability.js +++ b/erpnext/projects/report/project_profitability/project_profitability.js @@ -2,7 +2,7 @@ // For license information, please see license.txt /* eslint-disable */ -frappe.query_reports["Profitability"] = { +frappe.query_reports["Project Profitability"] = { "filters": [ { "fieldname": "company", @@ -26,6 +26,12 @@ frappe.query_reports["Profitability"] = { "reqd": 1, "default": frappe.datetime.now_date() }, + { + "fieldname": "project", + "label": __("Project"), + "fieldtype": "Link", + "options": "Project" + }, { "fieldname": "customer_name", "label": __("Customer"), diff --git a/erpnext/projects/report/profitability/profitability.json b/erpnext/projects/report/project_profitability/project_profitability.json similarity index 69% rename from erpnext/projects/report/profitability/profitability.json rename to erpnext/projects/report/project_profitability/project_profitability.json index 4f91accf58..0b092cd2c0 100644 --- a/erpnext/projects/report/profitability/profitability.json +++ b/erpnext/projects/report/project_profitability/project_profitability.json @@ -1,7 +1,7 @@ { "add_total_row": 0, "columns": [], - "creation": "2021-03-18 10:19:40.124932", + "creation": "2021-04-16 15:50:28.914872", "disable_prepared_report": 0, "disabled": 0, "docstatus": 0, @@ -9,31 +9,36 @@ "filters": [], "idx": 0, "is_standard": "Yes", - "json": "{}", - "modified": "2021-03-18 10:20:15.559305", + "modified": "2021-04-16 15:50:48.490866", "modified_by": "Administrator", "module": "Projects", - "name": "Profitability", + "name": "Project Profitability", "owner": "Administrator", "prepared_report": 0, "ref_doctype": "Timesheet", - "report_name": "Profitability", + "report_name": "Project Profitability", "report_type": "Script Report", "roles": [ - { - "role": "Projects User" - }, { "role": "HR User" }, { - "role": "Manufacturing User" + "role": "Accounts User" }, { "role": "Employee" }, { - "role": "Accounts User" + "role": "Projects User" + }, + { + "role": "Manufacturing User" + }, + { + "role": "Employee Self Service" + }, + { + "role": "HR Manager" } ] } \ No newline at end of file diff --git a/erpnext/projects/report/project_profitability/project_profitability.py b/erpnext/projects/report/project_profitability/project_profitability.py new file mode 100644 index 0000000000..7a76213994 --- /dev/null +++ b/erpnext/projects/report/project_profitability/project_profitability.py @@ -0,0 +1,174 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe + +def execute(filters=None): + columns, data = [], [] + data = get_data(filters) + columns = get_columns() + charts = get_chart_data(data) + return columns, data, None, charts + +def get_data(filters): + conditions = get_conditions(filters) + standard_working_hours = frappe.db.get_single_value('HR Settings', 'standard_working_hours') + sql = ''' + SELECT + * + FROM + (SELECT + si.customer_name,tabTimesheet.title, + tabTimesheet.employee,si.base_grand_total + si.name as voucher_no,ss.base_gross_pay,ss.total_working_days, + tabTimesheet.end_date,tabTimesheet.total_billed_hours, + tabTimesheet.name as timesheet, + tabTimesheet.total_billed_hours/(ss.total_working_days * {0}) as utilization + FROM + `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) + if conditions: + sql += ''' + where + {0}) as t'''.format(conditions) + data = frappe.db.sql(sql,filters, as_dict=True) + data = perform_calculations(data) + return data + +def perform_calculations(data): + data.fractional_cost = data.base_gross_pay * data.utilization + data.profit = data.base_grand_total - data.base_gross_pay + return data + +def get_conditions(filters): + conditions = [] + if filters.get('company'): + conditions.append('tabTimesheet.company="{0}"'.format(filters.get('company'))) + if filters.get('customer_name'): + conditions.append('si.customer_name="{0}"'.format(filters.get('customer_name'))) + if filters.get('start_date'): + conditions.append('tabTimesheet.start_date>="{0}"'.format(filters.get('start_date'))) + if filters.get('end_date'): + conditions.append('tabTimesheet.end_date<="{0}"'.format(filters.get('end_date'))) + if filters.get('employee'): + conditions.append('tabTimesheet.employee="{0}"'.format(filters.get('employee'))) + + conditions = ' and '.join(conditions) + return conditions + +def get_chart_data(data): + if not data: + return None + + labels = [] + utilization = [] + + for entry in data: + labels.append(entry.get('title') + ' - ' + str(entry.get('end_date'))) + utilization.append(entry.get('utilization')) + charts = { + 'data': { + 'labels': labels, + 'datasets': [ + { + 'name': 'Utilization', + 'values': utilization + } + ] + }, + 'type': 'bar', + 'colors': ['#84BDD5'] + } + return charts + +def get_columns(): + return [ + { + 'fieldname': 'customer_name', + 'label': _('Customer'), + 'fieldtype': 'Link', + 'options': 'Customer', + 'width': 150 + }, + { + 'fieldname': 'employee', + 'label': _('Employee'), + 'fieldtype': 'Link', + 'options': 'Employee', + 'width': 150 + }, + { + 'fieldname': 'employee_name', + 'label': _('Employee Name'), + 'fieldtype': 'Data', + 'width': 120 + }, + { + 'fieldname': 'voucher_no', + 'label': _('Sales Invoice'), + 'fieldtype': 'Link', + 'options': 'Sales Invoice', + 'width': 200 + }, + { + 'fieldname': 'timesheet', + 'label': _('Timesheet'), + 'fieldtype': 'Link', + 'options': 'Timesheet', + 'width': 150 + }, + { + 'fieldname': 'grand_total', + 'label': _('Bill Amount'), + 'fieldtype': 'Currency', + 'options': 'currency', + 'width': 120 + }, + { + 'fieldname': 'gross_pay', + 'label': _('Cost'), + 'fieldtype': 'Currency', + 'options': 'currency', + 'width': 120 + }, + { + 'fieldname': 'profit', + 'label': _('Profit'), + 'fieldtype': 'Currency', + 'options': 'currency', + 'width': 120 + }, + { + 'fieldname': 'utilization', + 'label': _('Utilization'), + 'fieldtype': 'Percentage', + 'width': 120 + }, + { + 'fieldname': 'fractional_cost', + 'label': _('Fractional Cost'), + 'fieldtype': 'Int', + 'width': 100 + }, + { + 'fieldname': 'total_billed_hours', + 'label': _('Total Billed Hours'), + 'fieldtype': 'Int', + 'width': 100 + }, + { + 'fieldname': 'start_date', + 'label': _('Start Date'), + 'fieldtype': 'Date', + 'width': 120 + }, + { + 'fieldname': 'end_date', + 'label': _('End Date'), + 'fieldtype': 'Date', + 'width': 120 + } + ] diff --git a/erpnext/projects/report/project_profitability/test_project_profitability.py b/erpnext/projects/report/project_profitability/test_project_profitability.py new file mode 100644 index 0000000000..7036547e40 --- /dev/null +++ b/erpnext/projects/report/project_profitability/test_project_profitability.py @@ -0,0 +1,51 @@ +from __future__ import unicode_literals +import unittest +import frappe +from frappe.utils import getdate, nowdate +from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.projects.doctype.timesheet.test_timesheet import make_salary_structure_for_timesheet, make_timesheet +from erpnext.projects.doctype.timesheet.timesheet import make_salary_slip, make_sales_invoice +from erpnext.projects.report.project_profitability.project_profitability import execute + +class TestProjectProfitability(unittest.TestCase): + @classmethod + def setUp(self): + 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() + make_salary_structure_for_timesheet(emp, company='_Test Company') + self.timesheet = make_timesheet(emp, simulate = True, billable=1) + self.salary_slip = make_salary_slip(self.timesheet.name) + self.salary_slip.submit() + self.sales_invoice = make_sales_invoice(self.timesheet.name, '_Test Item', '_Test Customer') + self.sales_invoice.due_date = nowdate() + self.sales_invoice.submit() + + def test_project_profitability(self): + filters = { + 'company': '_Test Company', + 'start_date': getdate(), + 'end_date': getdate() + } + + report = execute(filters) + expected_data = [ + { + 'customer_name': '_Test Customer', + 'title': 'test_employee_9@salary.com', + 'grand_total': 100.0, + 'gross_pay': 78100.0, + 'profit': -19425.0, + 'total_billed_hours': 2.0, + 'utilization': 0.25, + 'fractional_cost': 19525.0, + 'total_working_days': 1.0 + } + ] + for key in ['customer_name','title','grand_total','gross_pay','profit','total_billed_hours','utilization','fractional_cost','total_working_days']: + self.assertEqual(expected_data[0].get(key), report[1][0].get(key)) + + def tearDown(self): + frappe.get_doc('Sales Invoice', self.sales_invoice.name).cancel() + frappe.get_doc('Salary Slip', self.salary_slip.name).cancel() + frappe.get_doc('Timesheet', self.timesheet.name).cancel() \ No newline at end of file diff --git a/erpnext/projects/workspace/projects/projects.json b/erpnext/projects/workspace/projects/projects.json index 8703ffb756..621c4bb52f 100644 --- a/erpnext/projects/workspace/projects/projects.json +++ b/erpnext/projects/workspace/projects/projects.json @@ -134,8 +134,8 @@ "dependencies": "Timesheet, Sales Invoice, Salary Slip", "hidden": 0, "is_query_report": 1, - "label": "Profitability", - "link_to": "Profitability", + "label": "Project Profitability", + "link_to": "Project Profitability", "link_type": "Report", "onboard": 0, "type": "Link" @@ -161,7 +161,7 @@ "type": "Link" } ], - "modified": "2021-03-25 13:25:17.609608", + "modified": "2021-04-16 16:27:16.548780", "modified_by": "Administrator", "module": "Projects", "name": "Projects", diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 3637de438c..ff4ac07041 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -159,7 +159,7 @@ def validate_document_name(doc, method=None): # Date was chosen as start of next FY to avoid irritating current users. if country != "India" or getdate(doc.posting_date) < getdate("2021-04-01"): return - + print(doc.name) if len(doc.name) > 16: frappe.throw(_("Maximum length of document number should be 16 characters as per GST rules. Please change the naming series.")) From cbdcfb4873f5b63e6215df9aef86a788f097b70b Mon Sep 17 00:00:00 2001 From: pateljannat Date: Tue, 20 Apr 2021 17:53:41 +0530 Subject: [PATCH 06/15] fix: multicurrency --- erpnext/hooks.py | 1 - .../project_profitability.js | 12 +- .../project_profitability.py | 281 +++++++++++------- .../test_project_profitability.py | 14 +- 4 files changed, 186 insertions(+), 122 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index bb6cd8bdc2..af357cd331 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -262,7 +262,6 @@ doc_events = { ], "on_trash": "erpnext.regional.check_deletion_permission", "validate": [ - "erpnext.regional.india.utils.validate_document_name", "erpnext.regional.india.utils.update_taxable_values" ] }, diff --git a/erpnext/projects/report/project_profitability/project_profitability.js b/erpnext/projects/report/project_profitability/project_profitability.js index cdf7bfdc9f..13ae19bb29 100644 --- a/erpnext/projects/report/project_profitability/project_profitability.js +++ b/erpnext/projects/report/project_profitability/project_profitability.js @@ -26,12 +26,6 @@ frappe.query_reports["Project Profitability"] = { "reqd": 1, "default": frappe.datetime.now_date() }, - { - "fieldname": "project", - "label": __("Project"), - "fieldtype": "Link", - "options": "Project" - }, { "fieldname": "customer_name", "label": __("Customer"), @@ -43,6 +37,12 @@ frappe.query_reports["Project Profitability"] = { "label": __("Employee"), "fieldtype": "Link", "options": "Employee" + }, + { + "fieldname": "project", + "label": __("Project"), + "fieldtype": "Link", + "options": "Project" } ] }; diff --git a/erpnext/projects/report/project_profitability/project_profitability.py b/erpnext/projects/report/project_profitability/project_profitability.py index 7a76213994..405c6fd344 100644 --- a/erpnext/projects/report/project_profitability/project_profitability.py +++ b/erpnext/projects/report/project_profitability/project_profitability.py @@ -2,7 +2,12 @@ # For license information, please see license.txt from __future__ import unicode_literals -# import frappe +import frappe +from frappe import _ +from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_employee_currency +from erpnext.accounts.utils import get_currency_precision +from frappe.utils import flt +from erpnext.setup.utils import get_exchange_rate def execute(filters=None): columns, data = [], [] @@ -12,51 +17,96 @@ def execute(filters=None): return columns, data, None, charts def get_data(filters): - conditions = get_conditions(filters) - standard_working_hours = frappe.db.get_single_value('HR Settings', 'standard_working_hours') - sql = ''' - SELECT - * - FROM - (SELECT - si.customer_name,tabTimesheet.title, - tabTimesheet.employee,si.base_grand_total - si.name as voucher_no,ss.base_gross_pay,ss.total_working_days, - tabTimesheet.end_date,tabTimesheet.total_billed_hours, - tabTimesheet.name as timesheet, - tabTimesheet.total_billed_hours/(ss.total_working_days * {0}) as utilization - FROM - `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) - if conditions: - sql += ''' - where - {0}) as t'''.format(conditions) - data = frappe.db.sql(sql,filters, as_dict=True) - data = perform_calculations(data) + data = get_rows(filters) + data = handle_multi_currency(data, filters) + data = calculate_cost_and_profit(data) return data -def perform_calculations(data): - data.fractional_cost = data.base_gross_pay * data.utilization - data.profit = data.base_grand_total - data.base_gross_pay +def get_rows(filters): + conditions = get_conditions(filters) + standard_working_hours = frappe.db.get_single_value("HR Settings", "standard_working_hours") + sql = """ + SELECT + * + FROM + (SELECT + si.customer_name,si.base_grand_total, + si.name as voucher_no,tabTimesheet.employee, + tabTimesheet.title as employee_name,tabTimesheet.parent_project as project, + tabTimesheet.start_date,tabTimesheet.end_date, + tabTimesheet.total_billed_hours,tabTimesheet.name as timesheet, + ss.base_gross_pay,ss.total_working_days, + tabTimesheet.total_billed_hours/(ss.total_working_days * {0}) as utilization + FROM + `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) + if conditions: + sql += """ + WHERE + {0}) as t""".format(conditions) + return frappe.db.sql(sql,filters, as_dict=True) + +def handle_multi_currency(data, filters): + currency_precision = get_currency_precision() or 2 + company_currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency") + + for row in data: + row.currency = company_currency + + if filters.get("employee"): + party_currency = get_employee_currency(row.employee) + + if filters.get("customer_name"): + party_currency = frappe.db.get_value("Customer", row.customer_name, ["default_currency"]) + + if party_currency and party_currency != company_currency: + exchange_rate = get_exchange_rate(company_currency, party_currency) + row.currency = party_currency + + row.base_grand_total = flt(flt(row.base_grand_total) * + flt(exchange_rate), currency_precision) + + row.base_gross_pay = flt(flt(row.base_gross_pay) * + flt(exchange_rate), currency_precision) + + row.profit = flt(flt(row.profit) * + flt(exchange_rate), currency_precision) + + row.fractional_cost = flt(flt(row.fractional_cost) * + flt(exchange_rate), currency_precision) + + return data + +def calculate_cost_and_profit(data): + for row in data: + row.fractional_cost = row.base_gross_pay * row.utilization + row.profit = row.base_grand_total - row.base_gross_pay * row.utilization return data def get_conditions(filters): conditions = [] - if filters.get('company'): - conditions.append('tabTimesheet.company="{0}"'.format(filters.get('company'))) - if filters.get('customer_name'): - conditions.append('si.customer_name="{0}"'.format(filters.get('customer_name'))) - if filters.get('start_date'): - conditions.append('tabTimesheet.start_date>="{0}"'.format(filters.get('start_date'))) - if filters.get('end_date'): - conditions.append('tabTimesheet.end_date<="{0}"'.format(filters.get('end_date'))) - if filters.get('employee'): - conditions.append('tabTimesheet.employee="{0}"'.format(filters.get('employee'))) + + if filters.get("company"): + conditions.append("tabTimesheet.company='{0}'".format(filters.get("company"))) + + if filters.get("start_date"): + conditions.append("tabTimesheet.start_date>='{0}'".format(filters.get("start_date"))) + + if filters.get("end_date"): + conditions.append("tabTimesheet.end_date<='{0}'".format(filters.get("end_date"))) + + if filters.get("customer_name"): + conditions.append("si.customer_name='{0}'".format(filters.get("customer_name"))) + + if filters.get("employee"): + conditions.append("tabTimesheet.employee='{0}'".format(filters.get("employee"))) + + if filters.get("project"): + conditions.append("tabTimesheet.parent_project='{0}'".format(filters.get("project"))) - conditions = ' and '.join(conditions) + conditions = " and ".join(conditions) return conditions def get_chart_data(data): @@ -67,108 +117,123 @@ def get_chart_data(data): utilization = [] for entry in data: - labels.append(entry.get('title') + ' - ' + str(entry.get('end_date'))) - utilization.append(entry.get('utilization')) + labels.append(entry.get("employee_name") + " - " + str(entry.get("end_date"))) + utilization.append(entry.get("utilization")) + charts = { - 'data': { - 'labels': labels, - 'datasets': [ + "data": { + "labels": labels, + "datasets": [ { - 'name': 'Utilization', - 'values': utilization + "name": "Utilization", + "values": utilization } ] }, - 'type': 'bar', - 'colors': ['#84BDD5'] + "type": "bar", + "colors": ["#84BDD5"] } return charts def get_columns(): return [ { - 'fieldname': 'customer_name', - 'label': _('Customer'), - 'fieldtype': 'Link', - 'options': 'Customer', - 'width': 150 + "fieldname": "customer_name", + "label": _("Customer"), + "fieldtype": "Link", + "options": "Customer", + "width": 150 }, { - 'fieldname': 'employee', - 'label': _('Employee'), - 'fieldtype': 'Link', - 'options': 'Employee', - 'width': 150 + "fieldname": "employee", + "label": _("Employee"), + "fieldtype": "Link", + "options": "Employee", + "width": 130 }, { - 'fieldname': 'employee_name', - 'label': _('Employee Name'), - 'fieldtype': 'Data', - 'width': 120 + "fieldname": "employee_name", + "label": _("Employee Name"), + "fieldtype": "Data", + "width": 120 }, { - 'fieldname': 'voucher_no', - 'label': _('Sales Invoice'), - 'fieldtype': 'Link', - 'options': 'Sales Invoice', - 'width': 200 + "fieldname": "voucher_no", + "label": _("Sales Invoice"), + "fieldtype": "Link", + "options": "Sales Invoice", + "width": 180 }, { - 'fieldname': 'timesheet', - 'label': _('Timesheet'), - 'fieldtype': 'Link', - 'options': 'Timesheet', - 'width': 150 + "fieldname": "timesheet", + "label": _("Timesheet"), + "fieldtype": "Link", + "options": "Timesheet", + "width": 130 }, { - 'fieldname': 'grand_total', - 'label': _('Bill Amount'), - 'fieldtype': 'Currency', - 'options': 'currency', - 'width': 120 + "fieldname": "project", + "label": _("Project"), + "fieldtype": "Link", + "options": "Project", + "width": 100 }, { - 'fieldname': 'gross_pay', - 'label': _('Cost'), - 'fieldtype': 'Currency', - 'options': 'currency', - 'width': 120 + "fieldname": "base_grand_total", + "label": _("Bill Amount"), + "fieldtype": "Currency", + "options": "currency", + "width": 100 }, { - 'fieldname': 'profit', - 'label': _('Profit'), - 'fieldtype': 'Currency', - 'options': 'currency', - 'width': 120 + "fieldname": "base_gross_pay", + "label": _("Cost"), + "fieldtype": "Currency", + "options": "currency", + "width": 100 }, { - 'fieldname': 'utilization', - 'label': _('Utilization'), - 'fieldtype': 'Percentage', - 'width': 120 + "fieldname": "profit", + "label": _("Profit"), + "fieldtype": "Currency", + "options": "currency", + "width": 100 }, { - 'fieldname': 'fractional_cost', - 'label': _('Fractional Cost'), - 'fieldtype': 'Int', - 'width': 100 + "fieldname": "utilization", + "label": _("Utilization"), + "fieldtype": "Percentage", + "width": 120 }, { - 'fieldname': 'total_billed_hours', - 'label': _('Total Billed Hours'), - 'fieldtype': 'Int', - 'width': 100 + "fieldname": "fractional_cost", + "label": _("Fractional Cost"), + "fieldtype": "Int", + "width": 100 }, { - 'fieldname': 'start_date', - 'label': _('Start Date'), - 'fieldtype': 'Date', - 'width': 120 + "fieldname": "total_billed_hours", + "label": _("Total Billed Hours"), + "fieldtype": "Int", + "width": 100 }, { - 'fieldname': 'end_date', - 'label': _('End Date'), - 'fieldtype': 'Date', - 'width': 120 + "fieldname": "start_date", + "label": _("Start Date"), + "fieldtype": "Date", + "width": 120 + }, + { + "fieldname": "end_date", + "label": _("End Date"), + "fieldtype": "Date", + "width": 120 + }, + { + "label": _("Currency"), + "fieldname": "currency", + "fieldtype": "Link", + "options": "Currency", + "width": 100 } - ] + ] \ No newline at end of file diff --git a/erpnext/projects/report/project_profitability/test_project_profitability.py b/erpnext/projects/report/project_profitability/test_project_profitability.py index 7036547e40..96659bc606 100644 --- a/erpnext/projects/report/project_profitability/test_project_profitability.py +++ b/erpnext/projects/report/project_profitability/test_project_profitability.py @@ -32,9 +32,9 @@ class TestProjectProfitability(unittest.TestCase): expected_data = [ { 'customer_name': '_Test Customer', - 'title': 'test_employee_9@salary.com', - 'grand_total': 100.0, - 'gross_pay': 78100.0, + 'employee_name': 'test_employee_9@salary.com', + 'base_grand_total': 100.0, + 'base_gross_pay': 78100.0, 'profit': -19425.0, 'total_billed_hours': 2.0, 'utilization': 0.25, @@ -42,10 +42,10 @@ class TestProjectProfitability(unittest.TestCase): 'total_working_days': 1.0 } ] - for key in ['customer_name','title','grand_total','gross_pay','profit','total_billed_hours','utilization','fractional_cost','total_working_days']: + for key in ['customer_name','employee_name','base_grand_total','base_gross_pay','profit','total_billed_hours','utilization','fractional_cost','total_working_days']: self.assertEqual(expected_data[0].get(key), report[1][0].get(key)) def tearDown(self): - frappe.get_doc('Sales Invoice', self.sales_invoice.name).cancel() - frappe.get_doc('Salary Slip', self.salary_slip.name).cancel() - frappe.get_doc('Timesheet', self.timesheet.name).cancel() \ No newline at end of file + frappe.get_doc("Sales Invoice", self.sales_invoice.name).cancel() + frappe.get_doc("Salary Slip", self.salary_slip.name).cancel() + frappe.get_doc("Timesheet", self.timesheet.name).cancel() \ No newline at end of file From 039a5a829ddc7a7ef034665c7050f06277c7da8b Mon Sep 17 00:00:00 2001 From: pateljannat Date: Wed, 21 Apr 2021 12:32:25 +0530 Subject: [PATCH 07/15] fix: dynamic values for tests --- .../test_project_profitability.py | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/erpnext/projects/report/project_profitability/test_project_profitability.py b/erpnext/projects/report/project_profitability/test_project_profitability.py index 96659bc606..251b71da59 100644 --- a/erpnext/projects/report/project_profitability/test_project_profitability.py +++ b/erpnext/projects/report/project_profitability/test_project_profitability.py @@ -29,21 +29,26 @@ class TestProjectProfitability(unittest.TestCase): } report = execute(filters) - expected_data = [ - { - 'customer_name': '_Test Customer', - 'employee_name': 'test_employee_9@salary.com', - 'base_grand_total': 100.0, - 'base_gross_pay': 78100.0, - 'profit': -19425.0, - 'total_billed_hours': 2.0, - 'utilization': 0.25, - 'fractional_cost': 19525.0, - 'total_working_days': 1.0 - } - ] - for key in ['customer_name','employee_name','base_grand_total','base_gross_pay','profit','total_billed_hours','utilization','fractional_cost','total_working_days']: - self.assertEqual(expected_data[0].get(key), report[1][0].get(key)) + + row = report[1][0] + timesheet = frappe.get_doc("Timesheet", self.timesheet.name) + + self.assertEqual(self.sales_invoice.customer, row.customer_name) + self.assertEqual(timesheet.title, row.employee_name) + self.assertEqual(self.sales_invoice.base_grand_total, row.base_grand_total) + self.assertEqual(self.salary_slip.base_gross_pay, row.base_gross_pay) + self.assertEqual(timesheet.total_billed_hours, row.total_billed_hours) + 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) + self.assertEqual(utilization, row.utilization) + + profit = self.sales_invoice.base_grand_total - self.salary_slip.base_gross_pay * utilization + self.assertEqual(profit, row.profit) + + fractional_cost = self.salary_slip.base_gross_pay * utilization + self.assertEqual(fractional_cost, row.fractional_cost) def tearDown(self): frappe.get_doc("Sales Invoice", self.sales_invoice.name).cancel() From 485464d1f49172debd439fd50917fba2744b2526 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Wed, 21 Apr 2021 18:04:58 +0530 Subject: [PATCH 08/15] fix: added back a hook --- erpnext/hooks.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index af357cd331..bb6cd8bdc2 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -262,6 +262,7 @@ doc_events = { ], "on_trash": "erpnext.regional.check_deletion_permission", "validate": [ + "erpnext.regional.india.utils.validate_document_name", "erpnext.regional.india.utils.update_taxable_values" ] }, From ba6ad7f5be20b9d23346a439d1e0092857553d66 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Thu, 22 Apr 2021 11:19:44 +0530 Subject: [PATCH 09/15] fix: escape for format and msg for working hours if not set --- .../project_profitability/project_profitability.py | 13 +++++++++---- erpnext/regional/india/utils.py | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/erpnext/projects/report/project_profitability/project_profitability.py b/erpnext/projects/report/project_profitability/project_profitability.py index 405c6fd344..7703b81cf7 100644 --- a/erpnext/projects/report/project_profitability/project_profitability.py +++ b/erpnext/projects/report/project_profitability/project_profitability.py @@ -25,6 +25,11 @@ def get_data(filters): 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: + hr_settings = "HR Settings" + frappe.msgprint(_("The metrics for this report are calculated based on the Standard Working Hours. Please set Standard Working Hours in {0}.").format(hr_settings)) + return [] + sql = """ SELECT * @@ -89,7 +94,7 @@ def get_conditions(filters): conditions = [] if filters.get("company"): - conditions.append("tabTimesheet.company='{0}'".format(filters.get("company"))) + conditions.append("tabTimesheet.company={0}".format(frappe.db.escape(filters.get("company")))) if filters.get("start_date"): conditions.append("tabTimesheet.start_date>='{0}'".format(filters.get("start_date"))) @@ -98,13 +103,13 @@ def get_conditions(filters): conditions.append("tabTimesheet.end_date<='{0}'".format(filters.get("end_date"))) if filters.get("customer_name"): - conditions.append("si.customer_name='{0}'".format(filters.get("customer_name"))) + conditions.append("si.customer_name={0}".format(frappe.db.escape(filters.get("customer_name")))) if filters.get("employee"): - conditions.append("tabTimesheet.employee='{0}'".format(filters.get("employee"))) + conditions.append("tabTimesheet.employee={0}".format(frappe.db.escape(filters.get("employee")))) if filters.get("project"): - conditions.append("tabTimesheet.parent_project='{0}'".format(filters.get("project"))) + conditions.append("tabTimesheet.parent_project={0}".format(frappe.db.escape(filters.get("project")))) conditions = " and ".join(conditions) return conditions diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 0c757e962d..6338056698 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -161,7 +161,7 @@ def validate_document_name(doc, method=None): # Date was chosen as start of next FY to avoid irritating current users. if country != "India" or getdate(doc.posting_date) < getdate("2021-04-01"): return - print(doc.name) + if len(doc.name) > 16: frappe.throw(_("Maximum length of document number should be 16 characters as per GST rules. Please change the naming series.")) From 2004fec8eb3aa03d933fe5fdc6f217f5ec7c70e5 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Thu, 22 Apr 2021 14:52:36 +0530 Subject: [PATCH 10/15] fix: removed multicurrency --- .../project_profitability.py | 32 ------------------- 1 file changed, 32 deletions(-) diff --git a/erpnext/projects/report/project_profitability/project_profitability.py b/erpnext/projects/report/project_profitability/project_profitability.py index 7703b81cf7..0051c47458 100644 --- a/erpnext/projects/report/project_profitability/project_profitability.py +++ b/erpnext/projects/report/project_profitability/project_profitability.py @@ -18,7 +18,6 @@ def execute(filters=None): def get_data(filters): data = get_rows(filters) - data = handle_multi_currency(data, filters) data = calculate_cost_and_profit(data) return data @@ -53,37 +52,6 @@ def get_rows(filters): {0}) as t""".format(conditions) return frappe.db.sql(sql,filters, as_dict=True) -def handle_multi_currency(data, filters): - currency_precision = get_currency_precision() or 2 - company_currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency") - - for row in data: - row.currency = company_currency - - if filters.get("employee"): - party_currency = get_employee_currency(row.employee) - - if filters.get("customer_name"): - party_currency = frappe.db.get_value("Customer", row.customer_name, ["default_currency"]) - - if party_currency and party_currency != company_currency: - exchange_rate = get_exchange_rate(company_currency, party_currency) - row.currency = party_currency - - row.base_grand_total = flt(flt(row.base_grand_total) * - flt(exchange_rate), currency_precision) - - row.base_gross_pay = flt(flt(row.base_gross_pay) * - flt(exchange_rate), currency_precision) - - row.profit = flt(flt(row.profit) * - flt(exchange_rate), currency_precision) - - row.fractional_cost = flt(flt(row.fractional_cost) * - flt(exchange_rate), currency_precision) - - return data - def calculate_cost_and_profit(data): for row in data: row.fractional_cost = row.base_gross_pay * row.utilization From 84ef7419da0e117466cac6170bade3ec29c23caf Mon Sep 17 00:00:00 2001 From: pateljannat Date: Thu, 22 Apr 2021 16:31:16 +0530 Subject: [PATCH 11/15] fix: sider --- .../report/project_profitability/project_profitability.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/erpnext/projects/report/project_profitability/project_profitability.py b/erpnext/projects/report/project_profitability/project_profitability.py index 0051c47458..593ce1b22b 100644 --- a/erpnext/projects/report/project_profitability/project_profitability.py +++ b/erpnext/projects/report/project_profitability/project_profitability.py @@ -4,10 +4,6 @@ from __future__ import unicode_literals import frappe from frappe import _ -from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_employee_currency -from erpnext.accounts.utils import get_currency_precision -from frappe.utils import flt -from erpnext.setup.utils import get_exchange_rate def execute(filters=None): columns, data = [], [] From e63f3b2f36fe3cbfec6278b59902974f00f6b906 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 23 Apr 2021 20:33:42 +0530 Subject: [PATCH 12/15] fix: standard working hours message and link for HR Settings --- .../project_profitability/project_profitability.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/erpnext/projects/report/project_profitability/project_profitability.py b/erpnext/projects/report/project_profitability/project_profitability.py index 593ce1b22b..23e8f79b45 100644 --- a/erpnext/projects/report/project_profitability/project_profitability.py +++ b/erpnext/projects/report/project_profitability/project_profitability.py @@ -21,11 +21,13 @@ 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: - hr_settings = "HR Settings" - frappe.msgprint(_("The metrics for this report are calculated based on the Standard Working Hours. Please set Standard Working Hours in {0}.").format(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 [] - sql = """ + sql = """ SELECT * FROM @@ -74,7 +76,7 @@ def get_conditions(filters): if filters.get("project"): conditions.append("tabTimesheet.parent_project={0}".format(frappe.db.escape(filters.get("project")))) - + conditions = " and ".join(conditions) return conditions From 4a805b5622cde87a9436113e1d7f9d1dcf71172b Mon Sep 17 00:00:00 2001 From: Walstan Baptista <38958184+walstanb@users.noreply.github.com> Date: Sat, 24 Apr 2021 14:23:08 +0530 Subject: [PATCH 13/15] chore: frappe.whitelist for doc methods (#25465) --- erpnext/accounts/doctype/journal_entry/journal_entry.py | 1 + .../doctype/amazon_mws_settings/amazon_mws_settings.py | 4 +++- erpnext/healthcare/doctype/therapy_type/therapy_type.py | 1 + erpnext/hr/doctype/holiday_list/holiday_list.py | 1 + .../doctype/leave_control_panel/leave_control_panel.py | 1 + .../import_supplier_invoice/import_supplier_invoice.py | 3 ++- erpnext/selling/doctype/sms_center/sms_center.py | 2 ++ erpnext/setup/doctype/naming_series/naming_series.py | 1 + .../stock/doctype/purchase_receipt/purchase_receipt.py | 1 + erpnext/stock/doctype/stock_entry/stock_entry.py | 9 +++++++-- 10 files changed, 20 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index ff2c8c29b4..fefab82efc 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -592,6 +592,7 @@ class JournalEntry(AccountsController): self.validate_total_debit_and_credit() + @frappe.whitelist() def get_outstanding_invoices(self): self.set('accounts', []) total = 0 diff --git a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_settings.py b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_settings.py index 899b7ffe13..9c59840149 100644 --- a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_settings.py +++ b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_settings.py @@ -17,10 +17,12 @@ class AmazonMWSSettings(Document): else: self.enable_sync = 0 + @frappe.whitelist() def get_products_details(self): if self.enable_amazon == 1: frappe.enqueue('erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_methods.get_products_details') + @frappe.whitelist() def get_order_details(self): if self.enable_amazon == 1: after_date = dateutil.parser.parse(self.after_date).strftime("%Y-%m-%d") @@ -40,4 +42,4 @@ def setup_custom_fields(): fieldtype='Data', insert_after='title', read_only=1, print_hide=1)] } - create_custom_fields(custom_fields) \ No newline at end of file + create_custom_fields(custom_fields) diff --git a/erpnext/healthcare/doctype/therapy_type/therapy_type.py b/erpnext/healthcare/doctype/therapy_type/therapy_type.py index 6c825b8a58..3f6a36a968 100644 --- a/erpnext/healthcare/doctype/therapy_type/therapy_type.py +++ b/erpnext/healthcare/doctype/therapy_type/therapy_type.py @@ -50,6 +50,7 @@ class TherapyType(Document): self.db_set('change_in_item', 0) + @frappe.whitelist() def add_exercises(self): exercises = self.get_exercises_for_body_parts() last_idx = max([cint(d.idx) for d in self.get('exercises')] or [0,]) diff --git a/erpnext/hr/doctype/holiday_list/holiday_list.py b/erpnext/hr/doctype/holiday_list/holiday_list.py index 67630a0abe..8af8cea605 100644 --- a/erpnext/hr/doctype/holiday_list/holiday_list.py +++ b/erpnext/hr/doctype/holiday_list/holiday_list.py @@ -62,6 +62,7 @@ class HolidayList(Document): return date_list + @frappe.whitelist() def clear_table(self): self.set('holidays', []) diff --git a/erpnext/hr/doctype/leave_control_panel/leave_control_panel.py b/erpnext/hr/doctype/leave_control_panel/leave_control_panel.py index 57e61b5e08..74014020fc 100644 --- a/erpnext/hr/doctype/leave_control_panel/leave_control_panel.py +++ b/erpnext/hr/doctype/leave_control_panel/leave_control_panel.py @@ -29,6 +29,7 @@ class LeaveControlPanel(Document): frappe.throw(_("{0} is required").format(self.meta.get_label(f))) self.validate_from_to_dates('from_date', 'to_date') + @frappe.whitelist() def allocate_leave(self): self.validate_values() leave_allocated_for = [] diff --git a/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.py b/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.py index 31a7545a0d..cc6b907bc1 100644 --- a/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.py +++ b/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.py @@ -124,6 +124,7 @@ class ImportSupplierInvoice(Document): if disc_line.find("Percentuale"): invoices_args["total_discount"] += flt((flt(disc_line.Percentuale.text) / 100) * (rate * qty)) + @frappe.whitelist() def process_file_data(self): self.status = "Processing File Data" self.save() @@ -400,4 +401,4 @@ def get_full_path(file_name): elif not self.file_url: frappe.throw(_("There is some problem with the file url: {0}").format(file_path)) - return file_path \ No newline at end of file + return file_path diff --git a/erpnext/selling/doctype/sms_center/sms_center.py b/erpnext/selling/doctype/sms_center/sms_center.py index bb6ba1ffce..d142d16248 100644 --- a/erpnext/selling/doctype/sms_center/sms_center.py +++ b/erpnext/selling/doctype/sms_center/sms_center.py @@ -12,6 +12,7 @@ from frappe.model.document import Document from frappe.core.doctype.sms_settings.sms_settings import send_sms class SMSCenter(Document): + @frappe.whitelist() def create_receiver_list(self): rec, where_clause = '', '' if self.send_to == 'All Customer Contact': @@ -73,6 +74,7 @@ class SMSCenter(Document): return receiver_nos + @frappe.whitelist() def send_sms(self): receiver_list = [] if not self.message: diff --git a/erpnext/setup/doctype/naming_series/naming_series.py b/erpnext/setup/doctype/naming_series/naming_series.py index c4f1de14e4..373b0a58c9 100644 --- a/erpnext/setup/doctype/naming_series/naming_series.py +++ b/erpnext/setup/doctype/naming_series/naming_series.py @@ -159,6 +159,7 @@ class NamingSeries(Document): if frappe.db.get_value('Series', series, 'name', order_by="name") == None: frappe.db.sql("insert into tabSeries (name, current) values (%s, 0)", (series)) + @frappe.whitelist() def update_series_start(self): if self.prefix: prefix = self.parse_naming_series() diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 5d7597b2db..d8d8310733 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -221,6 +221,7 @@ class PurchaseReceipt(BuyingController): self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation') self.delete_auto_created_batches() + @frappe.whitelist() def get_current_stock(self): for d in self.get('supplied_items'): if self.supplier_warehouse: diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index f8ac400a8e..48cfa51041 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -398,8 +398,12 @@ class StockEntry(StockController): and item_code = %s and ifnull(s_warehouse,'')='' """ % (", ".join(["%s" * len(other_ste)]), "%s"), args)[0][0] if fg_qty_already_entered and fg_qty_already_entered >= qty: - frappe.throw(_("Stock Entries already created for Work Order ") - + self.work_order + ":" + ", ".join(other_ste), DuplicateEntryForWorkOrderError) + frappe.throw( + _("Stock Entries already created for Work Order {0}: {1}").format( + self.work_order, ", ".join(other_ste) + ), + DuplicateEntryForWorkOrderError, + ) def set_actual_qty(self): allow_negative_stock = cint(frappe.db.get_value("Stock Settings", None, "allow_negative_stock")) @@ -435,6 +439,7 @@ class StockEntry(StockController): if transferred_serial_no: d.serial_no = transferred_serial_no + @frappe.whitelist() def get_stock_and_rate(self): """ Updates rate and availability of all the items. From 2a25c7505efbba95393a4a0b6ed6916510885227 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 25 Apr 2021 20:29:14 +0530 Subject: [PATCH 14/15] fix: report column widths --- .../project_profitability.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/erpnext/projects/report/project_profitability/project_profitability.py b/erpnext/projects/report/project_profitability/project_profitability.py index 23e8f79b45..5ad2d85232 100644 --- a/erpnext/projects/report/project_profitability/project_profitability.py +++ b/erpnext/projects/report/project_profitability/project_profitability.py @@ -133,14 +133,14 @@ def get_columns(): "label": _("Sales Invoice"), "fieldtype": "Link", "options": "Sales Invoice", - "width": 180 + "width": 120 }, { "fieldname": "timesheet", "label": _("Timesheet"), "fieldtype": "Link", "options": "Timesheet", - "width": 130 + "width": 120 }, { "fieldname": "project", @@ -174,37 +174,37 @@ def get_columns(): "fieldname": "utilization", "label": _("Utilization"), "fieldtype": "Percentage", - "width": 120 + "width": 100 }, { "fieldname": "fractional_cost", "label": _("Fractional Cost"), "fieldtype": "Int", - "width": 100 + "width": 120 }, { "fieldname": "total_billed_hours", "label": _("Total Billed Hours"), "fieldtype": "Int", - "width": 100 + "width": 150 }, { "fieldname": "start_date", "label": _("Start Date"), "fieldtype": "Date", - "width": 120 + "width": 100 }, { "fieldname": "end_date", "label": _("End Date"), "fieldtype": "Date", - "width": 120 + "width": 100 }, { "label": _("Currency"), "fieldname": "currency", "fieldtype": "Link", "options": "Currency", - "width": 100 + "width": 80 } ] \ No newline at end of file From b9d4719285baabdb031bdce649947fd5b49a73fc Mon Sep 17 00:00:00 2001 From: Shadrak Gurupnor <30501401+shadrak98@users.noreply.github.com> Date: Mon, 26 Apr 2021 11:14:07 +0530 Subject: [PATCH 15/15] fix: circular loop in project task (#25454) * fix: circular loop in project task --- erpnext/projects/doctype/task/task.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/projects/doctype/task/task.js b/erpnext/projects/doctype/task/task.js index 002ddb2f40..6a9d2d1424 100644 --- a/erpnext/projects/doctype/task/task.js +++ b/erpnext/projects/doctype/task/task.js @@ -32,7 +32,8 @@ frappe.ui.form.on("Task", { frm.set_query("parent_task", function () { let filters = { - "is_group": 1 + "is_group": 1, + "name": ["!=", frm.doc.name] }; if (frm.doc.project) filters["project"] = frm.doc.project; return {