diff --git a/erpnext/projects/doctype/timesheet_detail/timesheet_detail.py b/erpnext/projects/doctype/timesheet_detail/timesheet_detail.py index cd61da18496..9445a22294a 100644 --- a/erpnext/projects/doctype/timesheet_detail/timesheet_detail.py +++ b/erpnext/projects/doctype/timesheet_detail/timesheet_detail.py @@ -40,4 +40,94 @@ class TimesheetDetail(Document): to_time: DF.Datetime | None # end: auto-generated types +<<<<<<< HEAD pass +======= + def set_to_time(self): + """Set to_time based on from_time and hours.""" + if not (self.from_time and self.hours): + return + + _to_time = get_datetime(add_to_date(self.from_time, hours=self.hours, as_datetime=True)) + if abs(time_diff_in_seconds(_to_time, self.to_time)) >= 1: + self.to_time = _to_time + + def set_project(self): + """Set project based on task.""" + if self.task and not self.project: + self.project = frappe.db.get_value("Task", self.task, "project") + + def calculate_hours(self): + """Calculate hours based on from_time and to_time.""" + if self.to_time and self.from_time: + self.hours = time_diff_in_hours(self.to_time, self.from_time) + + def update_billing_hours(self): + """Update billing hours based on hours.""" + if not self.is_billable: + self.billing_hours = 0 + return + + if flt(self.billing_hours) == 0.0: + self.billing_hours = self.hours + + def update_cost(self, employee: str): + """Update costing and billing rates based on activity type.""" + from erpnext.projects.doctype.timesheet.timesheet import get_activity_cost + + if not self.is_billable and not self.activity_type: + return + + rate = get_activity_cost(employee, self.activity_type) + if not rate: + return + + self.billing_rate = ( + flt(rate.get("billing_rate")) if flt(self.billing_rate) == 0 else self.billing_rate + ) + self.costing_rate = ( + flt(rate.get("costing_rate")) if flt(self.costing_rate) == 0 else self.costing_rate + ) + + self.billing_amount = self.billing_rate * (self.billing_hours or 0) + self.costing_amount = self.costing_rate * (self.hours or 0) + exchange_rate = flt(frappe.get_value("Timesheet", self.parent, "exchange_rate")) or 1.0 + self.base_billing_rate = flt(self.billing_rate) * exchange_rate + self.base_costing_rate = flt(self.costing_rate) * exchange_rate + self.base_billing_amount = flt(self.billing_amount) * exchange_rate + self.base_costing_amount = flt(self.costing_amount) * exchange_rate + + def validate_dates(self): + """Validate that to_time is not before from_time.""" + if self.from_time and self.to_time and time_diff_in_hours(self.to_time, self.from_time) < 0: + frappe.throw(_("To Time cannot be before from date")) + + def validate_parent_project(self, parent_project: str): + """Validate that project is same as Timesheet's parent project.""" + if parent_project and parent_project != self.project: + frappe.throw( + _("Row {0}: Project must be same as the one set in the Timesheet: {1}.").format( + self.idx, parent_project + ) + ) + + def validate_task_project(self): + """Validate that the the task belongs to the project specified in the timesheet detail.""" + if self.task and self.project: + task_project = frappe.db.get_value("Task", self.task, "project") + if task_project and task_project != self.project: + frappe.throw( + _("Row {0}: Task {1} does not belong to Project {2}").format( + self.idx, frappe.bold(self.task), frappe.bold(self.project) + ) + ) + + def validate_billing_hours(self): + """Warn if billing hours are more than actual hours.""" + if flt(self.billing_hours) > flt(self.hours): + frappe.msgprint( + _("Warning - Row {0}: Billing Hours are more than Actual Hours").format(self.idx), + indicator="orange", + alert=True, + ) +>>>>>>> 88d8310a47 (fix: updating base amounts through python for timesheet)