diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 7c73ad6c90e..2b79e18358d 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/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 }); }); diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index c6c67b4ddc1..607e4c43d0b 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/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", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index bb74a02606f..a0087423906 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/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() diff --git a/erpnext/accounts/doctype/sales_invoice_timesheet/sales_invoice_timesheet.json b/erpnext/accounts/doctype/sales_invoice_timesheet/sales_invoice_timesheet.json index f7b9aef96cc..9321630829c 100644 --- a/erpnext/accounts/doctype/sales_invoice_timesheet/sales_invoice_timesheet.json +++ b/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 } \ No newline at end of file diff --git a/erpnext/hooks.py b/erpnext/hooks.py index bb6cd8bdc2b..ca8ca871bdc 100644 --- a/erpnext/hooks.py +++ b/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"] diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 82d223cada0..cec2dd5341a 100644 --- a/erpnext/patches.txt +++ b/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 diff --git a/erpnext/patches/v7_0/convert_timelog_to_timesheet.py b/erpnext/patches/v7_0/convert_timelog_to_timesheet.py index 3af6622d96e..8c60b5b71ec 100644 --- a/erpnext/patches/v7_0/convert_timelog_to_timesheet.py +++ b/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, diff --git a/erpnext/projects/doctype/timesheet/test_timesheet.py b/erpnext/projects/doctype/timesheet/test_timesheet.py index d21ac0f2f02..2b0c3abdd77 100644 --- a/erpnext/projects/doctype/timesheet/test_timesheet.py +++ b/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 diff --git a/erpnext/projects/doctype/timesheet/timesheet.js b/erpnext/projects/doctype/timesheet/timesheet.js index b123af5d188..5554ed9264c 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.js +++ b/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