Browse Source

refactor: timesheet

develop
Anupam 4 years ago
parent
commit
fd4743cc31
  1. 51
      erpnext/accounts/doctype/sales_invoice/sales_invoice.js
  2. 3
      erpnext/accounts/doctype/sales_invoice/sales_invoice.json
  3. 24
      erpnext/accounts/doctype/sales_invoice/sales_invoice.py
  4. 233
      erpnext/accounts/doctype/sales_invoice_timesheet/sales_invoice_timesheet.json
  5. 3
      erpnext/hooks.py
  6. 1
      erpnext/patches.txt
  7. 2
      erpnext/patches/v7_0/convert_timelog_to_timesheet.py
  8. 14
      erpnext/projects/doctype/timesheet/test_timesheet.py
  9. 87
      erpnext/projects/doctype/timesheet/timesheet.js
  10. 24
      erpnext/projects/doctype/timesheet/timesheet.json
  11. 67
      erpnext/projects/doctype/timesheet/timesheet.py
  12. 1216
      erpnext/projects/doctype/timesheet_detail/timesheet_detail.json
  13. 4
      erpnext/projects/report/billing_summary.py
  14. 6
      erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py
  15. 4
      erpnext/projects/report/employee_hours_utilization_based_on_timesheet/test_employee_util.py
  16. 2
      erpnext/projects/report/project_profitability/test_project_profitability.py
  17. 12
      erpnext/public/js/utils.js

51
erpnext/accounts/doctype/sales_invoice/sales_invoice.js

@ -685,14 +685,16 @@ frappe.ui.form.on('Sales Invoice', {
},
project: function(frm){
frm.call({
method: "add_timesheet_data",
doc: frm.doc,
callback: function(r, rt) {
refresh_field(['timesheets'])
}
})
frm.refresh();
if (!frm.doc.is_return) {
frm.call({
method: "add_timesheet_data",
doc: frm.doc,
callback: function(r, rt) {
refresh_field(['timesheets'])
}
})
frm.refresh();
}
},
onload: function(frm) {
@ -808,27 +810,45 @@ frappe.ui.form.on('Sales Invoice', {
},
refresh: function(frm) {
if (frm.doc.project) {
if (frm.doc.project && frm.doc.docstatus===0 && !frm.doc.is_return) {
frm.add_custom_button(__('Fetch Timesheet'), function() {
let d = new frappe.ui.Dialog({
title: __('Fetch Timesheet'),
fields: [
{
"label" : "From",
"label" : __("From"),
"fieldname": "from_time",
"fieldtype": "Date",
"reqd": 1,
},
{
"label" : __("Currency"),
"fieldname": "currency",
"fieldtype": "Link",
"options": "Currency",
"default": frm.doc.currency,
"reqd": 1,
"read_only": 1
},
{
fieldtype: 'Column Break',
fieldname: 'col_break_1',
},
{
"label" : "To",
"label" : __("To"),
"fieldname": "to_time",
"fieldtype": "Date",
"reqd": 1,
}
},
{
"label" : __("Project"),
"fieldname": "project",
"fieldtype": "Link",
"options": "Project",
"default": frm.doc.project,
"reqd": 1,
"read_only": 1
},
],
primary_action: function() {
let data = d.get_values();
@ -837,7 +857,8 @@ frappe.ui.form.on('Sales Invoice', {
args: {
from_time: data.from_time,
to_time: data.to_time,
project: frm.doc.project
project: data.project,
currency: data.currency
},
callback: function(r) {
if(!r.exc) {
@ -845,9 +866,11 @@ frappe.ui.form.on('Sales Invoice', {
frm.clear_table('timesheets')
r.message.forEach((d) => {
frm.add_child('timesheets',{
'activity_type': d.activity_type,
'description': d.description,
'time_sheet': d.parent,
'billing_hours': d.billing_hours,
'billing_amount': d.billing_amt,
'billing_amount': d.billing_amount,
'timesheet_detail': d.name
});
});

3
erpnext/accounts/doctype/sales_invoice/sales_invoice.json

@ -748,6 +748,7 @@
{
"collapsible": 1,
"collapsible_depends_on": "eval:doc.total_billing_amount > 0",
"depends_on": "eval: !doc.is_return",
"fieldname": "time_sheet_list",
"fieldtype": "Section Break",
"hide_days": 1,
@ -1969,7 +1970,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2021-04-15 23:57:58.766651",
"modified": "2021-05-13 17:53:26.185370",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

24
erpnext/accounts/doctype/sales_invoice/sales_invoice.py

@ -125,6 +125,8 @@ class SalesInvoice(SellingController):
self.validate_multiple_billing("Delivery Note", "dn_detail", "amount", "items")
if not self.is_return:
self.validate_serial_numbers()
else:
self.timesheets = []
self.update_packing_list()
self.set_billing_hours_and_amount()
self.update_timesheet_billing_for_project()
@ -337,7 +339,7 @@ class SalesInvoice(SellingController):
if "Healthcare" in active_domains:
manage_invoice_submit_cancel(self, "on_cancel")
self.unlink_sales_invoice_from_timesheets()
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
def update_status_updater_args(self):
@ -393,6 +395,18 @@ class SalesInvoice(SellingController):
if validate_against_credit_limit:
check_credit_limit(self.customer, self.company, bypass_credit_limit_check_at_sales_order)
def unlink_sales_invoice_from_timesheets(self):
for row in self.timesheets:
timesheet = frappe.get_doc('Timesheet', row.time_sheet)
for time_log in timesheet.time_logs:
if time_log.sales_invoice == self.name:
time_log.sales_invoice = None
timesheet.calculate_total_amounts()
timesheet.calculate_percentage_billed()
timesheet.flags.ignore_validate_update_after_submit = True
timesheet.set_status()
timesheet.db_update_all()
@frappe.whitelist()
def set_missing_values(self, for_validate=False):
pos = self.set_pos_fields(for_validate)
@ -427,7 +441,7 @@ class SalesInvoice(SellingController):
timesheet.calculate_percentage_billed()
timesheet.flags.ignore_validate_update_after_submit = True
timesheet.set_status()
timesheet.save()
timesheet.db_update_all()
def update_time_sheet_detail(self, timesheet, args, sales_invoice):
for data in timesheet.time_logs:
@ -741,8 +755,10 @@ class SalesInvoice(SellingController):
self.append('timesheets', {
'time_sheet': data.parent,
'billing_hours': data.billing_hours,
'billing_amount': data.billing_amt,
'timesheet_detail': data.name
'billing_amount': data.billing_amount,
'timesheet_detail': data.name,
'activity_type': data.activity_type,
'description': data.description
})
self.calculate_billing_amount_for_timesheet()

233
erpnext/accounts/doctype/sales_invoice_timesheet/sales_invoice_timesheet.json

@ -1,172 +1,77 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2016-06-14 19:21:34.321662",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"actions": [],
"creation": "2016-06-14 19:21:34.321662",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"activity_type",
"description",
"billing_hours",
"billing_amount",
"time_sheet",
"timesheet_detail"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "time_sheet",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 1,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Time Sheet",
"length": 0,
"no_copy": 0,
"options": "Timesheet",
"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
},
"fieldname": "time_sheet",
"fieldtype": "Link",
"in_global_search": 1,
"in_list_view": 1,
"label": "Time Sheet",
"options": "Timesheet",
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "billing_hours",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Billing Hours",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "billing_hours",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Billing Hours",
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "billing_amount",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Billing Amount",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "billing_amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Billing Amount",
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 1,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "timesheet_detail",
"fieldtype": "Data",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Timesheet Detail",
"length": 0,
"no_copy": 1,
"permlevel": 0,
"precision": "",
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"allow_on_submit": 1,
"fieldname": "timesheet_detail",
"fieldtype": "Data",
"hidden": 1,
"label": "Timesheet Detail",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "activity_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Activity Type",
"options": "Activity Type",
"read_only": 1
},
{
"fieldname": "description",
"fieldtype": "Small Text",
"in_list_view": 1,
"label": "Description",
"read_only": 1
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2019-02-18 18:50:44.770361",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Timesheet",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
],
"istable": 1,
"links": [],
"modified": "2021-05-13 16:52:32.995266",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Timesheet",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

3
erpnext/hooks.py

@ -304,7 +304,8 @@ doc_events = {
# if payment entry not in auto cancel exempted doctypes it will cancel payment entry.
auto_cancel_exempted_doctypes= [
"Payment Entry",
"Inpatient Medication Entry"
"Inpatient Medication Entry",
"Timesheet"
]
after_migrate = ["erpnext.setup.install.update_select_perm_after_install"]

1
erpnext/patches.txt

@ -778,3 +778,4 @@ erpnext.patches.v12_0.add_ewaybill_validity_field
erpnext.patches.v13_0.germany_make_custom_fields
erpnext.patches.v13_0.germany_fill_debtor_creditor_number
erpnext.patches.v13_0.set_pos_closing_as_failed
erpnext.patches.v13_0.rename_billable_to_is_billable_in_timesheet

2
erpnext/patches/v7_0/convert_timelog_to_timesheet.py

@ -51,7 +51,7 @@ def execute():
def get_timelog_data(data):
return {
'billable': data.billable,
'is_billable': data.billable,
'from_time': data.from_time,
'hours': data.hours,
'to_time': data.to_time,

14
erpnext/projects/doctype/timesheet/test_timesheet.py

@ -37,7 +37,7 @@ class TestTimesheet(unittest.TestCase):
emp = make_employee("test_employee_6@salary.com")
make_salary_structure_for_timesheet(emp)
timesheet = make_timesheet(emp, simulate=True, billable=1)
timesheet = make_timesheet(emp, simulate=True, is_billable=1)
self.assertEqual(timesheet.total_hours, 2)
self.assertEqual(timesheet.total_billable_hours, 2)
@ -49,7 +49,7 @@ class TestTimesheet(unittest.TestCase):
emp = make_employee("test_employee_6@salary.com")
make_salary_structure_for_timesheet(emp)
timesheet = make_timesheet(emp, simulate=True, billable=0)
timesheet = make_timesheet(emp, simulate=True, is_billable=0)
self.assertEqual(timesheet.total_hours, 2)
self.assertEqual(timesheet.total_billable_hours, 0)
@ -61,7 +61,7 @@ class TestTimesheet(unittest.TestCase):
emp = make_employee("test_employee_6@salary.com", company="_Test Company")
salary_structure = make_salary_structure_for_timesheet(emp)
timesheet = make_timesheet(emp, simulate = True, billable=1)
timesheet = make_timesheet(emp, simulate = True, is_billable=1)
salary_slip = make_salary_slip(timesheet.name)
salary_slip.submit()
@ -82,7 +82,7 @@ class TestTimesheet(unittest.TestCase):
def test_sales_invoice_from_timesheet(self):
emp = make_employee("test_employee_6@salary.com")
timesheet = make_timesheet(emp, simulate=True, billable=1)
timesheet = make_timesheet(emp, simulate=True, is_billable=1)
sales_invoice = make_sales_invoice(timesheet.name, '_Test Item', '_Test Customer')
sales_invoice.due_date = nowdate()
sales_invoice.submit()
@ -100,7 +100,7 @@ class TestTimesheet(unittest.TestCase):
emp = make_employee("test_employee_6@salary.com")
project = frappe.get_value("Project", {"project_name": "_Test Project"})
timesheet = make_timesheet(emp, simulate=True, billable=1, project=project, company='_Test Company')
timesheet = make_timesheet(emp, simulate=True, is_billable=1, project=project, company='_Test Company')
sales_invoice = create_sales_invoice(do_not_save=True)
sales_invoice.project = project
sales_invoice.submit()
@ -171,13 +171,13 @@ def make_salary_structure_for_timesheet(employee, company=None):
return salary_structure
def make_timesheet(employee, simulate=False, billable = 0, activity_type="_Test Activity Type", project=None, task=None, company=None):
def make_timesheet(employee, simulate=False, is_billable = 0, activity_type="_Test Activity Type", project=None, task=None, company=None):
update_activity_type(activity_type)
timesheet = frappe.new_doc("Timesheet")
timesheet.employee = employee
timesheet.company = company or '_Test Company'
timesheet_detail = timesheet.append('time_logs', {})
timesheet_detail.billable = billable
timesheet_detail.is_billable = is_billable
timesheet_detail.activity_type = activity_type
timesheet_detail.from_time = now_datetime()
timesheet_detail.hours = 2

87
erpnext/projects/doctype/timesheet/timesheet.js

@ -90,17 +90,50 @@ frappe.ui.form.on("Timesheet", {
}
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);
frm.fields_dict["time_logs"].grid.toggle_enable("is_billable", false);
}
frm.trigger('setup_filters');
},
customer: function(frm) {
frm.set_query('parent_project', function(doc) {
return {
filters: {
"customer": doc.customer
}
};
});
frm.set_query('project', 'time_logs', function(doc) {
return {
filters: {
"customer": doc.customer
}
};
});
frm.refresh();
},
make_invoice: function(frm) {
let fields = [{
"fieldtype": "Link",
"label": __("Item Code"),
"fieldname": "item_code",
"options": "Item"
}]
if (!frm.doc.customer) {
fields.push({
"fieldtype": "Link",
"label": __("Customer"),
"fieldname": "customer",
"options": "Customer",
"default": frm.doc.customer
});
}
let dialog = new frappe.ui.Dialog({
title: __("Select Item (optional)"),
fields: [
{"fieldtype": "Link", "label": __("Item Code"), "fieldname": "item_code", "options":"Item"},
{"fieldtype": "Link", "label": __("Customer"), "fieldname": "customer", "options":"Customer"}
]
title: __("Create Sales Invoice"),
fields: fields
});
dialog.set_primary_action(__('Create Sales Invoice'), () => {
@ -113,7 +146,8 @@ frappe.ui.form.on("Timesheet", {
args: {
"source_name": frm.doc.name,
"item_code": args.item_code,
"customer": args.customer
"customer": frm.doc.customer || args.customer,
"currency": frm.doc.currency
},
freeze: true,
callback: function(r) {
@ -136,8 +170,7 @@ frappe.ui.form.on("Timesheet", {
parent_project: function(frm) {
set_project_in_timelog(frm);
},
}
});
frappe.ui.form.on("Timesheet Detail", {
@ -196,7 +229,7 @@ frappe.ui.form.on("Timesheet Detail", {
calculate_billing_costing_amount(frm, cdt, cdn);
},
billable: function(frm, cdt, cdn) {
is_billable: function(frm, cdt, cdn) {
update_billing_hours(frm, cdt, cdn);
update_time_rates(frm, cdt, cdn);
calculate_billing_costing_amount(frm, cdt, cdn);
@ -239,9 +272,9 @@ var calculate_end_time = function(frm, cdt, cdn) {
}
};
var update_billing_hours = function(frm, cdt, cdn){
var child = locals[cdt][cdn];
if(!child.billable) {
var update_billing_hours = function(frm, cdt, cdn) {
let child = frappe.get_doc(cdt, cdn);
if (!child.is_billable) {
frappe.model.set_value(cdt, cdn, 'billing_hours', 0.0);
} else {
// bill all hours by default
@ -249,19 +282,19 @@ var update_billing_hours = function(frm, cdt, cdn){
}
};
var update_time_rates = function(frm, cdt, cdn){
var child = locals[cdt][cdn];
if(!child.billable){
var update_time_rates = function(frm, cdt, cdn) {
let child = frappe.get_doc(cdt, cdn);
if (!child.is_billable) {
frappe.model.set_value(cdt, cdn, 'billing_rate', 0.0);
}
};
var calculate_billing_costing_amount = function(frm, cdt, cdn){
var child = locals[cdt][cdn];
var billing_amount = 0.0;
var costing_amount = 0.0;
var calculate_billing_costing_amount = function(frm, cdt, cdn) {
let child = frappe.get_doc(cdt, cdn);
let billing_amount = 0.0;
let costing_amount = 0.0;
if(child.billing_hours && child.billable){
if (child.billing_hours && child.is_billable) {
billing_amount = (child.billing_hours * child.billing_rate);
}
costing_amount = flt(child.costing_rate * child.hours);
@ -271,18 +304,18 @@ var calculate_billing_costing_amount = function(frm, cdt, cdn){
};
var calculate_time_and_amount = function(frm) {
var tl = frm.doc.time_logs || [];
var total_working_hr = 0;
var total_billing_hr = 0;
var total_billable_amount = 0;
var total_costing_amount = 0;
let tl = frm.doc.time_logs || [];
let total_working_hr = 0;
let total_billing_hr = 0;
let total_billable_amount = 0;
let total_costing_amount = 0;
for(var i=0; i<tl.length; i++) {
if (tl[i].hours) {
total_working_hr += tl[i].hours;
total_billable_amount += tl[i].billing_amount;
total_costing_amount += tl[i].costing_amount;
if(tl[i].billable){
if (tl[i].is_billable) {
total_billing_hr += tl[i].billing_hours;
}
}

24
erpnext/projects/doctype/timesheet/timesheet.json

@ -11,6 +11,8 @@
"title",
"naming_series",
"company",
"customer",
"currency",
"sales_invoice",
"column_break_3",
"salary_slip",
@ -176,7 +178,6 @@
"default": "0",
"fieldname": "total_hours",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Total Working Hours",
"read_only": 1
},
@ -199,7 +200,6 @@
"allow_on_submit": 1,
"fieldname": "total_billed_hours",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Total Billed Hours",
"print_hide": 1,
"read_only": 1
@ -209,6 +209,7 @@
"fieldname": "total_costing_amount",
"fieldtype": "Currency",
"label": "Total Costing Amount",
"options": "currency",
"print_hide": 1,
"read_only": 1
},
@ -222,6 +223,7 @@
"fieldname": "total_billable_amount",
"fieldtype": "Currency",
"label": "Total Billable Amount",
"options": "currency",
"read_only": 1
},
{
@ -229,6 +231,7 @@
"fieldname": "total_billed_amount",
"fieldtype": "Currency",
"label": "Total Billed Amount",
"options": "currency",
"print_hide": 1,
"read_only": 1
},
@ -236,6 +239,7 @@
"allow_on_submit": 1,
"fieldname": "per_billed",
"fieldtype": "Percent",
"in_list_view": 1,
"label": "% Amount Billed",
"no_copy": 1,
"print_hide": 1,
@ -265,13 +269,27 @@
"fieldtype": "Link",
"label": "Project",
"options": "Project"
},
{
"fieldname": "customer",
"fieldtype": "Link",
"label": "Customer",
"options": "Customer"
},
{
"fetch_from": "customer.default_currency",
"fetch_if_empty": 1,
"fieldname": "currency",
"fieldtype": "Link",
"label": "Currency",
"options": "Currency"
}
],
"icon": "fa fa-clock-o",
"idx": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-01-08 20:51:14.590080",
"modified": "2021-05-13 17:13:29.954960",
"modified_by": "Administrator",
"module": "Projects",
"name": "Timesheet",

67
erpnext/projects/doctype/timesheet/timesheet.py

@ -47,7 +47,7 @@ class Timesheet(Document):
self.total_hours += flt(d.hours)
self.total_costing_amount += flt(d.costing_amount)
if d.billable:
if d.is_billable:
self.total_billable_hours += flt(d.billing_hours)
self.total_billable_amount += flt(d.billing_amount)
self.total_billed_amount += flt(d.billing_amount) if d.sales_invoice else 0.0
@ -59,7 +59,7 @@ class Timesheet(Document):
self.per_billed = (self.total_billed_amount * 100) / self.total_billable_amount
def update_billing_hours(self, args):
if args.billable:
if args.is_billable:
if flt(args.billing_hours) == 0.0:
args.billing_hours = args.hours
else:
@ -133,16 +133,20 @@ class Timesheet(Document):
def validate_time_logs(self):
for data in self.get('time_logs'):
self.validate_overlap(data)
self.validate_task_project()
self.validate_task_project(data)
self.validate_project(data)
def validate_overlap(self, data):
settings = frappe.get_single('Projects Settings')
self.validate_overlap_for("user", data, self.user, settings.ignore_user_time_overlap)
self.validate_overlap_for("employee", data, self.employee, settings.ignore_employee_time_overlap)
def validate_task_project(self):
for log in self.time_logs:
log.project = log.project or frappe.db.get_value("Task", log.task, "project")
def validate_task_project(self, data):
data.project = data.project or frappe.db.get_value("Task", data.task, "project")
def validate_project(self, data):
if self.parent_project != data.project:
frappe.throw(_("Row {0}: Poject must be same as {1}.")).format(data.idx, self.parent_project)
def validate_overlap_for(self, fieldname, args, value, ignore_validation=False):
if not value or ignore_validation:
@ -189,7 +193,7 @@ class Timesheet(Document):
def update_cost(self):
for data in self.time_logs:
if data.activity_type or data.billable:
if data.activity_type or data.is_billable:
rate = get_activity_cost(self.employee, data.activity_type)
hours = data.billing_hours or 0
costing_hours = data.billing_hours or data.hours or 0
@ -200,20 +204,31 @@ class Timesheet(Document):
data.costing_amount = data.costing_rate * costing_hours
def update_time_rates(self, ts_detail):
if not ts_detail.billable:
if not ts_detail.is_billable:
ts_detail.billing_rate = 0.0
@frappe.whitelist()
def get_projectwise_timesheet_data(project, parent=None, from_time=None, to_time=None):
condition = ''
def get_projectwise_timesheet_data(project, parent=None, from_time=None, to_time=None, currency=None):
condition = field = join = ''
if parent:
condition = "AND parent = %(parent)s"
condition = "AND tsd.parent = %(parent)s"
if from_time and to_time:
condition += "AND CAST(from_time as DATE) BETWEEN %(from_time)s AND %(to_time)s"
return frappe.db.sql("""select name, parent, billing_hours, billing_amount as billing_amt
from `tabTimesheet Detail` where parenttype = 'Timesheet' and docstatus=1 and project = %(project)s {0} and billable = 1
and sales_invoice is null""".format(condition), {'project': project, 'parent': parent, 'from_time': from_time, 'to_time': to_time}, as_dict=1)
condition += "AND CAST(tsd.from_time as DATE) BETWEEN %(from_time)s AND %(to_time)s"
if currency:
field = ", ts.currency as currency"
join = " INNER JOIN `tabTimesheet` ts ON ts.name = tsd.parent "
condition += " AND ts.currency = %(currency)s"
return frappe.db.sql("""SELECT tsd.name as name,
tsd.parent as parent, tsd.billing_hours as billing_hours,
tsd.billing_amount as billing_amount, tsd.activity_type as activity_type,
tsd.description as description {0}
FROM `tabTimesheet Detail` tsd
{1} where tsd.parenttype = 'Timesheet'
and tsd.docstatus=1
and tsd.project = %(project)s {2}
and tsd.is_billable = 1
and tsd.sales_invoice is null""".format(field, join, condition), {'project': project, 'parent': parent, 'from_time': from_time, 'to_time': to_time, 'currency': currency}, as_dict=1)
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
@ -250,7 +265,7 @@ def get_timesheet_data(name, project):
}
@frappe.whitelist()
def make_sales_invoice(source_name, item_code=None, customer=None):
def make_sales_invoice(source_name, item_code=None, customer=None, currency=None):
target = frappe.new_doc("Sales Invoice")
timesheet = frappe.get_doc('Timesheet', source_name)
@ -268,6 +283,9 @@ def make_sales_invoice(source_name, item_code=None, customer=None):
if customer:
target.customer = customer
if currency:
target.currency = currency
if item_code:
target.append('items', {
'item_code': item_code,
@ -275,11 +293,16 @@ def make_sales_invoice(source_name, item_code=None, customer=None):
'rate': billing_rate
})
target.append('timesheets', {
'time_sheet': timesheet.name,
'billing_hours': hours,
'billing_amount': billing_amount
})
for time_log in timesheet.time_logs:
if time_log.is_billable:
target.append('timesheets', {
'time_sheet': timesheet.name,
'billing_hours': time_log.billing_hours,
'billing_amount': time_log.billing_amount,
'timesheet_detail': time_log.name,
'activity_type': time_log.activity_type,
'description': time_log.description
})
target.run_method("calculate_billing_amount_for_timesheet")
target.run_method("set_missing_values")

1216
erpnext/projects/doctype/timesheet_detail/timesheet_detail.json

File diff suppressed because it is too large

4
erpnext/projects/report/billing_summary.py

@ -126,7 +126,7 @@ def get_timesheet_details(filters, timesheet_list):
timesheet_details = frappe.get_all(
"Timesheet Detail",
filters = timesheet_details_filter,
fields=["from_time", "to_time", "hours", "billable", "billing_hours", "billing_rate", "parent"]
fields=["from_time", "to_time", "hours", "is_billable", "billing_hours", "billing_rate", "parent"]
)
timesheet_details_map = frappe._dict()
@ -139,7 +139,7 @@ def get_billable_and_total_duration(activity, start_time, end_time):
precision = frappe.get_precision("Timesheet Detail", "hours")
activity_duration = time_diff_in_hours(end_time, start_time)
billing_duration = 0.0
if activity.billable:
if activity.is_billable:
billing_duration = activity.billing_hours
if activity_duration != activity.billing_hours:
billing_duration = activity_duration * activity.billing_hours / activity.hours

6
erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py

@ -140,7 +140,7 @@ class EmployeeHoursReport:
additional_filters += f"AND tt.{field} = '{self.filters.get(field)}'"
self.filtered_time_logs = frappe.db.sql('''
SELECT tt.employee AS employee, ttd.hours AS hours, ttd.billable AS billable, ttd.project AS project
SELECT tt.employee AS employee, ttd.hours AS hours, ttd.is_billable AS is_billable, ttd.project AS project
FROM `tabTimesheet Detail` AS ttd
JOIN `tabTimesheet` AS tt
ON ttd.parent = tt.name
@ -153,14 +153,14 @@ class EmployeeHoursReport:
def generate_stats_by_employee(self):
self.stats_by_employee = frappe._dict()
for emp, hours, billable, project in self.filtered_time_logs:
for emp, hours, is_billable, project in self.filtered_time_logs:
self.stats_by_employee.setdefault(
emp, frappe._dict()
).setdefault('billed_hours', 0.0)
self.stats_by_employee[emp].setdefault('non_billed_hours', 0.0)
if billable:
if is_billable:
self.stats_by_employee[emp]['billed_hours'] += flt(hours, 2)
else:
self.stats_by_employee[emp]['non_billed_hours'] += flt(hours, 2)

4
erpnext/projects/report/employee_hours_utilization_based_on_timesheet/test_employee_util.py

@ -31,7 +31,7 @@ class TestEmployeeUtilization(unittest.TestCase):
timesheet1.append("time_logs", {
"activity_type": get_random("Activity Type"),
"hours": 5,
"billable": 1,
"is_billable": 1,
"from_time": '2021-04-01 13:30:00.000000',
"to_time": '2021-04-01 18:30:00.000000'
})
@ -46,7 +46,7 @@ class TestEmployeeUtilization(unittest.TestCase):
timesheet2.append("time_logs", {
"activity_type": get_random("Activity Type"),
"hours": 10,
"billable": 0,
"is_billable": 0,
"from_time": '2021-04-01 13:30:00.000000',
"to_time": '2021-04-01 23:30:00.000000',
"project": cls.test_project.name

2
erpnext/projects/report/project_profitability/test_project_profitability.py

@ -14,7 +14,7 @@ class TestProjectProfitability(unittest.TestCase):
if not frappe.db.exists('Salary Component', 'Timesheet Component'):
frappe.get_doc({'doctype': 'Salary Component', 'salary_component': 'Timesheet Component'}).insert()
make_salary_structure_for_timesheet(emp, company='_Test Company')
self.timesheet = make_timesheet(emp, simulate = True, billable=1)
self.timesheet = make_timesheet(emp, simulate = True, is_billable=1)
self.salary_slip = make_salary_slip(self.timesheet.name)
self.salary_slip.submit()
self.sales_invoice = make_sales_invoice(self.timesheet.name, '_Test Item', '_Test Customer')

12
erpnext/public/js/utils.js

@ -724,6 +724,18 @@ frappe.form.link_formatters['Employee'] = function(value, doc) {
}
}
frappe.form.link_formatters['Project'] = function(value, doc) {
if (doc && value && doc.project_name && doc.project_name !== value && doc.project === value) {
return value + ': ' + doc.project_name;
} else if (!value && doc.doctype && doc.project_name) {
// format blank value in child table
return doc.project;
} else {
// if value is blank in report view or project name and name are the same, return as is
return value;
}
}
// add description on posting time
$(document).on('app_ready', function() {
if(!frappe.datetime.is_timezone_same()) {

Loading…
Cancel
Save