diff --git a/erpnext/docs/assets/img/project/timesheet/timesheet-after-complete.png b/erpnext/docs/assets/img/project/timesheet/timesheet-after-complete.png
new file mode 100644
index 0000000000..6138176ecf
Binary files /dev/null and b/erpnext/docs/assets/img/project/timesheet/timesheet-after-complete.png differ
diff --git a/erpnext/docs/assets/img/project/timesheet/timesheet-timer-alert.png b/erpnext/docs/assets/img/project/timesheet/timesheet-timer-alert.png
new file mode 100644
index 0000000000..46da2eee17
Binary files /dev/null and b/erpnext/docs/assets/img/project/timesheet/timesheet-timer-alert.png differ
diff --git a/erpnext/docs/assets/img/project/timesheet/timesheet-timer-in-progress.png b/erpnext/docs/assets/img/project/timesheet/timesheet-timer-in-progress.png
new file mode 100644
index 0000000000..a060344f4d
Binary files /dev/null and b/erpnext/docs/assets/img/project/timesheet/timesheet-timer-in-progress.png differ
diff --git a/erpnext/docs/assets/img/project/timesheet/timesheet-timer.gif b/erpnext/docs/assets/img/project/timesheet/timesheet-timer.gif
new file mode 100644
index 0000000000..a838614d39
Binary files /dev/null and b/erpnext/docs/assets/img/project/timesheet/timesheet-timer.gif differ
diff --git a/erpnext/docs/user/manual/en/projects/timesheet/index.txt b/erpnext/docs/user/manual/en/projects/timesheet/index.txt
index 47d5ea1e39..9cd8cee924 100644
--- a/erpnext/docs/user/manual/en/projects/timesheet/index.txt
+++ b/erpnext/docs/user/manual/en/projects/timesheet/index.txt
@@ -1,3 +1,4 @@
salary-slip-from-timesheet
sales-invoice-from-timesheet
-timesheet-against-production-order
\ No newline at end of file
+timesheet-against-production-order
+timer-in-timesheet
\ No newline at end of file
diff --git a/erpnext/docs/user/manual/en/projects/timesheet/timer-in-timesheet.md b/erpnext/docs/user/manual/en/projects/timesheet/timer-in-timesheet.md
new file mode 100644
index 0000000000..9e3bac47b2
--- /dev/null
+++ b/erpnext/docs/user/manual/en/projects/timesheet/timer-in-timesheet.md
@@ -0,0 +1,26 @@
+
+# Timer in Timesheet
+
+Timesheets can be tracked against Project and Tasks along with a Timer.
+
+
+
+#### Steps to start a Timer:
+
+- On clicking, **Start Timer**, a dialog pops up and starts the timer for already present activity for which checkbox `completed` is unchecked.
+
+
+
+- If no activities are present, fill up the activity details, i.e. activity type, expected hours or project in the dialog itself, on clicking **Start**, a new row is added into the Timesheet Details child table and timer begins.
+
+- On clicking, **Complete**, the `hours` and `to_time` fields are updated for that particular activity.
+
+
+
+- At any point of time, if the dialog is closed without completing the activity, on opening the dialog again, the timer resumes by calculating how much time has elapsed since `from_time` of the activity.
+
+- If any activities are already present in the Timesheet with completed unchecked, clicking on **Resume Timer** fetches the activity and starts its timer.
+
+- If the time exceeds the `expected_hours`, an alert box appears.
+
+
diff --git a/erpnext/projects/doctype/timesheet/timesheet.css b/erpnext/projects/doctype/timesheet/timesheet.css
new file mode 100644
index 0000000000..3a38415e6c
--- /dev/null
+++ b/erpnext/projects/doctype/timesheet/timesheet.css
@@ -0,0 +1,23 @@
+.stopwatch {
+ text-align: center;
+ padding: 1em;
+ padding-bottom: 1em;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.stopwatch span {
+ display: inline-block;
+ position: relative;
+ font-size: 3em;
+ font-family: menlo;
+}
+
+.stopwatch .colon {
+ margin-top: -8px;
+}
+.playpause {
+ border-right: 1px dashed #fff;
+ border-bottom: 1px dashed #fff;
+}
\ No newline at end of file
diff --git a/erpnext/projects/doctype/timesheet/timesheet.js b/erpnext/projects/doctype/timesheet/timesheet.js
index 99ee2a2c5e..678c01639f 100644
--- a/erpnext/projects/doctype/timesheet/timesheet.js
+++ b/erpnext/projects/doctype/timesheet/timesheet.js
@@ -3,6 +3,7 @@
frappe.ui.form.on("Timesheet", {
setup: function(frm) {
+ frappe.require("/assets/erpnext/js/projects/timer.js");
frm.add_fetch('employee', 'employee_name', 'employee_name');
frm.fields_dict.employee.get_query = function() {
return {
@@ -50,6 +51,41 @@ frappe.ui.form.on("Timesheet", {
}
}
+ if (frm.doc.docstatus < 1) {
+
+ $.each(frm.doc.time_logs || [], function(i, row) {
+ if(row.from_time && !row.completed) {
+ if (row.to_time && frappe.datetime.now_datetime() > row.to_time) {
+ frappe.utils.play_sound("alert");
+ frappe.msgprint(__(`Timer exceeded the expected hours for activity ${row.activity_type} in row ${row.idx}.`));
+ }
+ }
+ frm.refresh_fields();
+ });
+
+ let button = 'Start Timer';
+ $.each(frm.doc.time_logs || [], function(i, row) {
+ if ((row.from_time <= frappe.datetime.now_datetime()) && !row.completed) {
+ button = 'Resume Timer';
+ }
+ })
+
+ frm.add_custom_button(__(button), function() {
+ var flag = true;
+ // Fetch the row for timer where activity is not completed and from_time is not <= now_time
+ $.each(frm.doc.time_logs || [], function(i, row) {
+ if (flag && row.from_time <= frappe.datetime.now_datetime() && !row.completed) {
+ let timestamp = moment(frappe.datetime.now_datetime()).diff(moment(row.from_time),"seconds");
+ erpnext.timesheet.timer(frm, row, timestamp);
+ flag = false;
+ }
+ })
+ // If no activities found to start a timer, create new
+ if (flag) {
+ erpnext.timesheet.timer(frm);
+ }
+ }).addClass("btn-primary");
+ }
if(frm.doc.per_billed > 0) {
frm.fields_dict["time_logs"].grid.toggle_enable("billing_hours", false);
frm.fields_dict["time_logs"].grid.toggle_enable("billable", false);
@@ -86,7 +122,6 @@ frappe.ui.form.on("Timesheet", {
}
})
})
-
dialog.show();
},
@@ -114,7 +149,15 @@ frappe.ui.form.on("Timesheet Detail", {
frappe.model.set_value(cdt, cdn, "hours", moment(child.to_time).diff(moment(child.from_time),
"seconds") / 3600);
},
-
+ time_logs_add: function(frm) {
+ var $trigger_again = $('.form-grid').find('.grid-row').find('.btn-open-row');
+ $trigger_again.on('click', () => {
+ $('.form-grid')
+ .find('[data-fieldname="timer"]')
+ .append(frappe.render_template("timesheet"));
+ frm.trigger("control_timer");
+ })
+ },
hours: function(frm, cdt, cdn) {
calculate_end_time(frm, cdt, cdn)
},
diff --git a/erpnext/projects/doctype/timesheet_detail/timesheet_detail.json b/erpnext/projects/doctype/timesheet_detail/timesheet_detail.json
index d82e79d3ed..b9482384a8 100644
--- a/erpnext/projects/doctype/timesheet_detail/timesheet_detail.json
+++ b/erpnext/projects/doctype/timesheet_detail/timesheet_detail.json
@@ -39,6 +39,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
+ "translatable": 0,
"unique": 0
},
{
@@ -69,6 +70,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
+ "translatable": 0,
"unique": 0
},
{
@@ -98,6 +100,38 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "expected_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": "Expected Hrs",
+ "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
},
{
@@ -127,6 +161,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
+ "translatable": 0,
"unique": 0
},
{
@@ -157,6 +192,40 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "default": "0",
+ "fieldname": "completed",
+ "fieldtype": "Check",
+ "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": "Completed",
+ "length": 0,
+ "no_copy": 0,
+ "options": "",
+ "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
},
{
@@ -186,6 +255,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
+ "translatable": 0,
"unique": 0
},
{
@@ -194,7 +264,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
- "depends_on": "eval:parent.work_order",
+ "depends_on": "eval:parent.production_order",
"fieldname": "completed_qty",
"fieldtype": "Float",
"hidden": 0,
@@ -217,6 +287,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
+ "translatable": 0,
"unique": 0
},
{
@@ -225,7 +296,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
- "depends_on": "eval:parent.work_order",
+ "depends_on": "eval:parent.production_order",
"fieldname": "workstation",
"fieldtype": "Link",
"hidden": 0,
@@ -249,6 +320,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
+ "translatable": 0,
"unique": 0
},
{
@@ -278,6 +350,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
+ "translatable": 0,
"unique": 0
},
{
@@ -286,7 +359,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
- "depends_on": "eval:parent.work_order",
+ "depends_on": "eval:parent.production_order",
"fieldname": "operation",
"fieldtype": "Link",
"hidden": 0,
@@ -310,6 +383,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
+ "translatable": 0,
"unique": 0
},
{
@@ -318,7 +392,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
- "depends_on": "eval:parent.work_order",
+ "depends_on": "eval:parent.production_order",
"fieldname": "operation_id",
"fieldtype": "Data",
"hidden": 1,
@@ -341,6 +415,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
+ "translatable": 0,
"unique": 0
},
{
@@ -370,6 +445,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
+ "translatable": 0,
"unique": 0
},
{
@@ -401,6 +477,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
+ "translatable": 0,
"unique": 0
},
{
@@ -430,6 +507,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
+ "translatable": 0,
"unique": 0
},
{
@@ -462,6 +540,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
+ "translatable": 0,
"unique": 0
},
{
@@ -491,6 +570,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
+ "translatable": 0,
"unique": 0
},
{
@@ -522,6 +602,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
+ "translatable": 0,
"unique": 0
},
{
@@ -551,6 +632,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
+ "translatable": 0,
"unique": 0
},
{
@@ -582,6 +664,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
+ "translatable": 0,
"unique": 0
},
{
@@ -612,6 +695,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
+ "translatable": 0,
"unique": 0
},
{
@@ -643,6 +727,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
+ "translatable": 0,
"unique": 0
},
{
@@ -676,6 +761,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
+ "translatable": 0,
"unique": 0
},
{
@@ -705,6 +791,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
+ "translatable": 0,
"unique": 0
},
{
@@ -735,6 +822,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
+ "translatable": 0,
"unique": 0
},
{
@@ -767,6 +855,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
+ "translatable": 0,
"unique": 0
},
{
@@ -797,6 +886,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
+ "translatable": 0,
"unique": 0
},
{
@@ -828,6 +918,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
+ "translatable": 0,
"unique": 0
}
],
@@ -841,7 +932,7 @@
"issingle": 0,
"istable": 1,
"max_attachments": 0,
- "modified": "2018-01-07 11:46:04.045313",
+ "modified": "2018-03-21 17:13:32.561550",
"modified_by": "Administrator",
"module": "Projects",
"name": "Timesheet Detail",
diff --git a/erpnext/public/js/projects/timer.js b/erpnext/public/js/projects/timer.js
new file mode 100644
index 0000000000..925398db6a
--- /dev/null
+++ b/erpnext/public/js/projects/timer.js
@@ -0,0 +1,155 @@
+frappe.provide("erpnext.timesheet");
+
+erpnext.timesheet.timer = function(frm, row, timestamp=0) {
+ let dialog = new frappe.ui.Dialog({
+ title: __("Timer"),
+ fields:
+ [
+ {"fieldtype": "Link", "label": __("Activity Type"), "fieldname": "activity_type",
+ "reqd": 1, "options": "Activity Type"},
+ {"fieldtype": "Link", "label": __("Project"), "fieldname": "project", "options": "Project"},
+ {"fieldtype": "Link", "label": __("Task"), "fieldname": "task", "options": "Task"},
+ {"fieldtype": "Float", "label": __("Expected Hrs"), "fieldname": "expected_hours"},
+ {"fieldtype": "Section Break"},
+ {"fieldtype": "HTML", "fieldname": "timer_html"}
+ ]
+ });
+
+ if (row) {
+ dialog.set_values({
+ 'activity_type': row.activity_type,
+ 'project': row.project,
+ 'task': row.task,
+ 'expected_hours': row.expected_hours
+ });
+ }
+ dialog.get_field("timer_html").$wrapper.append(get_timer_html());
+ function get_timer_html() {
+ return `
+