[fix] #14038-Std hours at company level to calculate timesheet hours (#15819)
* [fix] #14038
* codacy fixes
* add end time calc method
* test case and rename function
* Update timesheet.py
diff --git a/erpnext/projects/doctype/timesheet/test_timesheet.py b/erpnext/projects/doctype/timesheet/test_timesheet.py
index 9f1c586..8c84c11 100644
--- a/erpnext/projects/doctype/timesheet/test_timesheet.py
+++ b/erpnext/projects/doctype/timesheet/test_timesheet.py
@@ -128,6 +128,50 @@
settings.ignore_employee_time_overlap = initial_setting
settings.save()
+ def test_timesheet_std_working_hours(self):
+ company = frappe.get_doc('Company', "_Test Company")
+ company.standard_working_hours = 8
+ company.save()
+
+ timesheet = frappe.new_doc("Timesheet")
+ timesheet.employee = "_T-Employee-00001"
+ timesheet.company = '_Test Company'
+ timesheet.append(
+ 'time_logs',
+ {
+ "activity_type": "_Test Activity Type",
+ "from_time": now_datetime(),
+ "to_time": now_datetime() + datetime.timedelta(days= 4)
+ }
+ )
+ timesheet.save()
+
+ ts = frappe.get_doc('Timesheet', timesheet.name)
+ self.assertEqual(ts.total_hours, 32)
+ ts.submit()
+ ts.cancel()
+
+ company = frappe.get_doc('Company', "_Test Company")
+ company.standard_working_hours = 0
+ company.save()
+
+ timesheet = frappe.new_doc("Timesheet")
+ timesheet.employee = "_T-Employee-00001"
+ timesheet.company = '_Test Company'
+ timesheet.append(
+ 'time_logs',
+ {
+ "activity_type": "_Test Activity Type",
+ "from_time": now_datetime(),
+ "to_time": now_datetime() + datetime.timedelta(days= 4)
+ }
+ )
+ timesheet.save()
+
+ ts = frappe.get_doc('Timesheet', timesheet.name)
+ self.assertEqual(ts.total_hours, 96)
+ ts.submit()
+ ts.cancel()
def make_salary_structure_for_timesheet(employee):
salary_structure_name = "Timesheet Salary Structure Test"
diff --git a/erpnext/projects/doctype/timesheet/timesheet.js b/erpnext/projects/doctype/timesheet/timesheet.js
index 5234df6..e890bef 100644
--- a/erpnext/projects/doctype/timesheet/timesheet.js
+++ b/erpnext/projects/doctype/timesheet/timesheet.js
@@ -90,6 +90,13 @@
}
},
+ company: function(frm) {
+ frappe.db.get_value('Company', { 'company_name' : frm.doc.company }, 'standard_working_hours')
+ .then(({ message }) => {
+ (frappe.working_hours = message.standard_working_hours || 0);
+ });
+ },
+
make_invoice: function(frm) {
let dialog = new frappe.ui.Dialog({
title: __("Select Item (optional)"),
@@ -142,11 +149,21 @@
to_time: function(frm, cdt, cdn) {
var child = locals[cdt][cdn];
+ var time_diff = (moment(child.to_time).diff(moment(child.from_time),"seconds")) / ( 60 * 60 * 24);
+ var std_working_hours = 0;
if(frm._setting_hours) return;
- frappe.model.set_value(cdt, cdn, "hours", moment(child.to_time).diff(moment(child.from_time),
- "seconds") / 3600);
+
+ var hours = moment(child.to_time).diff(moment(child.from_time), "seconds") / 3600;
+ std_working_hours = time_diff * frappe.working_hours;
+
+ if (std_working_hours < hours && std_working_hours > 0) {
+ frappe.model.set_value(cdt, cdn, "hours", std_working_hours);
+ } else {
+ frappe.model.set_value(cdt, cdn, "hours", hours);
+ }
},
+
time_logs_add: function(frm) {
var $trigger_again = $('.form-grid').find('.grid-row').find('.btn-open-row');
$trigger_again.on('click', () => {
@@ -209,17 +226,23 @@
let d = moment(child.from_time);
if(child.hours) {
- d.add(child.hours, "hours");
- frm._setting_hours = true;
- frappe.model.set_value(cdt, cdn, "to_time",
- d.format(frappe.defaultDatetimeFormat)).then(() => {
- frm._setting_hours = false;
- });
- }
+ var time_diff = (moment(child.to_time).diff(moment(child.from_time),"seconds")) / (60 * 60 * 24);
+ var std_working_hours = 0;
+ var hours = moment(child.to_time).diff(moment(child.from_time), "seconds") / 3600;
+ std_working_hours = time_diff * frappe.working_hours;
- if((frm.doc.__islocal || frm.doc.__onload.maintain_bill_work_hours_same) && child.hours){
- frappe.model.set_value(cdt, cdn, "billing_hours", child.hours);
+ if (std_working_hours < hours && std_working_hours > 0) {
+ frappe.model.set_value(cdt, cdn, "hours", std_working_hours);
+ frappe.model.set_value(cdt, cdn, "to_time", d.add(hours, "hours").format(frappe.defaultDatetimeFormat));
+ } else {
+ d.add(child.hours, "hours");
+ frm._setting_hours = true;
+ frappe.model.set_value(cdt, cdn, "to_time",
+ d.format(frappe.defaultDatetimeFormat)).then(() => {
+ frm._setting_hours = false;
+ });
+ }
}
}
diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py
index f48c0c6..4b466d2 100644
--- a/erpnext/projects/doctype/timesheet/timesheet.py
+++ b/erpnext/projects/doctype/timesheet/timesheet.py
@@ -9,7 +9,7 @@
import json
from datetime import timedelta
from erpnext.controllers.queries import get_match_cond
-from frappe.utils import flt, time_diff_in_hours, get_datetime, getdate, cint
+from frappe.utils import flt, time_diff_in_hours, get_datetime, getdate, cint, date_diff, add_to_date
from frappe.model.document import Document
from erpnext.manufacturing.doctype.workstation.workstation import (check_if_within_operating_hours,
WorkstationHolidayError)
@@ -27,6 +27,7 @@
self.set_status()
self.validate_dates()
self.validate_time_logs()
+ self.calculate_std_hours()
self.update_cost()
self.calculate_total_amounts()
self.calculate_percentage_billed()
@@ -93,6 +94,17 @@
self.start_date = getdate(start_date)
self.end_date = getdate(end_date)
+ def calculate_std_hours(self):
+ std_working_hours = frappe.get_value("Company", self.company, 'standard_working_hours')
+
+ for time in self.time_logs:
+ if time.from_time and time.to_time:
+ if flt(std_working_hours) > 0:
+ time.hours = flt(std_working_hours) * date_diff(time.to_time, time.from_time)
+ else:
+ if not time.hours:
+ time.hours = time_diff_in_hours(time.to_time, time.from_time)
+
def before_cancel(self):
self.set_status()
diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json
index 9377cad..01f8956 100644
--- a/erpnext/setup/doctype/company/company.json
+++ b/erpnext/setup/doctype/company/company.json
@@ -1,5 +1,6 @@
{
"allow_copy": 0,
+ "allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 1,
"allow_rename": 1,
@@ -730,6 +731,38 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
+ "fieldname": "standard_working_hours",
+ "fieldtype": "Float",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Standard Working Hours",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
"fieldname": "default_terms",
"fieldtype": "Link",
"hidden": 0,
@@ -837,7 +870,7 @@
"label": "Create Chart Of Accounts Based On",
"length": 0,
"no_copy": 0,
- "options": "\nStandard Template\nExisting Company",
+ "options": "\nStandard Template\nExisting Company",
"permlevel": 0,
"precision": "",
"print_hide": 0,
@@ -871,7 +904,7 @@
"label": "Chart Of Accounts Template",
"length": 0,
"no_copy": 1,
- "options": "",
+ "options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
@@ -1158,39 +1191,39 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
- "fieldname": "round_off_cost_center",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Round Off Cost Center",
- "length": 0,
- "no_copy": 0,
- "options": "Cost Center",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
+ "fieldname": "round_off_cost_center",
+ "fieldtype": "Link",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Round Off Cost Center",
+ "length": 0,
+ "no_copy": 0,
+ "options": "Cost Center",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
"unique": 0
- },
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
+ "allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
"fieldname": "write_off_account",
"fieldtype": "Link",
"hidden": 0,
@@ -1491,7 +1524,7 @@
"collapsible": 0,
"columns": 0,
"depends_on": "eval:!doc.__islocal",
- "fieldname": "default_deferred_expense_account",
+ "fieldname": "default_deferred_expense_account",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 1,
@@ -1500,7 +1533,7 @@
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
- "label": "Default Deferred Expense Account",
+ "label": "Default Deferred Expense Account",
"length": 0,
"no_copy": 1,
"options": "Account",
@@ -1525,7 +1558,7 @@
"collapsible": 0,
"columns": 0,
"depends_on": "eval:!doc.__islocal",
- "fieldname": "default_payroll_payable_account",
+ "fieldname": "default_payroll_payable_account",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 1,
@@ -1534,7 +1567,7 @@
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
- "label": "Default Payroll Payable Account",
+ "label": "Default Payroll Payable Account",
"length": 0,
"no_copy": 1,
"options": "Account",
@@ -1558,20 +1591,20 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
- "depends_on": "eval:!doc.__islocal",
- "fieldname": "default_expense_claim_payable_account",
+ "depends_on": "eval:!doc.__islocal",
+ "fieldname": "default_expense_claim_payable_account",
"fieldtype": "Link",
"hidden": 0,
- "ignore_user_permissions": 1,
+ "ignore_user_permissions": 1,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
- "label": "Default Expense Claim Payable Account",
+ "label": "Default Expense Claim Payable Account",
"length": 0,
- "no_copy": 1,
- "options": "Account",
+ "no_copy": 1,
+ "options": "Account",
"permlevel": 0,
"precision": "",
"print_hide": 0,
@@ -2870,8 +2903,8 @@
"istable": 0,
"max_attachments": 0,
"menu_index": 0,
- "modified": "2018-09-13 10:00:47.915706",
- "modified_by": "Administrator",
+ "modified": "2018-10-24 12:57:46.776452",
+ "modified_by": "Administrator",
"module": "Setup",
"name": "Company",
"owner": "Administrator",