Merge pull request #3968 from rmehta/task-not-mandatory
[minor] task not mandatory for Time Log and Expense Claim #3904
This commit is contained in:
		
						commit
						43888546f6
					
				| @ -42,13 +42,13 @@ frappe.pages["Accounts Browser"].on_page_load  = function(wrapper){ | ||||
| 		wrapper.page.add_menu_item(__('New Company'), function() { newdoc('Company'); }, true); | ||||
| 	} | ||||
| 
 | ||||
| 	wrapper.page.set_secondary_action(__('Refresh'), function() { | ||||
| 	wrapper.page.add_menu_item(__('Refresh'), function() { | ||||
| 			wrapper.$company_select.change(); | ||||
| 		}); | ||||
| 
 | ||||
| 	wrapper.page.set_primary_action(__('New'), function() { | ||||
| 		erpnext.account_chart && erpnext.account_chart.make_new(); | ||||
| 	}); | ||||
| 	}, "octicon octicon-plus"); | ||||
| 
 | ||||
| 	// company-select
 | ||||
| 	wrapper.$company_select = wrapper.page.add_select("Company", []) | ||||
| @ -121,7 +121,8 @@ erpnext.AccountsChart = Class.extend({ | ||||
| 					label: __("Add Child"), | ||||
| 					click: function() { | ||||
| 						me.make_new() | ||||
| 					} | ||||
| 					}, | ||||
| 					btnClass: "hidden-xs" | ||||
| 				}, | ||||
| 				{ | ||||
| 					condition: function(node) { | ||||
| @ -137,8 +138,8 @@ erpnext.AccountsChart = Class.extend({ | ||||
| 							"company": me.company | ||||
| 						}; | ||||
| 						frappe.set_route("query-report", "General Ledger"); | ||||
| 					} | ||||
| 
 | ||||
| 					}, | ||||
| 					btnClass: "hidden-xs" | ||||
| 				}, | ||||
| 				{ | ||||
| 					condition: function(node) { return !node.root && me.can_write }, | ||||
| @ -147,7 +148,8 @@ erpnext.AccountsChart = Class.extend({ | ||||
| 						frappe.model.rename_doc(me.ctype, node.label, function(new_name) { | ||||
| 							node.reload(); | ||||
| 						}); | ||||
| 					} | ||||
| 					}, | ||||
| 					btnClass: "hidden-xs" | ||||
| 				}, | ||||
| 				{ | ||||
| 					condition: function(node) { return !node.root && me.can_delete }, | ||||
| @ -156,7 +158,8 @@ erpnext.AccountsChart = Class.extend({ | ||||
| 						frappe.model.delete_doc(me.ctype, node.label, function() { | ||||
| 							node.parent.remove(); | ||||
| 						}); | ||||
| 					} | ||||
| 					}, | ||||
| 					btnClass: "hidden-xs" | ||||
| 				} | ||||
| 			], | ||||
| 			onrender: function(node) { | ||||
|  | ||||
							
								
								
									
										2
									
								
								erpnext/change_log/current/activity_type.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								erpnext/change_log/current/activity_type.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | ||||
| - Set default costing rate and billing rate in **Activity Type** | ||||
| - Task not mandatory in **Time Log** and **Expense Claim** | ||||
| @ -456,12 +456,13 @@ | ||||
|    "unique": 0 | ||||
|   },  | ||||
|   { | ||||
|    "allow_on_submit": 0,  | ||||
|    "allow_on_submit": 1,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "default": "{employee_name}",  | ||||
|    "fieldname": "title",  | ||||
|    "fieldtype": "Data",  | ||||
|    "hidden": 0,  | ||||
|    "hidden": 1,  | ||||
|    "ignore_user_permissions": 0,  | ||||
|    "in_filter": 0,  | ||||
|    "in_list_view": 0,  | ||||
| @ -535,12 +536,32 @@ | ||||
|  "is_submittable": 1,  | ||||
|  "issingle": 0,  | ||||
|  "istable": 0,  | ||||
|  "modified": "2015-05-02 07:42:25.202983",  | ||||
|  "modified": "2015-09-01 07:11:25.759637",  | ||||
|  "modified_by": "Administrator",  | ||||
|  "module": "HR",  | ||||
|  "name": "Expense Claim",  | ||||
|  "owner": "harshada@webnotestech.com",  | ||||
|  "permissions": [ | ||||
|   { | ||||
|    "amend": 1,  | ||||
|    "apply_user_permissions": 0,  | ||||
|    "cancel": 1,  | ||||
|    "create": 1,  | ||||
|    "delete": 1,  | ||||
|    "email": 1,  | ||||
|    "export": 1,  | ||||
|    "if_owner": 0,  | ||||
|    "import": 0,  | ||||
|    "permlevel": 0,  | ||||
|    "print": 1,  | ||||
|    "read": 1,  | ||||
|    "report": 1,  | ||||
|    "role": "HR Manager",  | ||||
|    "set_user_permissions": 0,  | ||||
|    "share": 1,  | ||||
|    "submit": 1,  | ||||
|    "write": 1 | ||||
|   },  | ||||
|   { | ||||
|    "amend": 0,  | ||||
|    "apply_user_permissions": 1,  | ||||
|  | ||||
| @ -20,22 +20,27 @@ class ExpenseClaim(Document): | ||||
| 		validate_fiscal_year(self.posting_date, self.fiscal_year, _("Posting Date"), self) | ||||
| 		self.validate_sanctioned_amount() | ||||
| 		self.validate_expense_approver() | ||||
| 		self.validate_task() | ||||
| 		self.calculate_total_amount() | ||||
| 		set_employee_name(self) | ||||
| 		if self.task and not self.project: | ||||
| 			self.project = frappe.db.get_value("Task", self.task, "project") | ||||
| 
 | ||||
| 	def on_submit(self): | ||||
| 		if self.approval_status=="Draft": | ||||
| 			frappe.throw(_("""Approval Status must be 'Approved' or 'Rejected'""")) | ||||
| 		if self.task: | ||||
| 			self.update_task() | ||||
| 			 | ||||
| 		self.update_task_and_project() | ||||
| 
 | ||||
| 	def on_cancel(self): | ||||
| 		self.update_task_and_project() | ||||
| 
 | ||||
| 	def update_task_and_project(self): | ||||
| 		if self.task: | ||||
| 			self.update_task() | ||||
| 			 | ||||
| 		elif self.project: | ||||
| 			frappe.get_doc("Project", self.project).update_project() | ||||
| 
 | ||||
| 	def calculate_total_amount(self): | ||||
| 		self.total_claimed_amount = 0  | ||||
| 		self.total_claimed_amount = 0 | ||||
| 		self.total_sanctioned_amount = 0 | ||||
| 		for d in self.get('expenses'): | ||||
| 			self.total_claimed_amount += flt(d.claim_amount) | ||||
| @ -45,26 +50,22 @@ class ExpenseClaim(Document): | ||||
| 		if self.exp_approver and "Expense Approver" not in frappe.get_roles(self.exp_approver): | ||||
| 			frappe.throw(_("{0} ({1}) must have role 'Expense Approver'")\ | ||||
| 				.format(get_fullname(self.exp_approver), self.exp_approver), InvalidExpenseApproverError) | ||||
| 	 | ||||
| 
 | ||||
| 	def update_task(self): | ||||
| 		task = frappe.get_doc("Task", self.task) | ||||
| 		task.update_total_expense_claim() | ||||
| 		task.save() | ||||
| 
 | ||||
| 	def validate_task(self): | ||||
| 		if self.project and not self.task: | ||||
| 			frappe.throw(_("Task is mandatory if Expense Claim is against a Project")) | ||||
| 
 | ||||
| 	def validate_sanctioned_amount(self): | ||||
| 		for d in self.get('expenses'): | ||||
| 			if flt(d.sanctioned_amount) > flt(d.claim_amount): | ||||
| 				frappe.throw(_("Sanctioned Amount cannot be greater than Claim Amount in Row {0}.").format(d.idx)) | ||||
| 				 | ||||
| 
 | ||||
| 
 | ||||
| @frappe.whitelist() | ||||
| def get_expense_approver(doctype, txt, searchfield, start, page_len, filters): | ||||
| 	return frappe.db.sql(""" | ||||
| 		select u.name, concat(u.first_name, ' ', u.last_name)  | ||||
| 		select u.name, concat(u.first_name, ' ', u.last_name) | ||||
| 		from tabUser u, tabUserRole r | ||||
| 		where u.name = r.parent and r.role = 'Expense Approver' and u.name like %s | ||||
| 	""", ("%" + txt + "%")) | ||||
| 	""", ("%" + txt + "%")) | ||||
|  | ||||
| @ -11,44 +11,47 @@ class TestExpenseClaim(unittest.TestCase): | ||||
| 	def test_total_expense_claim_for_project(self): | ||||
| 		frappe.db.sql("""delete from `tabTask` where project = "_Test Project 1" """) | ||||
| 		frappe.db.sql("""delete from `tabProject` where name = "_Test Project 1" """) | ||||
| 		 | ||||
| 		frappe.db.sql("""delete from `tabExpense Claim`""") | ||||
| 		frappe.db.sql("""delete from `tabExpense Claim Detail`""") | ||||
| 
 | ||||
| 		frappe.get_doc({ | ||||
| 			"project_name": "_Test Project 1", | ||||
| 			"doctype": "Project", | ||||
| 			"tasks" : | ||||
| 				[{ "title": "_Test Project Task 1", "status": "Open" }] | ||||
| 		}).save() | ||||
| 		 | ||||
| 		task_name = frappe.db.get_value("Task",{"project": "_Test Project 1"}) | ||||
| 
 | ||||
| 		task_name = frappe.db.get_value("Task", {"project": "_Test Project 1"}) | ||||
| 
 | ||||
| 		expense_claim = frappe.get_doc({ | ||||
| 			 "doctype": "Expense Claim", | ||||
| 			 "employee": "_T-Employee-0001", | ||||
| 			 "approval_status": "Approved", | ||||
| 			 "project": "_Test Project 1", | ||||
| 			 "task": task_name, | ||||
| 			 "expenses":  | ||||
| 			 "expenses": | ||||
| 			 	[{ "expense_type": "Food", "claim_amount": 300, "sanctioned_amount": 200 }] | ||||
| 		}) | ||||
| 		expense_claim.submit() | ||||
| 		 | ||||
| 
 | ||||
| 		self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 200) | ||||
| 		self.assertEqual(frappe.db.get_value("Project", "_Test Project 1", "total_expense_claim"), 200) | ||||
| 		 | ||||
| 
 | ||||
| 		expense_claim2 = frappe.get_doc({ | ||||
| 			 "doctype": "Expense Claim", | ||||
| 			 "employee": "_T-Employee-0001", | ||||
| 			 "approval_status": "Approved", | ||||
| 			 "project": "_Test Project 1", | ||||
| 			 "task": task_name, | ||||
| 			 "expenses":  | ||||
| 			 "expenses": | ||||
| 			 	[{ "expense_type": "Food", "claim_amount": 600, "sanctioned_amount": 500 }] | ||||
| 		}) | ||||
| 		expense_claim2.submit() | ||||
| 		 | ||||
| 
 | ||||
| 		self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 700) | ||||
| 		self.assertEqual(frappe.db.get_value("Project", "_Test Project 1", "total_expense_claim"), 700) | ||||
| 		 | ||||
| 
 | ||||
| 		expense_claim2.cancel() | ||||
| 		 | ||||
| 
 | ||||
| 		self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 200) | ||||
| 		self.assertEqual(frappe.db.get_value("Project", "_Test Project 1", "total_expense_claim"), 200) | ||||
|  | ||||
| @ -201,3 +201,4 @@ execute:frappe.delete_doc_if_exists("Print Format", "Credit Note - Negative Invo | ||||
| 
 | ||||
| # V6.0 | ||||
| erpnext.patches.v6_0.set_default_title # 2015-09-03 | ||||
| erpnext.patches.v6_0.default_activity_rate | ||||
|  | ||||
							
								
								
									
										11
									
								
								erpnext/patches/v6_0/default_activity_rate.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								erpnext/patches/v6_0/default_activity_rate.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | ||||
| import frappe | ||||
| 
 | ||||
| def execute(): | ||||
| 	for cost in frappe.db.get_list("Activity Cost", filters = {"employee": ""}, | ||||
| 		fields = ("name", "activity_type", "costing_rate", "billing_rate")): | ||||
| 		activity_type = frappe.get_doc("Activity Type", cost.activity_type) | ||||
| 		activity_type.costing_rate = cost.costing_rate | ||||
| 		activity_type.billing_rate = cost.billing_rate | ||||
| 		activity_type.save() | ||||
| 
 | ||||
| 		frappe.delete_doc("Activity Cost", cost.name) | ||||
| @ -7,7 +7,7 @@ | ||||
|  "custom": 0,  | ||||
|  "docstatus": 0,  | ||||
|  "doctype": "DocType",  | ||||
|  "document_type": "Master",  | ||||
|  "document_type": "Setup",  | ||||
|  "fields": [ | ||||
|   { | ||||
|    "allow_on_submit": 0,  | ||||
| @ -219,7 +219,7 @@ | ||||
|  "is_submittable": 0,  | ||||
|  "issingle": 0,  | ||||
|  "istable": 0,  | ||||
|  "modified": "2015-06-16 03:12:25.644839",  | ||||
|  "modified": "2015-08-31 06:43:42.804365",  | ||||
|  "modified_by": "Administrator",  | ||||
|  "module": "Projects",  | ||||
|  "name": "Activity Cost",  | ||||
|  | ||||
							
								
								
									
										8
									
								
								erpnext/projects/doctype/activity_type/activity_type.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								erpnext/projects/doctype/activity_type/activity_type.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | ||||
| frappe.ui.form.on("Activity Type", { | ||||
| 	refresh: function(frm) { | ||||
| 		frm.add_custom_button("Activity Cost per Employee", function() { | ||||
| 			frappe.route_options = {"activity_type": frm.doc.name}; | ||||
| 			frappe.set_route("List", "Activity Cost"); | ||||
| 		}); | ||||
| 	} | ||||
| }); | ||||
| @ -7,7 +7,7 @@ | ||||
|  "custom": 0,  | ||||
|  "docstatus": 0,  | ||||
|  "doctype": "DocType",  | ||||
|  "document_type": "Master",  | ||||
|  "document_type": "Setup",  | ||||
|  "fields": [ | ||||
|   { | ||||
|    "allow_on_submit": 0,  | ||||
| @ -29,6 +29,71 @@ | ||||
|    "search_index": 0,  | ||||
|    "set_only_once": 0,  | ||||
|    "unique": 0 | ||||
|   },  | ||||
|   { | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "fieldname": "costing_rate",  | ||||
|    "fieldtype": "Currency",  | ||||
|    "hidden": 0,  | ||||
|    "ignore_user_permissions": 0,  | ||||
|    "in_filter": 0,  | ||||
|    "in_list_view": 0,  | ||||
|    "label": "Default Costing Rate",  | ||||
|    "no_copy": 0,  | ||||
|    "permlevel": 0,  | ||||
|    "precision": "",  | ||||
|    "print_hide": 0,  | ||||
|    "read_only": 0,  | ||||
|    "report_hide": 0,  | ||||
|    "reqd": 0,  | ||||
|    "search_index": 0,  | ||||
|    "set_only_once": 0,  | ||||
|    "unique": 0 | ||||
|   },  | ||||
|   { | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "fieldname": "column_break_3",  | ||||
|    "fieldtype": "Column Break",  | ||||
|    "hidden": 0,  | ||||
|    "ignore_user_permissions": 0,  | ||||
|    "in_filter": 0,  | ||||
|    "in_list_view": 0,  | ||||
|    "no_copy": 0,  | ||||
|    "permlevel": 0,  | ||||
|    "precision": "",  | ||||
|    "print_hide": 0,  | ||||
|    "read_only": 0,  | ||||
|    "report_hide": 0,  | ||||
|    "reqd": 0,  | ||||
|    "search_index": 0,  | ||||
|    "set_only_once": 0,  | ||||
|    "unique": 0 | ||||
|   },  | ||||
|   { | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "fieldname": "billing_rate",  | ||||
|    "fieldtype": "Currency",  | ||||
|    "hidden": 0,  | ||||
|    "ignore_user_permissions": 0,  | ||||
|    "in_filter": 0,  | ||||
|    "in_list_view": 0,  | ||||
|    "label": "Default Billing Rate",  | ||||
|    "no_copy": 0,  | ||||
|    "permlevel": 0,  | ||||
|    "precision": "",  | ||||
|    "print_hide": 0,  | ||||
|    "read_only": 0,  | ||||
|    "report_hide": 0,  | ||||
|    "reqd": 0,  | ||||
|    "search_index": 0,  | ||||
|    "set_only_once": 0,  | ||||
|    "unique": 0 | ||||
|   } | ||||
|  ],  | ||||
|  "hide_heading": 0,  | ||||
| @ -40,7 +105,7 @@ | ||||
|  "is_submittable": 0,  | ||||
|  "issingle": 0,  | ||||
|  "istable": 0,  | ||||
|  "modified": "2015-02-18 13:05:23.608066",  | ||||
|  "modified": "2015-08-31 06:39:04.527080",  | ||||
|  "modified_by": "Administrator",  | ||||
|  "module": "Projects",  | ||||
|  "name": "Activity Type",  | ||||
|  | ||||
| @ -46,8 +46,6 @@ class Project(Document): | ||||
| 		"""sync tasks and remove table""" | ||||
| 		if self.flags.dont_sync_tasks: return | ||||
| 
 | ||||
| 
 | ||||
| 		task_added_or_deleted = False | ||||
| 		task_names = [] | ||||
| 		for t in self.tasks: | ||||
| 			if t.task_id: | ||||
| @ -55,7 +53,6 @@ class Project(Document): | ||||
| 			else: | ||||
| 				task = frappe.new_doc("Task") | ||||
| 				task.project = self.name | ||||
| 				task_added_or_deleted = True | ||||
| 
 | ||||
| 			task.update({ | ||||
| 				"subject": t.title, | ||||
| @ -73,14 +70,15 @@ class Project(Document): | ||||
| 		# delete | ||||
| 		for t in frappe.get_all("Task", ["name"], {"project": self.name, "name": ("not in", task_names)}): | ||||
| 			frappe.delete_doc("Task", t.name) | ||||
| 			task_added_or_deleted = True | ||||
| 
 | ||||
| 		if task_added_or_deleted: | ||||
| 			self.update_project() | ||||
| 		self.update_percent_complete() | ||||
| 		self.update_costing() | ||||
| 
 | ||||
| 	def update_project(self): | ||||
| 		self.update_percent_complete() | ||||
| 		self.update_costing() | ||||
| 		self.flags.dont_sync_tasks = True | ||||
| 		self.save() | ||||
| 
 | ||||
| 	def update_percent_complete(self): | ||||
| 		total = frappe.db.sql("""select count(*) from tabTask where project=%s""", self.name)[0][0] | ||||
| @ -91,18 +89,31 @@ class Project(Document): | ||||
| 			self.percent_complete = flt(completed) / total * 100 | ||||
| 
 | ||||
| 	def update_costing(self): | ||||
| 		total_cost = frappe.db.sql("""select sum(ifnull(total_costing_amount, 0)) as costing_amount, | ||||
| 			sum(ifnull(total_billing_amount, 0)) as billing_amount, sum(ifnull(total_expense_claim, 0)) as expense_claim, | ||||
| 			min(act_start_date) as start_date, max(act_end_date) as end_date, sum(actual_time) as time | ||||
| 			from `tabTask` where project = %s""", self.name, as_dict=1)[0] | ||||
| 		from_time_log = frappe.db.sql("""select | ||||
| 			sum(ifnull(costing_amount, 0)) as costing_amount, | ||||
| 			sum(ifnull(billing_amount, 0)) as billing_amount, | ||||
| 			min(from_time) as start_date, | ||||
| 			max(to_time) as end_date, | ||||
| 			sum(hours) as time | ||||
| 			from `tabTime Log` where project = %s and docstatus = 1""", self.name, as_dict=1)[0] | ||||
| 
 | ||||
| 		from_expense_claim = frappe.db.sql("""select | ||||
| 			sum(ifnull(total_sanctioned_amount, 0)) as total_sanctioned_amount | ||||
| 			from `tabExpense Claim` where project = %s and approval_status='Approved' | ||||
| 			and docstatus = 1""", | ||||
| 			self.name, as_dict=1)[0] | ||||
| 
 | ||||
| 		self.actual_start_date = from_time_log.start_date | ||||
| 		self.actual_end_date = from_time_log.end_date | ||||
| 
 | ||||
| 		self.total_costing_amount = from_time_log.costing_amount | ||||
| 		self.total_billing_amount = from_time_log.billing_amount | ||||
| 		self.actual_time = from_time_log.time | ||||
| 
 | ||||
| 		self.total_expense_claim = from_expense_claim.total_sanctioned_amount | ||||
| 
 | ||||
| 		self.gross_margin = flt(self.total_billing_amount) - flt(self.total_costing_amount) | ||||
| 
 | ||||
| 		self.total_costing_amount = total_cost.costing_amount | ||||
| 		self.total_billing_amount = total_cost.billing_amount | ||||
| 		self.total_expense_claim = total_cost.expense_claim | ||||
| 		self.actual_start_date = total_cost.start_date | ||||
| 		self.actual_end_date = total_cost.end_date | ||||
| 		self.actual_time = total_cost.time | ||||
| 		self.gross_margin = flt(total_cost.billing_amount) - flt(total_cost.costing_amount) | ||||
| 		if self.total_billing_amount: | ||||
| 			self.per_gross_margin = (self.gross_margin / flt(self.total_billing_amount)) *100 | ||||
| 
 | ||||
|  | ||||
| @ -3,7 +3,7 @@ frappe.listview_settings['Project'] = { | ||||
| 	filters:[["status","=", "Open"]], | ||||
| 	get_indicator: function(doc) { | ||||
| 		if(doc.status=="Open" && doc.percent_complete) { | ||||
| 			return [__("{0}% Complete", [doc.percent_complete]), "orange", "percent_complete,>,0|status,=,Open"]; | ||||
| 			return [__("{0}% Complete", [cint(doc.percent_complete)]), "orange", "percent_complete,>,0|status,=,Open"]; | ||||
| 		} else { | ||||
| 			return [__(doc.status), frappe.utils.guess_colour(doc.status), "status,=," + doc.status]; | ||||
| 		} | ||||
|  | ||||
| @ -57,7 +57,7 @@ class Task(Document): | ||||
| 
 | ||||
| 	def update_time_and_costing(self): | ||||
| 		tl = frappe.db.sql("""select min(from_time) as start_date, max(to_time) as end_date, | ||||
| 			 sum(billing_amount) as total_billing_amount, sum(costing_amount) as total_costing_amount, | ||||
| 			sum(billing_amount) as total_billing_amount, sum(costing_amount) as total_costing_amount, | ||||
| 			sum(hours) as time from `tabTime Log` where task = %s and docstatus=1""" | ||||
| 			,self.name, as_dict=1)[0] | ||||
| 		if self.status == "Open": | ||||
| @ -70,10 +70,7 @@ class Task(Document): | ||||
| 
 | ||||
| 	def update_project(self): | ||||
| 		if self.project and not self.flags.from_project: | ||||
| 			project = frappe.get_doc("Project", self.project) | ||||
| 			project.flags.dont_sync_tasks = True | ||||
| 			project.update_project() | ||||
| 			project.save() | ||||
| 			frappe.get_doc("Project", self.project).update_project() | ||||
| 
 | ||||
| 	def check_recursion(self): | ||||
| 		if self.flags.ignore_recursion_check: return | ||||
|  | ||||
| @ -26,21 +26,21 @@ class TestTimeLog(unittest.TestCase): | ||||
| 		prod_order.set_production_order_operations() | ||||
| 		prod_order.save() | ||||
| 
 | ||||
| 		time_log = make_time_log_test_record(for_manufacturing= 1, production_order= prod_order.name, qty= 1,  | ||||
| 		time_log = make_time_log_test_record(for_manufacturing= 1, production_order= prod_order.name, qty= 1, | ||||
| 			employee= "_T-Employee-0003", do_not_save= True, simulate=1) | ||||
| 
 | ||||
| 		self.assertRaises(NotSubmittedError, time_log.save) | ||||
| 
 | ||||
| 	def test_time_log_on_holiday(self): | ||||
| 		prod_order = make_prod_order_test_record(item= "_Test FG Item 2", qty= 1,  | ||||
| 		prod_order = make_prod_order_test_record(item= "_Test FG Item 2", qty= 1, | ||||
| 			planned_start_date= now(), do_not_save= True) | ||||
| 		prod_order.set_production_order_operations() | ||||
| 		prod_order.save() | ||||
| 		prod_order.submit() | ||||
| 
 | ||||
| 		time_log = make_time_log_test_record(from_time= "2013-02-01 10:00:00", to_time= "2013-02-01 20:00:00", | ||||
| 			for_manufacturing= 1, production_order= prod_order.name, qty= 1,  | ||||
| 			operation= prod_order.operations[0].operation, operation_id= prod_order.operations[0].name,  | ||||
| 			for_manufacturing= 1, production_order= prod_order.name, qty= 1, | ||||
| 			operation= prod_order.operations[0].operation, operation_id= prod_order.operations[0].name, | ||||
| 			workstation= "_Test Workstation 1", do_not_save= True) | ||||
| 
 | ||||
| 		self.assertRaises(WorkstationHolidayError , time_log.save) | ||||
| @ -61,59 +61,81 @@ class TestTimeLog(unittest.TestCase): | ||||
| 			employee="_T-Employee-0006",do_not_save= True) | ||||
| 		self.assertRaises(NegativeHoursError, time_log.save) | ||||
| 
 | ||||
| 	def test_default_activity_cost(self): | ||||
| 		activity_type = frappe.get_doc("Activity Type", "_Test Activity Type") | ||||
| 		activity_type.billing_rate = 20 | ||||
| 		activity_type.costing_rate = 15 | ||||
| 		activity_type.save() | ||||
| 
 | ||||
| 		project_name = "_Test Project for Activity Type" | ||||
| 
 | ||||
| 		frappe.db.sql("delete from `tabTime Log` where project=%s or employee='_T-Employee-0002'", project_name) | ||||
| 		frappe.delete_doc("Project", project_name) | ||||
| 		project = frappe.get_doc({"doctype": "Project", "project_name": project_name}).insert() | ||||
| 
 | ||||
| 		make_time_log_test_record(employee="_T-Employee-0002", hours=2, | ||||
| 			activity_type = "_Test Activity Type", project = project.name) | ||||
| 
 | ||||
| 		project = frappe.get_doc("Project", project.name) | ||||
| 		self.assertTrue(project.total_costing_amount, 30) | ||||
| 		self.assertTrue(project.total_billing_amount, 40) | ||||
| 
 | ||||
| 	def test_total_activity_cost_for_project(self): | ||||
| 		frappe.db.sql("""delete from `tabTask` where project = "_Test Project 1" """) | ||||
| 		frappe.db.sql("""delete from `tabProject` where name = "_Test Project 1" """) | ||||
| 		 | ||||
| 		frappe.db.sql("""delete from `tabTime Log` where name = "_Test Project 1" """) | ||||
| 
 | ||||
| 		if not frappe.db.exists('Activity Cost', {"activity_type": "_Test Activity Type"}): | ||||
| 			activity_cost = frappe.get_doc({ | ||||
| 				"doctype": "Activity Cost", | ||||
| 				"employee": "", | ||||
| 				"employee": "_T-Employee-0002", | ||||
| 				"activity_type": "_Test Activity Type", | ||||
| 				"billing_rate": 100, | ||||
| 				"costing_rate": 50 | ||||
| 			}) | ||||
| 			activity_cost.insert() | ||||
| 		 | ||||
| 
 | ||||
| 		frappe.get_doc({ | ||||
| 			"project_name": "_Test Project 1", | ||||
| 			"doctype": "Project", | ||||
| 			"tasks" : | ||||
| 				[{ "title": "_Test Project Task 1", "status": "Open" }] | ||||
| 		}).save() | ||||
| 		 | ||||
| 
 | ||||
| 		task_name = frappe.db.get_value("Task",{"project": "_Test Project 1"}) | ||||
| 		 | ||||
| 		time_log = make_time_log_test_record(employee="_T-Employee-0002", hours=2, task= task_name) | ||||
| 
 | ||||
| 		time_log = make_time_log_test_record(employee="_T-Employee-0002", hours=2, | ||||
| 			task=task_name) | ||||
| 		self.assertEqual(time_log.costing_rate, 50) | ||||
| 		self.assertEqual(time_log.costing_amount, 100) | ||||
| 		self.assertEqual(time_log.billing_rate, 100) | ||||
| 		self.assertEqual(time_log.billing_amount, 200) | ||||
| 		 | ||||
| 
 | ||||
| 		self.assertEqual(frappe.db.get_value("Task", task_name, "total_billing_amount"), 200) | ||||
| 		self.assertEqual(frappe.db.get_value("Project", "_Test Project 1", "total_billing_amount"), 200) | ||||
| 		 | ||||
| 		time_log2 = make_time_log_test_record(employee="_T-Employee-0003", hours=2, task= task_name) | ||||
| 
 | ||||
| 		time_log2 = make_time_log_test_record(employee="_T-Employee-0002", | ||||
| 			hours=2, task= task_name, from_time = now_datetime() + datetime.timedelta(hours= 3)) | ||||
| 		self.assertEqual(frappe.db.get_value("Task", task_name, "total_billing_amount"), 400) | ||||
| 		self.assertEqual(frappe.db.get_value("Project", "_Test Project 1", "total_billing_amount"), 400) | ||||
| 		 | ||||
| 
 | ||||
| 		time_log2.cancel() | ||||
| 		 | ||||
| 
 | ||||
| 		self.assertEqual(frappe.db.get_value("Task", task_name, "total_billing_amount"), 200) | ||||
| 		self.assertEqual(frappe.db.get_value("Project", "_Test Project 1", "total_billing_amount"), 200) | ||||
| 		time_log.cancel() | ||||
| 		 | ||||
| 
 | ||||
| test_ignore = ["Time Log Batch", "Sales Invoice"] | ||||
| 
 | ||||
| def make_time_log_test_record(**args): | ||||
| 	args = frappe._dict(args) | ||||
| 
 | ||||
| 	time_log = frappe.new_doc("Time Log") | ||||
| 	 | ||||
| 
 | ||||
| 	time_log.from_time = args.from_time or now_datetime() | ||||
| 	time_log.hours = args.hours or 1 | ||||
| 	time_log.to_time = args.to_time or time_log.from_time + datetime.timedelta(hours= time_log.hours) | ||||
| 	 | ||||
| 
 | ||||
| 	time_log.project = args.project | ||||
| 	time_log.task = args.task | ||||
| 	time_log.for_manufacturing = args.for_manufacturing | ||||
| @ -126,7 +148,7 @@ def make_time_log_test_record(**args): | ||||
| 	time_log.billable = args.billable or 1 | ||||
| 	time_log.employee = args.employee | ||||
| 	time_log.user = args.user | ||||
| 	 | ||||
| 
 | ||||
| 	if not args.do_not_save: | ||||
| 		if args.simulate: | ||||
| 			while True: | ||||
| @ -141,4 +163,4 @@ def make_time_log_test_record(**args): | ||||
| 		if not args.do_not_submit: | ||||
| 			time_log.submit() | ||||
| 
 | ||||
| 	return time_log | ||||
| 	return time_log | ||||
|  | ||||
| @ -17,6 +17,8 @@ frappe.ui.form.on("Time Log", "refresh", function(frm) { | ||||
| 	if (frm.doc.__islocal && !frm.doc.user) { | ||||
| 		frm.set_value("user", user); | ||||
| 	} | ||||
| 
 | ||||
| 	frm.toggle_reqd("activity_type", !frm.doc.for_manufacturing); | ||||
| }); | ||||
| 
 | ||||
| 
 | ||||
|  | ||||
| @ -8,7 +8,7 @@ | ||||
|  "description": "Log of Activities performed by users against Tasks that can be used for tracking time, billing.",  | ||||
|  "docstatus": 0,  | ||||
|  "doctype": "DocType",  | ||||
|  "document_type": "Master",  | ||||
|  "document_type": "Setup",  | ||||
|  "fields": [ | ||||
|   { | ||||
|    "allow_on_submit": 0,  | ||||
| @ -181,6 +181,29 @@ | ||||
|    "set_only_once": 0,  | ||||
|    "unique": 0 | ||||
|   },  | ||||
|   { | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "depends_on": "eval:!doc.for_manufacturing",  | ||||
|    "fieldname": "activity_type",  | ||||
|    "fieldtype": "Link",  | ||||
|    "hidden": 0,  | ||||
|    "ignore_user_permissions": 0,  | ||||
|    "in_filter": 0,  | ||||
|    "in_list_view": 0,  | ||||
|    "label": "Activity Type",  | ||||
|    "no_copy": 0,  | ||||
|    "options": "Activity Type",  | ||||
|    "permlevel": 0,  | ||||
|    "print_hide": 0,  | ||||
|    "read_only": 0,  | ||||
|    "report_hide": 0,  | ||||
|    "reqd": 0,  | ||||
|    "search_index": 0,  | ||||
|    "set_only_once": 0,  | ||||
|    "unique": 0 | ||||
|   },  | ||||
|   { | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 0,  | ||||
| @ -227,29 +250,6 @@ | ||||
|    "set_only_once": 0,  | ||||
|    "unique": 0 | ||||
|   },  | ||||
|   { | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "depends_on": "eval:!doc.for_manufacturing",  | ||||
|    "fieldname": "activity_type",  | ||||
|    "fieldtype": "Link",  | ||||
|    "hidden": 0,  | ||||
|    "ignore_user_permissions": 0,  | ||||
|    "in_filter": 0,  | ||||
|    "in_list_view": 0,  | ||||
|    "label": "Activity Type",  | ||||
|    "no_copy": 0,  | ||||
|    "options": "Activity Type",  | ||||
|    "permlevel": 0,  | ||||
|    "print_hide": 0,  | ||||
|    "read_only": 0,  | ||||
|    "report_hide": 0,  | ||||
|    "reqd": 0,  | ||||
|    "search_index": 0,  | ||||
|    "set_only_once": 0,  | ||||
|    "unique": 0 | ||||
|   },  | ||||
|   { | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 0,  | ||||
| @ -618,7 +618,7 @@ | ||||
|    "ignore_user_permissions": 0,  | ||||
|    "in_filter": 0,  | ||||
|    "in_list_view": 0,  | ||||
|    "label": "Costing Rate (per hour)",  | ||||
|    "label": "Costing Rate based on Activity Type (per hour)",  | ||||
|    "no_copy": 0,  | ||||
|    "permlevel": 0,  | ||||
|    "precision": "",  | ||||
| @ -686,7 +686,7 @@ | ||||
|    "ignore_user_permissions": 0,  | ||||
|    "in_filter": 0,  | ||||
|    "in_list_view": 0,  | ||||
|    "label": "Billing Rate (per hour)",  | ||||
|    "label": "Billing Rate based on Activity Type (per hour)",  | ||||
|    "no_copy": 0,  | ||||
|    "permlevel": 0,  | ||||
|    "precision": "",  | ||||
| @ -812,7 +812,7 @@ | ||||
|    "unique": 0 | ||||
|   },  | ||||
|   { | ||||
|    "allow_on_submit": 0,  | ||||
|    "allow_on_submit": 1,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "fieldname": "title",  | ||||
| @ -843,7 +843,7 @@ | ||||
|  "is_submittable": 1,  | ||||
|  "issingle": 0,  | ||||
|  "istable": 0,  | ||||
|  "modified": "2015-04-14 09:07:28.468792",  | ||||
|  "modified": "2015-08-31 06:34:07.703583",  | ||||
|  "modified_by": "Administrator",  | ||||
|  "module": "Projects",  | ||||
|  "name": "Time Log",  | ||||
|  | ||||
| @ -26,16 +26,16 @@ class TimeLog(Document): | ||||
| 		self.check_workstation_timings() | ||||
| 		self.validate_production_order() | ||||
| 		self.validate_manufacturing() | ||||
| 		self.validate_task() | ||||
| 		self.set_project_if_missing() | ||||
| 		self.update_cost() | ||||
| 
 | ||||
| 	def on_submit(self): | ||||
| 		self.update_production_order() | ||||
| 		self.update_task() | ||||
| 		self.update_task_and_project() | ||||
| 
 | ||||
| 	def on_cancel(self): | ||||
| 		self.update_production_order() | ||||
| 		self.update_task() | ||||
| 		self.update_task_and_project() | ||||
| 
 | ||||
| 	def before_update_after_submit(self): | ||||
| 		self.set_status() | ||||
| @ -57,14 +57,17 @@ class TimeLog(Document): | ||||
| 			self.status="Billed" | ||||
| 
 | ||||
| 	def set_title(self): | ||||
| 		"""Set default title for the Time Log""" | ||||
| 		if self.title: | ||||
| 			return | ||||
| 
 | ||||
| 		from frappe.utils import get_fullname | ||||
| 		if self.production_order: | ||||
| 			self.title = _("{0} for {1}").format(self.operation, self.production_order) | ||||
| 		elif self.task: | ||||
| 			self.title = _("{0} for {1}").format(self.activity_type, self.task) | ||||
| 		elif self.project: | ||||
| 			self.title = _("{0} for {1}").format(self.activity_type, self.project) | ||||
| 		elif self.activity_type and (self.task or self.project): | ||||
| 			self.title = _("{0} for {1}").format(self.activity_type, self.task or self.project) | ||||
| 		else: | ||||
| 			self.title = self.activity_type | ||||
| 			self.title = self.task or self.project or get_fullname(frappe.session.user) | ||||
| 
 | ||||
| 	def validate_overlap(self): | ||||
| 		"""Checks if 'Time Log' entries overlap for a user, workstation. """ | ||||
| @ -111,6 +114,11 @@ class TimeLog(Document): | ||||
| 			from frappe.utils import time_diff_in_seconds | ||||
| 			self.hours = flt(time_diff_in_seconds(self.to_time, self.from_time)) / 3600 | ||||
| 
 | ||||
| 	def set_project_if_missing(self): | ||||
| 		"""Set project if task is set""" | ||||
| 		if self.task and not self.project: | ||||
| 			self.project = frappe.db.get_value("Task", self.task, "project") | ||||
| 
 | ||||
| 	def validate_time_log_for(self): | ||||
| 		if not self.for_manufacturing: | ||||
| 			for fld in ["production_order", "operation", "workstation", "completed_qty"]: | ||||
| @ -221,25 +229,26 @@ class TimeLog(Document): | ||||
| 	def update_cost(self): | ||||
| 		rate = get_activity_cost(self.employee, self.activity_type) | ||||
| 		if rate: | ||||
| 			self.costing_rate = rate.get('costing_rate') | ||||
| 			self.billing_rate = rate.get('billing_rate') | ||||
| 			self.costing_rate = flt(rate.get('costing_rate')) | ||||
| 			self.billing_rate = flt(rate.get('billing_rate')) | ||||
| 			self.costing_amount = self.costing_rate * self.hours | ||||
| 			if self.billable: | ||||
| 				self.billing_amount = self.billing_rate * self.hours | ||||
| 			else: | ||||
| 				self.billing_amount = 0 | ||||
| 
 | ||||
| 	def validate_task(self): | ||||
| 		# if a time log is being created against a project without production order | ||||
| 		if (self.project and not self.production_order) and not self.task: | ||||
| 			frappe.throw(_("Task is Mandatory if Time Log is against a project")) | ||||
| 	def update_task_and_project(self): | ||||
| 		"""Update costing rate in Task or Project if either is set""" | ||||
| 
 | ||||
| 	def update_task(self): | ||||
| 		if self.task and frappe.db.exists("Task", self.task): | ||||
| 		if self.task: | ||||
| 			task = frappe.get_doc("Task", self.task) | ||||
| 			task.update_time_and_costing() | ||||
| 			task.save() | ||||
| 
 | ||||
| 		elif self.project: | ||||
| 			frappe.get_doc("Project", self.project).update_project() | ||||
| 
 | ||||
| 
 | ||||
| @frappe.whitelist() | ||||
| def get_events(start, end, filters=None): | ||||
| 	"""Returns events for Gantt / Calendar view rendering. | ||||
| @ -270,9 +279,10 @@ def get_events(start, end, filters=None): | ||||
| 
 | ||||
| @frappe.whitelist() | ||||
| def get_activity_cost(employee=None, activity_type=None): | ||||
| 	rate = frappe.db.sql("""select costing_rate, billing_rate from `tabActivity Cost` where employee= %s | ||||
| 		and activity_type= %s""", (employee, activity_type), as_dict=1) | ||||
| 	rate = frappe.db.get_values("Activity Cost", {"employee": employee, | ||||
| 		"activity_type": activity_type}, ["costing_rate", "billing_rate"], as_dict=True) | ||||
| 	if not rate: | ||||
| 		rate = frappe.db.sql("""select costing_rate, billing_rate from `tabActivity Cost` where ifnull(employee, '')='' | ||||
| 			and activity_type= %s""", (activity_type), as_dict=1) | ||||
| 		rate = frappe.db.get_values("Activity Type", {"activity_type": activity_type}, | ||||
| 			["costing_rate", "billing_rate"], as_dict=True) | ||||
| 
 | ||||
| 	return rate[0] if rate else {} | ||||
|  | ||||
| @ -7,7 +7,7 @@ frappe.pages["Sales Browser"].on_page_load = function(wrapper){ | ||||
| 		single_column: true, | ||||
| 	}); | ||||
| 
 | ||||
| 	wrapper.page.set_secondary_action(__('Refresh'), function() { | ||||
| 	wrapper.page.add_menu_item(__('Refresh'), function() { | ||||
| 			wrapper.make_tree(); | ||||
| 		}); | ||||
| 
 | ||||
| @ -57,7 +57,7 @@ erpnext.SalesChart = Class.extend({ | ||||
| 
 | ||||
| 		me.page.set_primary_action(__("New"), function() { | ||||
| 			me.new_node(); | ||||
| 		}); | ||||
| 		}, "octicon octicon-plus"); | ||||
| 
 | ||||
| 		this.tree = new frappe.ui.Tree({ | ||||
| 			parent: $(parent), | ||||
| @ -80,7 +80,8 @@ erpnext.SalesChart = Class.extend({ | ||||
| 					condition: function(node) { return me.can_create && node.expandable; }, | ||||
| 					click: function(node) { | ||||
| 						me.new_node(); | ||||
| 					} | ||||
| 					}, | ||||
| 					btnClass: "hidden-xs" | ||||
| 				}, | ||||
| 				{ | ||||
| 					label:__("Rename"), | ||||
| @ -89,7 +90,8 @@ erpnext.SalesChart = Class.extend({ | ||||
| 						frappe.model.rename_doc(me.ctype, node.label, function(new_name) { | ||||
| 							node.$a.html(new_name); | ||||
| 						}); | ||||
| 					} | ||||
| 					}, | ||||
| 					btnClass: "hidden-xs" | ||||
| 				}, | ||||
| 				{ | ||||
| 					label:__("Delete"), | ||||
| @ -98,7 +100,8 @@ erpnext.SalesChart = Class.extend({ | ||||
| 						frappe.model.delete_doc(me.ctype, node.label, function() { | ||||
| 							node.parent.remove(); | ||||
| 						}); | ||||
| 					} | ||||
| 					}, | ||||
| 					btnClass: "hidden-xs" | ||||
| 				} | ||||
| 
 | ||||
| 			] | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user