Merge pull request #13363 from shreyashah115/timer

Timer in Timesheets!
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 0000000..6138176
--- /dev/null
+++ b/erpnext/docs/assets/img/project/timesheet/timesheet-after-complete.png
Binary files 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 0000000..46da2ee
--- /dev/null
+++ b/erpnext/docs/assets/img/project/timesheet/timesheet-timer-alert.png
Binary files 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 0000000..a060344
--- /dev/null
+++ b/erpnext/docs/assets/img/project/timesheet/timesheet-timer-in-progress.png
Binary files 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 0000000..a838614
--- /dev/null
+++ b/erpnext/docs/assets/img/project/timesheet/timesheet-timer.gif
Binary files differ
diff --git a/erpnext/docs/user/manual/en/projects/timesheet/index.txt b/erpnext/docs/user/manual/en/projects/timesheet/index.txt
index 47d5ea1..9cd8cee 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 0000000..9e3bac4
--- /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.
+
+<img class="screenshot" alt="Timer" src="{{docs_base_url}}/assets/img/project/timesheet/timesheet-timer.gif">
+
+#### 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.
+
+<img class="screenshot" alt="Timer in Progress" src="{{docs_base_url}}/assets/img/project/timesheet/timesheet-timer-in-progress.png">
+
+- 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.
+
+<img class="screenshot" alt="Timer Completed" src="{{docs_base_url}}/assets/img/project/timesheet/timesheet-after-complete.png">
+
+- 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.
+
+<img class="screenshot" alt="Timer Exceeded" src="{{docs_base_url}}/assets/img/project/timesheet/timesheet-timer-alert.png">
diff --git a/erpnext/projects/doctype/timesheet/timesheet.css b/erpnext/projects/doctype/timesheet/timesheet.css
new file mode 100644
index 0000000..3a38415
--- /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 99ee2a2..678c016 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 @@
 			}
 		}
 
+		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 @@
 				}
 			})
 		})
-
 		dialog.show();
 	},
 
@@ -114,7 +149,15 @@
 		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 d82e79d..b948238 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 0000000..925398d
--- /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 `
+			<div class="stopwatch">
+				<span class="hours">00</span>
+				<span class="colon">:</span>
+				<span class="minutes">00</span>
+				<span class="colon">:</span>
+				<span class="seconds">00</span>
+			</div>
+			<div class="playpause text-center">
+				<button class= "btn btn-primary btn-start"> ${ __("Start") } </button>
+				<button class= "btn btn-primary btn-complete"> ${ __("Complete") } </button>
+			</div>
+		`;
+	}
+	erpnext.timesheet.control_timer(frm, dialog, row, timestamp);
+	dialog.show();
+};
+
+erpnext.timesheet.control_timer = function(frm, dialog, row, timestamp=0) {
+	var $btn_start = $(".playpause .btn-start");
+	var $btn_complete = $(".playpause .btn-complete");
+	var interval = null;
+	var currentIncrement = timestamp;
+	var initialised = row ? true : false;
+	var clicked = false;
+
+	// If row with not completed status, initialize timer with the time elapsed on click of 'Start Timer'.
+	if (row) {
+		initialised = true;
+		$btn_start.hide();
+		$btn_complete.show();
+		initialiseTimer();
+	}
+	if (!initialised) {
+		$btn_complete.hide();
+	}
+	$btn_start.click(function(e) {
+		if (!initialised) {
+			// New activity if no activities found
+			var args = dialog.get_values();
+			if(!args) return;
+			if (!frm.doc.time_logs[0].activity_type) {
+				frm.doc.time_logs = [];
+			}
+			row = frappe.model.add_child(frm.doc, "Timesheet Detail", "time_logs");
+			row.activity_type = args.activity_type;
+			row.from_time = frappe.datetime.get_datetime_as_string();
+			row.project = args.project;
+			row.task = args.task;
+			row.expected_hours = args.expected_hours;
+			row.completed = 0;
+			let d = moment(row.from_time);
+			if(row.expected_hours) {
+				d.add(row.expected_hours, "hours");
+				row.to_time = d.format(moment.defaultDatetimeFormat);
+			}
+			frm.refresh_field("time_logs");
+			frm.save();
+		}
+
+		if (clicked) {
+			e.preventDefault();
+			return false;
+		}
+
+		if (!initialised) {
+			initialised = true;
+			$btn_start.hide();
+			$btn_complete.show();
+			initialiseTimer();
+		}
+	});
+
+	// Stop the timer and update the time logged by the timer on click of 'Complete' button
+	$btn_complete.click(function() {
+		var grid_row = cur_frm.fields_dict['time_logs'].grid.get_row(row.idx - 1);
+		var args = dialog.get_values();
+		grid_row.doc.completed = 1;
+		grid_row.doc.activity_type = args.activity_type;
+		grid_row.doc.project = args.project;
+		grid_row.doc.task = args.task;
+		grid_row.doc.expected_hours = args.expected_hours;
+		grid_row.doc.hours = currentIncrement / 3600;
+		grid_row.doc.to_time = frappe.datetime.now_datetime();
+		grid_row.refresh();
+		frm.save();
+		reset();
+		dialog.hide();
+	});
+	function initialiseTimer() {
+		interval = setInterval(function() {
+			var current = setCurrentIncrement();
+			updateStopwatch(current);
+		}, 1000);
+	}
+
+	function updateStopwatch(increment) {
+		var hours = Math.floor(increment / 3600);
+		var minutes = Math.floor((increment - (hours * 3600)) / 60);
+		var seconds = increment - (hours * 3600) - (minutes * 60);
+
+		// If modal is closed by clicking anywhere outside, reset the timer
+		if (!$('.modal-dialog').is(':visible')) {
+			reset();
+		}
+		if(hours > 99)
+			reset();
+		$(".hours").text(hours < 10 ? ("0" + hours.toString()) : hours.toString());
+		$(".minutes").text(minutes < 10 ? ("0" + minutes.toString()) : minutes.toString());
+		$(".seconds").text(seconds < 10 ? ("0" + seconds.toString()) : seconds.toString());
+	}
+
+	function setCurrentIncrement() {
+		currentIncrement += 1;
+		return currentIncrement;
+	}
+
+	function reset() {
+		currentIncrement = 0;
+		initialised = false;
+		clearInterval(interval);
+		$(".hours").text("00");
+		$(".minutes").text("00");
+		$(".seconds").text("00");
+		$btn_complete.hide();
+		$btn_start.show();
+	}
+};
\ No newline at end of file