From 32dc3bf0822517567acc00cf5a8924d9a1eb4ccc Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 6 Sep 2018 19:21:48 +0530 Subject: [PATCH] [Enhance] Job Card (#15244) * [Enhance] Added job card against the work order * removed work order from timesheet * Fixed codacy * Added patch to make job card from the timesheet * Timer in job card * Dates validation in job card * Added button to make job card from work order * Added sub-assembly operation in the work order --- erpnext/manufacturing/doctype/bom/bom.js | 17 + erpnext/manufacturing/doctype/bom/bom.json | 71 +- erpnext/manufacturing/doctype/bom/bom.py | 15 +- .../bom_explosion_item.json | 97 +- .../doctype/bom_item/bom_item.json | 35 +- .../doctype/job_card/__init__.py | 0 .../doctype/job_card/job_card.js | 113 +++ .../doctype/job_card/job_card.json | 912 ++++++++++++++++++ .../doctype/job_card/job_card.py | 211 ++++ .../doctype/job_card/job_card_dashboard.py | 12 + .../doctype/job_card/job_card_list.js | 13 + .../doctype/job_card/test_job_card.js | 23 + .../doctype/job_card/test_job_card.py | 9 + .../doctype/job_card_item/__init__.py | 0 .../doctype/job_card_item/job_card_item.json | 363 +++++++ .../doctype/job_card_item/job_card_item.py | 9 + .../manufacturing/doctype/routing/__init__.py | 0 .../manufacturing/doctype/routing/routing.js | 58 ++ .../doctype/routing/routing.json | 180 ++++ .../manufacturing/doctype/routing/routing.py | 9 + .../doctype/routing/test_routing.js | 23 + .../doctype/routing/test_routing.py | 9 + .../doctype/work_order/test_work_order.py | 47 - .../doctype/work_order/work_order.js | 131 ++- .../doctype/work_order/work_order.json | 35 +- .../doctype/work_order/work_order.py | 226 ++--- .../work_order/work_order_dashboard.py | 2 +- .../work_order_item/work_order_item.json | 33 + erpnext/patches.txt | 1 + erpnext/patches/v11_0/make_job_card.py | 18 + .../projects/doctype/timesheet/timesheet.json | 102 +- .../projects/doctype/timesheet/timesheet.py | 91 +- .../material_request/material_request.json | 35 +- .../material_request/material_request.py | 10 + .../stock/doctype/stock_entry/stock_entry.js | 2 +- .../doctype/stock_entry/stock_entry.json | 35 +- .../stock/doctype/stock_entry/stock_entry.py | 21 + 37 files changed, 2580 insertions(+), 388 deletions(-) create mode 100644 erpnext/manufacturing/doctype/job_card/__init__.py create mode 100644 erpnext/manufacturing/doctype/job_card/job_card.js create mode 100644 erpnext/manufacturing/doctype/job_card/job_card.json create mode 100644 erpnext/manufacturing/doctype/job_card/job_card.py create mode 100644 erpnext/manufacturing/doctype/job_card/job_card_dashboard.py create mode 100644 erpnext/manufacturing/doctype/job_card/job_card_list.js create mode 100644 erpnext/manufacturing/doctype/job_card/test_job_card.js create mode 100644 erpnext/manufacturing/doctype/job_card/test_job_card.py create mode 100644 erpnext/manufacturing/doctype/job_card_item/__init__.py create mode 100644 erpnext/manufacturing/doctype/job_card_item/job_card_item.json create mode 100644 erpnext/manufacturing/doctype/job_card_item/job_card_item.py create mode 100644 erpnext/manufacturing/doctype/routing/__init__.py create mode 100644 erpnext/manufacturing/doctype/routing/routing.js create mode 100644 erpnext/manufacturing/doctype/routing/routing.json create mode 100644 erpnext/manufacturing/doctype/routing/routing.py create mode 100644 erpnext/manufacturing/doctype/routing/test_routing.js create mode 100644 erpnext/manufacturing/doctype/routing/test_routing.py create mode 100644 erpnext/patches/v11_0/make_job_card.py diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index c706be9040..a01011a178 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -127,6 +127,23 @@ frappe.ui.form.on("BOM", { if(!r.exc) frm.refresh_fields(); } }); + }, + + routing: function(frm) { + if (frm.doc.routing) { + frappe.call({ + doc: frm.doc, + method: "get_routing", + freeze: true, + callback: function(r) { + if (!r.exc) { + frm.refresh_fields(); + erpnext.bom.calculate_op_cost(frm.doc); + erpnext.bom.calculate_total(frm.doc); + } + } + }); + } } }); diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json index 69a75c43a3..77fc4989e7 100644 --- a/erpnext/manufacturing/doctype/bom/bom.json +++ b/erpnext/manufacturing/doctype/bom/bom.json @@ -472,6 +472,39 @@ "translatable": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "with_operations", + "fieldname": "transfer_material_against_job_card", + "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": "Transfer Material Against Job Card", + "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 + }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, @@ -667,6 +700,39 @@ "translatable": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "routing", + "fieldtype": "Link", + "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": "Routing", + "length": 0, + "no_copy": 0, + "options": "Routing", + "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 + }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, @@ -1877,7 +1943,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-06-01 03:45:06.731308", + "modified": "2018-07-15 11:09:19.425998", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM", @@ -1930,5 +1996,6 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1, - "track_seen": 0 + "track_seen": 0, + "track_views": 0 } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 98aee05fad..5e9f46c201 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -89,6 +89,13 @@ class BOM(WebsiteGenerator): return item + def get_routing(self): + if self.routing: + for d in frappe.get_all("BOM Operation", fields = ["*"], + filters = {'parenttype': 'Routing', 'parent': self.routing}): + child = self.append('operations', d) + child.hour_rate = flt(d.hour_rate / self.conversion_rate, 2) + def validate_rm_item(self, item): if (item[0]['name'] in [it.item_code for it in self.items]) and item[0]['name'] == self.item: frappe.throw(_("BOM #{0}: Raw material cannot be same as main Item").format(self.name)) @@ -458,6 +465,7 @@ class BOM(WebsiteGenerator): self.add_to_cur_exploded_items(frappe._dict({ 'item_code' : d.item_code, 'item_name' : d.item_name, + 'operation' : d.operation, 'source_warehouse': d.source_warehouse, 'description' : d.description, 'image' : d.image, @@ -480,7 +488,7 @@ class BOM(WebsiteGenerator): """ Add all items from Flat BOM of child BOM""" # Did not use qty_consumed_per_unit in the query, as it leads to rounding loss child_fb_items = frappe.db.sql("""select bom_item.item_code, bom_item.item_name, - bom_item.description, bom_item.source_warehouse, + bom_item.description, bom_item.source_warehouse, bom_item.operation, bom_item.stock_uom, bom_item.stock_qty, bom_item.rate, bom_item.allow_transfer_for_manufacture, bom_item.stock_qty / ifnull(bom.quantity, 1) as qty_consumed_per_unit from `tabBOM Explosion Item` bom_item, tabBOM bom @@ -491,6 +499,7 @@ class BOM(WebsiteGenerator): 'item_code' : d['item_code'], 'item_name' : d['item_name'], 'source_warehouse' : d['source_warehouse'], + 'operation' : d['operation'], 'description' : d['description'], 'stock_uom' : d['stock_uom'], 'stock_qty' : d['qty_consumed_per_unit'] * stock_qty, @@ -571,7 +580,7 @@ def get_bom_items_as_dict(bom, company, qty=1, fetch_exploded=1, fetch_scrap_ite query = query.format(table="BOM Explosion Item", where_conditions="", is_stock_item=is_stock_item, - select_columns = """, bom_item.source_warehouse, bom_item.allow_transfer_for_manufacture, + select_columns = """, bom_item.source_warehouse, bom_item.operation, bom_item.allow_transfer_for_manufacture, (Select idx from `tabBOM Item` where item_code = bom_item.item_code and parent = %(parent)s ) as idx""") items = frappe.db.sql(query, { "parent": bom, "qty": qty, "bom": bom, "company": company }, as_dict=True) @@ -580,7 +589,7 @@ def get_bom_items_as_dict(bom, company, qty=1, fetch_exploded=1, fetch_scrap_ite items = frappe.db.sql(query, { "qty": qty, "bom": bom, "company": company }, as_dict=True) else: query = query.format(table="BOM Item", where_conditions="", is_stock_item=is_stock_item, - select_columns = ", bom_item.source_warehouse, bom_item.idx, bom_item.allow_transfer_for_manufacture") + select_columns = ", bom_item.source_warehouse, bom_item.idx, bom_item.operation, bom_item.allow_transfer_for_manufacture") items = frappe.db.sql(query, { "qty": qty, "bom": bom, "company": company }, as_dict=True) for item in items: diff --git a/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json b/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json index 8c7db8f8b1..ab3c5a1205 100644 --- a/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json +++ b/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json @@ -47,37 +47,6 @@ "translatable": 0, "unique": 0 }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "cb", - "fieldtype": "Column Break", - "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, - "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 - }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, @@ -110,6 +79,37 @@ "translatable": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "cb", + "fieldtype": "Column Break", + "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, + "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 + }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, @@ -143,6 +143,39 @@ "translatable": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "operation", + "fieldtype": "Link", + "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": "Operation", + "length": 0, + "no_copy": 0, + "options": "Operation", + "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 + }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, @@ -576,7 +609,7 @@ "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2018-07-12 16:29:55.464426", + "modified": "2018-08-27 16:32:35.152139", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Explosion Item", diff --git a/erpnext/manufacturing/doctype/bom_item/bom_item.json b/erpnext/manufacturing/doctype/bom_item/bom_item.json index ceee2c1555..b31f69208d 100644 --- a/erpnext/manufacturing/doctype/bom_item/bom_item.json +++ b/erpnext/manufacturing/doctype/bom_item/bom_item.json @@ -11,6 +11,39 @@ "document_type": "Setup", "editable_grid": 1, "fields": [ + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "operation", + "fieldtype": "Link", + "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": "Operation", + "length": 0, + "no_copy": 0, + "options": "Operation", + "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 + }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, @@ -1000,7 +1033,7 @@ "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2018-07-12 16:16:16.815165", + "modified": "2018-08-22 16:16:16.815165", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Item", diff --git a/erpnext/manufacturing/doctype/job_card/__init__.py b/erpnext/manufacturing/doctype/job_card/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js new file mode 100644 index 0000000000..6f5290e9ca --- /dev/null +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -0,0 +1,113 @@ +// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Job Card', { + refresh: function(frm) { + if (frm.doc.items && frm.doc.docstatus==1) { + if (frm.doc.for_quantity != frm.doc.transferred_qty) { + frm.add_custom_button(__("Material Request"), () => { + frm.trigger("make_material_request"); + }); + } + + if (frm.doc.for_quantity != frm.doc.transferred_qty) { + frm.add_custom_button(__("Material Transfer"), () => { + frm.trigger("make_stock_entry"); + }); + } + } + + if (frm.doc.docstatus == 0) { + if (!frm.doc.actual_start_date || !frm.doc.actual_end_date) { + frm.trigger("make_dashboard"); + } + + if (!frm.doc.actual_start_date) { + frm.add_custom_button(__("Start Job"), () => { + frm.set_value('actual_start_date', frappe.datetime.now_datetime()); + frm.save(); + }); + } else if (!frm.doc.actual_end_date) { + frm.add_custom_button(__("Complete Job"), () => { + frm.set_value('actual_end_date', frappe.datetime.now_datetime()); + frm.save(); + }); + } + } + }, + + make_dashboard: function(frm) { + if(frm.doc.__islocal) + return; + + frm.dashboard.refresh(); + const timer = ` +
+ 00 + : + 00 + : + 00 +
`; + + var section = frm.dashboard.add_section(timer); + + if (frm.doc.actual_start_date) { + let currentIncrement = moment(frappe.datetime.now_datetime()).diff(moment(frm.doc.actual_start_date),"seconds"); + initialiseTimer(); + + function initialiseTimer() { + const 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); + + $(section).find(".hours").text(hours < 10 ? ("0" + hours.toString()) : hours.toString()); + $(section).find(".minutes").text(minutes < 10 ? ("0" + minutes.toString()) : minutes.toString()); + $(section).find(".seconds").text(seconds < 10 ? ("0" + seconds.toString()) : seconds.toString()); + } + + function setCurrentIncrement() { + currentIncrement += 1; + return currentIncrement; + } + } + }, + + for_quantity: function(frm) { + frm.doc.items = []; + frm.call({ + method: "get_required_items", + doc: frm.doc, + callback: function() { + refresh_field("items"); + } + }) + }, + + make_material_request: function(frm) { + frappe.model.open_mapped_doc({ + method: "erpnext.manufacturing.doctype.job_card.job_card.make_material_request", + frm: frm, + run_link_triggers: true + }); + }, + + make_stock_entry: function(frm) { + frappe.model.open_mapped_doc({ + method: "erpnext.manufacturing.doctype.job_card.job_card.make_stock_entry", + frm: frm, + run_link_triggers: true + }); + }, + + timer: function(frm) { + return `` + } +}); \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json new file mode 100644 index 0000000000..443cad8666 --- /dev/null +++ b/erpnext/manufacturing/doctype/job_card/job_card.json @@ -0,0 +1,912 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "autoname": "PO-JOB.#####", + "beta": 0, + "creation": "2018-07-09 17:23:29.518745", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "work_order", + "fieldtype": "Link", + "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": "Work Order", + "length": 0, + "no_copy": 0, + "options": "Work Order", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 1, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "workstation", + "fieldtype": "Link", + "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": "Workstation", + "length": 0, + "no_copy": 0, + "options": "Workstation", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "operation", + "fieldtype": "Link", + "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": "Operation", + "length": 0, + "no_copy": 0, + "options": "Operation", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "wip_warehouse", + "fieldtype": "Link", + "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": "WIP Warehouse", + "length": 0, + "no_copy": 0, + "options": "Warehouse", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_4", + "fieldtype": "Column Break", + "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, + "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 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "Today", + "fieldname": "posting_date", + "fieldtype": "Date", + "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": "Posting Date", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "company", + "fieldtype": "Link", + "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": "Company", + "length": 0, + "no_copy": 0, + "options": "Company", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "for_quantity", + "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": "For Quantity", + "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": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "0", + "fieldname": "transferred_qty", + "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": "Transferred Qty", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "timing_detail", + "fieldtype": "Section Break", + "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": "Timing Detail", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "employee", + "fieldtype": "Link", + "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": "Employee", + "length": 0, + "no_copy": 0, + "options": "Employee", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "time_in_mins", + "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": "Time In Mins", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_13", + "fieldtype": "Column Break", + "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, + "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 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "actual_start_date", + "fieldtype": "Datetime", + "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": "Actual Start Date", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "actual_end_date", + "fieldtype": "Datetime", + "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": "Actual End Date", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "section_break_8", + "fieldtype": "Section Break", + "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": "Raw Materials", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "items", + "fieldtype": "Table", + "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": "Items", + "length": 0, + "no_copy": 0, + "options": "Job Card Item", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "columns": 0, + "fieldname": "more_information", + "fieldtype": "Section Break", + "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": "More Information", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "operation_id", + "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": "Operation ID", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "bom_no", + "fieldtype": "Link", + "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": "BOM No", + "length": 0, + "no_copy": 0, + "options": "BOM", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "project", + "fieldtype": "Link", + "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": "Project", + "length": 0, + "no_copy": 0, + "options": "Project", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_20", + "fieldtype": "Column Break", + "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, + "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 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "remarks", + "fieldtype": "Small Text", + "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": "Remarks", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "Open", + "fieldname": "status", + "fieldtype": "Select", + "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": "Status", + "length": 0, + "no_copy": 0, + "options": "Open\nWork In Progress\nCancelled\nCompleted", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "amended_from", + "fieldtype": "Link", + "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": "Amended From", + "length": 0, + "no_copy": 1, + "options": "Job Card", + "permlevel": 0, + "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 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 1, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2018-08-28 16:50:43.576151", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Job Card", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "Manufacturing User", + "set_user_permissions": 0, + "share": 1, + "submit": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "Manufacturing Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 1, + "write": 1 + } + ], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "operation", + "track_changes": 1, + "track_seen": 0, + "track_views": 0 +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py new file mode 100644 index 0000000000..bce5b9088a --- /dev/null +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -0,0 +1,211 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ +from frappe.utils import flt, time_diff_in_hours, get_datetime +from frappe.model.mapper import get_mapped_doc +from frappe.model.document import Document + +class JobCard(Document): + def validate(self): + self.status = 'Open' + self.validate_actual_dates() + self.set_time_in_mins() + + def validate_actual_dates(self): + if get_datetime(self.actual_start_date) > get_datetime(self.actual_end_date): + frappe.throw(_("Actual start date must be less than actual end date")) + + if not (self.employee and self.actual_start_date and self.actual_end_date): + return + + data = frappe.db.sql(""" select name from `tabJob Card` + where + ((%(actual_start_date)s > actual_start_date and %(actual_start_date)s < actual_end_date) or + (%(actual_end_date)s > actual_start_date and %(actual_end_date)s < actual_end_date) or + (%(actual_start_date)s <= actual_start_date and %(actual_end_date)s >= actual_end_date)) and + name != %(name)s and employee = %(employee)s and docstatus =1 + """, { + 'actual_start_date': self.actual_start_date, + 'actual_end_date': self.actual_end_date, + 'employee': self.employee, + 'name': self.name + }, as_dict=1) + + if data: + frappe.throw(_("Start date and end date is overlapping with the job card {1}") + .format(data[0].name, data[0].name)) + + def set_time_in_mins(self): + if self.actual_start_date and self.actual_end_date: + self.time_in_mins = time_diff_in_hours(self.actual_end_date, self.actual_start_date) * 60 + + def get_required_items(self): + if not self.get('work_order'): + return + + doc = frappe.get_doc('Work Order', self.get('work_order')) + if not doc.transfer_material_against_job_card and doc.skip_transfer: + return + + for d in doc.required_items: + if not d.operation: + frappe.throw(_("Row {0} : Operation is required against the raw material item {1}") + .format(d.idx, d.item_code)) + + if self.get('operation') == d.operation: + child = self.append('items', { + 'item_code': d.item_code, + 'source_warehouse': d.source_warehouse, + 'uom': frappe.db.get_value("Item", d.item_code, 'stock_uom'), + 'item_name': d.item_name, + 'description': d.description, + 'required_qty': (d.required_qty * flt(self.for_quantity)) / doc.qty + }) + + def on_submit(self): + self.validate_dates() + self.update_work_order() + self.set_transferred_qty() + + def validate_dates(self): + if not self.actual_start_date and not self.actual_end_date: + frappe.throw(_("Actual start date and actual end date is mandatory")) + + def on_cancel(self): + self.update_work_order() + self.set_transferred_qty() + + def update_work_order(self): + if not self.work_order: + return + + data = frappe.db.get_value("Job Card", {'docstatus': 1, 'operation_id': self.operation_id}, + ['sum(time_in_mins)', 'min(actual_start_date)', 'max(actual_end_date)', 'sum(for_quantity)']) + + if data: + time_in_mins, actual_start_date, actual_end_date, for_quantity = data + + wo = frappe.get_doc('Work Order', self.work_order) + + for data in wo.operations: + if data.name == self.operation_id: + data.completed_qty = for_quantity + data.actual_operation_time = time_in_mins + data.actual_start_time = actual_start_date + data.actual_end_time = actual_end_date + + wo.flags.ignore_validate_update_after_submit = True + wo.update_operation_status() + wo.calculate_operating_cost() + wo.set_actual_dates() + wo.save() + + def set_transferred_qty(self): + if not self.items: + self.transferred_qty = self.for_quantity if self.docstatus == 1 else 0 + + if self.items: + self.transferred_qty = frappe.db.get_value('Stock Entry', {'job_card': self.name, + 'work_order': self.work_order, 'docstatus': 1}, 'sum(fg_completed_qty)') + + self.db_set("transferred_qty", self.transferred_qty) + + qty = 0 + if self.work_order: + doc = frappe.get_doc('Work Order', self.work_order) + if doc.transfer_material_against_job_card and not doc.skip_transfer: + completed = True + for d in doc.operations: + if d.status != 'Completed': + completed = False + break + + if completed: + job_cards = frappe.get_all('Job Card', filters = {'work_order': self.work_order, + 'docstatus': ('!=', 2)}, fields = 'sum(transferred_qty) as qty', group_by='operation_id') + qty = min([d.qty for d in job_cards]) + + doc.db_set('material_transferred_for_manufacturing', qty) + + self.set_status() + + def set_status(self): + status = 'Cancelled' if self.docstatus == 2 else 'Work In Progress' + + if self.for_quantity == self.transferred_qty: + status = 'Completed' + + self.db_set('status', status) + +def update_job_card_reference(name, fieldname, value): + frappe.db.set_value('Job Card', name, fieldname, value) + +@frappe.whitelist() +def make_material_request(source_name, target_doc=None): + def update_item(obj, target, source_parent): + target.warehouse = source_parent.wip_warehouse + + def set_missing_values(source, target): + target.material_request_type = "Material Transfer" + + doclist = get_mapped_doc("Job Card", source_name, { + "Job Card": { + "doctype": "Material Request", + "validation": { + "docstatus": ["=", 1] + }, + "field_map": { + "name": "job_card", + }, + }, + "Job Card Item": { + "doctype": "Material Request Item", + "field_map": { + "required_qty": "qty", + "uom": "stock_uom" + }, + "postprocess": update_item, + } + }, target_doc, set_missing_values) + + return doclist + +@frappe.whitelist() +def make_stock_entry(source_name, target_doc=None): + def update_item(obj, target, source_parent): + target.t_warehouse = source_parent.wip_warehouse + + def set_missing_values(source, target): + target.purpose = "Material Transfer for Manufacture" + target.from_bom = 1 + target.fg_completed_qty = source.get('for_quantity', 0) - source.get('transferred_qty', 0) + target.calculate_rate_and_amount() + target.set_missing_values() + + doclist = get_mapped_doc("Job Card", source_name, { + "Job Card": { + "doctype": "Stock Entry", + "validation": { + "docstatus": ["=", 1] + }, + "field_map": { + "name": "job_card", + "for_quantity": "fg_completed_qty" + }, + }, + "Job Card Item": { + "doctype": "Stock Entry Detail", + "field_map": { + "source_warehouse": "s_warehouse", + "required_qty": "qty", + "uom": "stock_uom" + }, + "postprocess": update_item, + } + }, target_doc, set_missing_values) + + return doclist diff --git a/erpnext/manufacturing/doctype/job_card/job_card_dashboard.py b/erpnext/manufacturing/doctype/job_card/job_card_dashboard.py new file mode 100644 index 0000000000..a9811fcf95 --- /dev/null +++ b/erpnext/manufacturing/doctype/job_card/job_card_dashboard.py @@ -0,0 +1,12 @@ +from frappe import _ + +def get_data(): + return { + 'fieldname': 'job_card', + 'transactions': [ + { + 'label': _('Transactions'), + 'items': ['Material Request', 'Stock Entry'] + } + ] + } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/job_card/job_card_list.js b/erpnext/manufacturing/doctype/job_card/job_card_list.js new file mode 100644 index 0000000000..d40a9fa495 --- /dev/null +++ b/erpnext/manufacturing/doctype/job_card/job_card_list.js @@ -0,0 +1,13 @@ +frappe.listview_settings['Job Card'] = { + get_indicator: function(doc) { + if (doc.status === "Work In Progress") { + return [__("Work In Progress"), "orange", "status,=,Work In Progress"]; + } else if (doc.status === "Completed") { + return [__("Completed"), "green", "status,=,Completed"]; + } else if (doc.docstatus == 2) { + return [__("Cancelled"), "red", "status,=,Cancelled"]; + } else { + return [__("Open"), "red", "status,=,Open"]; + } + } +}; \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.js b/erpnext/manufacturing/doctype/job_card/test_job_card.js new file mode 100644 index 0000000000..5dc7805d22 --- /dev/null +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +// rename this file from _test_[name] to test_[name] to activate +// and remove above this line + +QUnit.test("test: Job Card", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially([ + // insert a new Job Card + () => frappe.tests.make('Job Card', [ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py new file mode 100644 index 0000000000..ca05fea0f6 --- /dev/null +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +import unittest + +class TestJobCard(unittest.TestCase): + pass diff --git a/erpnext/manufacturing/doctype/job_card_item/__init__.py b/erpnext/manufacturing/doctype/job_card_item/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/manufacturing/doctype/job_card_item/job_card_item.json b/erpnext/manufacturing/doctype/job_card_item/job_card_item.json new file mode 100644 index 0000000000..bc9fe108ca --- /dev/null +++ b/erpnext/manufacturing/doctype/job_card_item/job_card_item.json @@ -0,0 +1,363 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2018-07-09 17:20:44.737289", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "item_code", + "fieldtype": "Link", + "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": "Item Code", + "length": 0, + "no_copy": 0, + "options": "Item", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "source_warehouse", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 1, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Source Warehouse", + "length": 0, + "no_copy": 0, + "options": "Warehouse", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "uom", + "fieldtype": "Link", + "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": "UOM", + "length": 0, + "no_copy": 0, + "options": "UOM", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_3", + "fieldtype": "Column Break", + "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, + "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 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "item_name", + "fieldtype": "Data", + "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": "Item Name", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "description", + "fieldtype": "Text", + "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": "Description", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "qty_section", + "fieldtype": "Section Break", + "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": "Qty", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "required_qty", + "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": "Required Qty", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_9", + "fieldtype": "Column Break", + "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, + "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 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "allow_alternative_item", + "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": "Allow Alternative Item", + "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 + } + ], + "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": "2018-08-28 15:23:48.099459", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Job Card Item", + "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 +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/job_card_item/job_card_item.py b/erpnext/manufacturing/doctype/job_card_item/job_card_item.py new file mode 100644 index 0000000000..373cba293e --- /dev/null +++ b/erpnext/manufacturing/doctype/job_card_item/job_card_item.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +from frappe.model.document import Document + +class JobCardItem(Document): + pass diff --git a/erpnext/manufacturing/doctype/routing/__init__.py b/erpnext/manufacturing/doctype/routing/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/manufacturing/doctype/routing/routing.js b/erpnext/manufacturing/doctype/routing/routing.js new file mode 100644 index 0000000000..6cfd0bae5b --- /dev/null +++ b/erpnext/manufacturing/doctype/routing/routing.js @@ -0,0 +1,58 @@ +// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Routing', { + calculate_operating_cost: function(frm, child) { + const operating_cost = flt(flt(child.hour_rate) * flt(child.time_in_mins) / 60, 2); + frappe.model.set_value(child.doctype, child.name, "operating_cost", operating_cost); + } +}); + +frappe.ui.form.on('BOM Operation', { + operation: function(frm, cdt, cdn) { + const d = locals[cdt][cdn]; + + if(!d.operation) return; + + frappe.call({ + "method": "frappe.client.get", + args: { + doctype: "Operation", + name: d.operation + }, + callback: function (data) { + if (data.message.description) { + frappe.model.set_value(d.doctype, d.name, "description", data.message.description); + } + + if (data.message.workstation) { + frappe.model.set_value(d.doctype, d.name, "workstation", data.message.workstation); + } + + frm.events.calculate_operating_cost(frm, d); + } + }); + }, + + workstation: function(frm, cdt, cdn) { + const d = locals[cdt][cdn]; + + frappe.call({ + "method": "frappe.client.get", + args: { + doctype: "Workstation", + name: d.workstation + }, + callback: function (data) { + frappe.model.set_value(d.doctype, d.name, "base_hour_rate", data.message.hour_rate); + frappe.model.set_value(d.doctype, d.name, "hour_rate", data.message.hour_rate); + frm.events.calculate_operating_cost(frm, d); + } + }); + }, + + time_in_mins: function(frm, cdt, cdn) { + const d = locals[cdt][cdn]; + frm.events.calculate_operating_cost(frm, d); + } +}); \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/routing/routing.json b/erpnext/manufacturing/doctype/routing/routing.json new file mode 100644 index 0000000000..e864c0c528 --- /dev/null +++ b/erpnext/manufacturing/doctype/routing/routing.json @@ -0,0 +1,180 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "autoname": "field:routing_name", + "beta": 0, + "creation": "2018-07-15 11:03:24.191613", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "routing_name", + "fieldtype": "Data", + "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": "Routing Name", + "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": 1 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "eval:!doc.__islocal", + "fieldname": "disabled", + "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": "Disabled", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "operations", + "fieldtype": "Table", + "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": "BOM Operation", + "length": 0, + "no_copy": 0, + "options": "BOM Operation", + "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 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2018-07-15 11:42:41.424793", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Routing", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "Manufacturing Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + }, + { + "amend": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "Manufacturing User", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "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 +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/routing/routing.py b/erpnext/manufacturing/doctype/routing/routing.py new file mode 100644 index 0000000000..ecd0ba8be8 --- /dev/null +++ b/erpnext/manufacturing/doctype/routing/routing.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +from frappe.model.document import Document + +class Routing(Document): + pass diff --git a/erpnext/manufacturing/doctype/routing/test_routing.js b/erpnext/manufacturing/doctype/routing/test_routing.js new file mode 100644 index 0000000000..6cb65494af --- /dev/null +++ b/erpnext/manufacturing/doctype/routing/test_routing.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +// rename this file from _test_[name] to test_[name] to activate +// and remove above this line + +QUnit.test("test: Routing", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially([ + // insert a new Routing + () => frappe.tests.make('Routing', [ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); diff --git a/erpnext/manufacturing/doctype/routing/test_routing.py b/erpnext/manufacturing/doctype/routing/test_routing.py new file mode 100644 index 0000000000..53ad152732 --- /dev/null +++ b/erpnext/manufacturing/doctype/routing/test_routing.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +import unittest + +class TestRouting(unittest.TestCase): + pass diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index b32db3b62f..fb8fd26206 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -73,53 +73,6 @@ class TestWorkOrder(unittest.TestCase): self.assertRaises(StockOverProductionError, s.submit) - def test_make_time_sheet(self): - from erpnext.manufacturing.doctype.work_order.work_order import make_timesheet - wo_order = make_wo_order_test_record(item="_Test FG Item 2", - planned_start_date=now(), qty=1, do_not_save=True) - - wo_order.set_work_order_operations() - wo_order.insert() - wo_order.submit() - - d = wo_order.operations[0] - d.completed_qty = flt(d.completed_qty) - - name = frappe.db.get_value('Timesheet', {'work_order': wo_order.name}, 'name') - time_sheet_doc = frappe.get_doc('Timesheet', name) - self.assertEqual(wo_order.company, time_sheet_doc.company) - time_sheet_doc.submit() - - self.assertEqual(wo_order.name, time_sheet_doc.work_order) - self.assertEqual((wo_order.qty - d.completed_qty), - sum([d.completed_qty for d in time_sheet_doc.time_logs])) - - manufacturing_settings = frappe.get_doc({ - "doctype": "Manufacturing Settings", - "allow_production_on_holidays": 0 - }) - - manufacturing_settings.save() - - wo_order.load_from_db() - self.assertEqual(wo_order.operations[0].status, "Completed") - self.assertEqual(wo_order.operations[0].completed_qty, wo_order.qty) - - self.assertEqual(wo_order.operations[0].actual_operation_time, 60) - self.assertEqual(wo_order.operations[0].actual_operating_cost, 6000) - - time_sheet_doc1 = make_timesheet(wo_order.name, wo_order.company) - self.assertEqual(len(time_sheet_doc1.get('time_logs')), 0) - - time_sheet_doc.cancel() - - wo_order.load_from_db() - self.assertEqual(wo_order.operations[0].status, "Pending") - self.assertEqual(flt(wo_order.operations[0].completed_qty), 0) - - self.assertEqual(flt(wo_order.operations[0].actual_operation_time), 0) - self.assertEqual(flt(wo_order.operations[0].actual_operating_cost), 0) - def test_planned_operating_cost(self): wo_order = make_wo_order_test_record(item="_Test FG Item 2", planned_start_date=now(), qty=1, do_not_save=True) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 013183ec2b..e85b0a5411 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -4,7 +4,6 @@ frappe.ui.form.on("Work Order", { setup: function(frm) { frm.custom_make_buttons = { - 'Timesheet': 'Make Timesheet', 'Stock Entry': 'Make Stock Entry', } @@ -113,13 +112,11 @@ frappe.ui.form.on("Work Order", { frm.trigger('show_progress'); } - if(frm.doc.docstatus == 1 && frm.doc.status != 'Stopped'){ - frm.add_custom_button(__('Make Timesheet'), function(){ - frappe.model.open_mapped_doc({ - method: "erpnext.manufacturing.doctype.work_order.work_order.make_new_timesheet", - frm: cur_frm - }) - }) + if (frm.doc.docstatus === 1 && frm.doc.operations + && frm.doc.qty != frm.doc.material_transferred_for_manufacturing) { + frm.add_custom_button(__('Make Job Card'), () => { + frm.trigger("make_job_card") + }).addClass('btn-primary'); } if(frm.doc.required_items && frm.doc.allow_alternative_item) { @@ -139,6 +136,113 @@ frappe.ui.form.on("Work Order", { }); } } + + if (frm.doc.status == "Completed" && + frm.doc.__onload.backflush_raw_materials_based_on == "Material Transferred for Manufacture") { + frm.add_custom_button(__("Make BOM"), () => { + frm.trigger("make_bom"); + }); + } + }, + + make_job_card: function(frm) { + let qty = 0; + const fields = [{ + fieldtype: "Link", + fieldname: "operation", + options: "Operation", + label: __("Operation"), + get_query: () => { + const filter_workstation = frm.doc.operations.filter(d => { + if (d.status != "Completed") { + return d; + } + }); + + return { + filters: { + name: ["in", (filter_workstation || []).map(d => d.operation)] + } + }; + }, + reqd: true + }, { + fieldtype: "Link", + fieldname: "workstation", + options: "Workstation", + label: __("Workstation"), + get_query: () => { + const operation = dialog.get_value("operation"); + const filter_workstation = frm.doc.operations.filter(d => { + if (d.operation == operation) { + return d; + } + }); + + return { + filters: { + name: ["in", (filter_workstation || []).map(d => d.workstation)] + } + }; + }, + onchange: () => { + const operation = dialog.get_value("operation"); + const workstation = dialog.get_value("workstation"); + if (operation && workstation) { + const row = frm.doc.operations.filter(d => d.operation == operation && d.workstation == workstation)[0]; + qty = frm.doc.qty - row.completed_qty; + + if (qty > 0) { + dialog.set_value("qty", qty); + } + } + }, + reqd: true + }, { + fieldtype: "Float", + fieldname: "qty", + label: __("For Quantity"), + reqd: true + }]; + + const dialog = frappe.prompt(fields, function(data) { + if (data.qty > qty) { + frappe.throw(__("For Quantity must be less than quantity {0}", [qty])); + } + + if (data.qty <= 0) { + frappe.throw(__("For Quantity must be greater than zero")); + } + + frappe.call({ + method: "erpnext.manufacturing.doctype.work_order.work_order.make_job_card", + args: { + work_order: frm.doc.name, + operation: data.operation, + workstation: data.workstation, + qty: data.qty + }, + callback: function(r){ + if (r.message) { + var doc = frappe.model.sync(r.message)[0]; + frappe.set_route("Form", doc.doctype, doc.name); + } + } + }); + }, __("For Job Card")); + }, + + make_bom: function(frm) { + frappe.call({ + method: "make_bom", + doc: frm.doc, + callback: function(r){ + if (r.message) { + var doc = frappe.model.sync(r.message)[0]; + frappe.set_route("Form", doc.doctype, doc.name); + } + } + }); }, show_progress: function(frm) { @@ -189,7 +293,8 @@ frappe.ui.form.on("Work Order", { frm.set_value('sales_order', ""); frm.trigger('set_sales_order'); erpnext.in_production_item_onchange = true; - $.each(["description", "stock_uom", "project", "bom_no", "allow_alternative_item"], function(i, field) { + $.each(["description", "stock_uom", "project", "bom_no", + "allow_alternative_item", "transfer_material_against_job_card"], function(i, field) { frm.set_value(field, r.message[field]); }); @@ -235,6 +340,9 @@ frappe.ui.form.on("Work Order", { before_submit: function(frm) { frm.toggle_reqd(["fg_warehouse", "wip_warehouse"], true); frm.fields_dict.required_items.grid.toggle_reqd("source_warehouse", true); + if (frm.doc.operations) { + frm.fields_dict.operations.grid.toggle_reqd("workstation", true); + } }, set_sales_order: function(frm) { @@ -316,7 +424,10 @@ erpnext.work_order = { }, __("Status")); } - if(!frm.doc.skip_transfer){ + const show_start_btn = (frm.doc.skip_transfer + || frm.doc.transfer_material_against_job_card) ? 0 : 1; + + if (show_start_btn){ if ((flt(doc.material_transferred_for_manufacturing) < flt(doc.qty)) && frm.doc.status != 'Stopped') { frm.has_start_btn = true; diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index aef2ac4fe2..df9dd83a70 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -552,6 +552,39 @@ "translatable": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "operations", + "fieldname": "transfer_material_against_job_card", + "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": "Transfer Material Against Job Card", + "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 + }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, @@ -1639,7 +1672,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-08-29 06:28:22.983369", + "modified": "2018-09-05 06:28:22.983369", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order", diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 6995829757..1d465d57ae 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -190,6 +190,9 @@ class WorkOrder(Document): for purpose, fieldname in (("Manufacture", "produced_qty"), ("Material Transfer for Manufacture", "material_transferred_for_manufacturing")): + if (purpose == 'Material Transfer for Manufacture' and + self.operations and self.transfer_material_against_job_card): + continue qty = flt(frappe.db.sql("""select sum(fg_completed_qty) from `tabStock Entry` where work_order=%s and docstatus=1 @@ -209,9 +212,6 @@ class WorkOrder(Document): production_plan = frappe.get_doc('Production Plan', self.production_plan) production_plan.run_method("update_produced_qty", self.produced_qty, self.production_plan_item) - def before_submit(self): - self.make_time_logs() - def on_submit(self): if not self.wip_warehouse: frappe.throw(_("Work-in-Progress Warehouse is required before Submit")) @@ -223,18 +223,27 @@ class WorkOrder(Document): self.update_completed_qty_in_material_request() self.update_planned_qty() self.update_ordered_qty() + self.create_job_card() def on_cancel(self): self.validate_cancel() frappe.db.set(self,'status', 'Cancelled') self.update_work_order_qty_in_so() - self.delete_timesheet() + self.delete_job_card() self.update_completed_qty_in_material_request() self.update_planned_qty() self.update_ordered_qty() self.update_reserved_qty_for_production() + def create_job_card(self): + for row in self.operations: + if not row.workstation: + frappe.throw(_("Row {0}: select the workstation against the operation {1}") + .format(row.idx, row.operation)) + + create_job_card(self, row, auto_create=True) + def validate_cancel(self): if self.status == "Stopped": frappe.throw(_("Stopped Work Order cannot be cancelled, Unstop it first to cancel")) @@ -312,6 +321,17 @@ class WorkOrder(Document): """ % ", ".join(["%s"]*len(bom_list)), tuple(bom_list), as_dict=1) self.set('operations', operations) + + if self.use_multi_level_bom and self.get('operations') and self.get('items'): + raw_material_operations = [d.operation for d in self.get('items')] + operations = [d.operation for d in self.get('operations')] + + for operation in raw_material_operations: + if operation not in operations: + self.append('operations', { + 'operation': operation + }) + self.calculate_time() def calculate_time(self): @@ -335,99 +355,6 @@ class WorkOrder(Document): return holidays[holiday_list] - def make_time_logs(self, open_new=False): - """Capacity Planning. Plan time logs based on earliest availablity of workstation after - Planned Start Date. Time logs will be created and remain in Draft mode and must be submitted - before manufacturing entry can be made.""" - - if not self.operations: - return - - timesheets = [] - plan_days = frappe.db.get_single_value("Manufacturing Settings", "capacity_planning_for_days") or 30 - - timesheet = make_timesheet(self.name, self.company) - timesheet.set('time_logs', []) - - for i, d in enumerate(self.operations): - - if d.status != 'Completed': - self.set_start_end_time_for_workstation(d, i) - - args = self.get_operations_data(d) - - add_timesheet_detail(timesheet, args) - original_start_time = d.planned_start_time - - # validate operating hours if workstation [not mandatory] is specified - try: - timesheet.validate_time_logs() - except OverlapError: - if frappe.message_log: frappe.message_log.pop() - timesheet.schedule_for_work_order(d.idx) - except WorkstationHolidayError: - if frappe.message_log: frappe.message_log.pop() - timesheet.schedule_for_work_order(d.idx) - - from_time, to_time = self.get_start_end_time(timesheet, d.name) - - if date_diff(from_time, original_start_time) > plan_days: - frappe.throw(_("Unable to find Time Slot in the next {0} days for Operation {1}").format(plan_days, d.operation)) - break - - d.planned_start_time = from_time - d.planned_end_time = to_time - d.db_update() - - if timesheet and open_new: - return timesheet - - if timesheet and timesheet.get("time_logs"): - timesheet.save() - timesheets.append(getlink("Timesheet", timesheet.name)) - - self.planned_end_date = self.operations[-1].planned_end_time - if timesheets: - frappe.local.message_log = [] - frappe.msgprint(_("Timesheet created:") + "\n" + "\n".join(timesheets)) - - def get_operations_data(self, data): - return { - 'from_time': get_datetime(data.planned_start_time), - 'hours': data.time_in_mins / 60.0, - 'to_time': get_datetime(data.planned_end_time), - 'project': self.project, - 'operation': data.operation, - 'operation_id': data.name, - 'workstation': data.workstation, - 'completed_qty': flt(self.qty) - flt(data.completed_qty) - } - - def set_start_end_time_for_workstation(self, data, index): - """Set start and end time for given operation. If first operation, set start as - `planned_start_date`, else add time diff to end time of earlier operation.""" - - if index == 0: - data.planned_start_time = self.planned_start_date - else: - data.planned_start_time = get_datetime(self.operations[index-1].planned_end_time)\ - + get_mins_between_operations() - - data.planned_end_time = get_datetime(data.planned_start_time) + relativedelta(minutes = data.time_in_mins) - - if data.planned_start_time == data.planned_end_time: - frappe.throw(_("Capacity Planning Error")) - - def get_start_end_time(self, timesheet, operation_id): - for data in timesheet.time_logs: - if data.operation_id == operation_id: - return data.from_time, data.to_time - - def check_operation_fits_in_working_hours(self, d): - """Raises expection if operation is longer than working hours in the given workstation.""" - from erpnext.manufacturing.doctype.workstation.workstation import check_if_within_operating_hours - check_if_within_operating_hours(d.workstation, d.operation, d.planned_start_time, d.planned_end_time) - def update_operation_status(self): for d in self.get("operations"): if not d.completed_qty: @@ -451,9 +378,9 @@ class WorkOrder(Document): if actual_end_dates: self.actual_end_date = max(actual_end_dates) - def delete_timesheet(self): - for timesheet in frappe.get_all("Timesheet", ["name"], {"work_order": self.name}): - frappe.delete_doc("Timesheet", timesheet.name) + def delete_job_card(self): + for d in frappe.get_all("Job Card", ["name"], {"work_order": self.name}): + frappe.delete_doc("Job Card", d.name) def validate_production_item(self): if frappe.db.get_value("Item", self.production_item, "has_variants"): @@ -523,6 +450,7 @@ class WorkOrder(Document): else: for item in sorted(item_dict.values(), key=lambda d: d['idx']): self.append('required_items', { + 'operation': item.operation, 'item_code': item.item_code, 'item_name': item.item_name, 'description': item.description, @@ -573,6 +501,30 @@ class WorkOrder(Document): d.db_set('consumed_qty', flt(consumed_qty), update_modified = False) + def make_bom(self): + data = frappe.db.sql(""" select sed.item_code, sed.qty, sed.s_warehouse + from `tabStock Entry Detail` sed, `tabStock Entry` se + where se.name = sed.parent and se.purpose = 'Manufacture' + and (sed.t_warehouse is null or sed.t_warehouse = '') and se.docstatus = 1 + and se.work_order = %s""", (self.name), as_dict=1) + + bom = frappe.new_doc("BOM") + bom.item = self.production_item + bom.conversion_rate = 1 + + for d in data: + bom.append('items', { + 'item_code': d.item_code, + 'qty': d.qty, + 'source_warehouse': d.s_warehouse + }) + + if self.operations: + bom.set('operations', self.operations) + bom.with_operations = 1 + + bom.set_bom_material_details() + return bom @frappe.whitelist() def get_item_details(item, project = None): @@ -609,8 +561,12 @@ def get_item_details(item, project = None): else: frappe.throw(_("Default BOM for {0} not found").format(item)) - res['project'] = project or frappe.db.get_value('BOM', res['bom_no'], 'project') - res['allow_alternative_item'] = frappe.db.get_value('BOM', res['bom_no'], 'allow_alternative_item') + bom_data = frappe.db.get_value('BOM', res['bom_no'], + ['project', 'allow_alternative_item', 'transfer_material_against_job_card'], as_dict=1) + + res['project'] = project or bom_data.project + res['allow_alternative_item'] = bom_data.allow_alternative_item + res['transfer_material_against_job_card'] = bom_data.transfer_material_against_job_card res.update(check_if_scrap_warehouse_mandatory(res["bom_no"])) return res @@ -667,25 +623,6 @@ def make_stock_entry(work_order_id, purpose, qty=None): stock_entry.get_items() return stock_entry.as_dict() -@frappe.whitelist() -def make_timesheet(work_order, company): - timesheet = frappe.new_doc("Timesheet") - timesheet.employee = "" - timesheet.work_order = work_order - timesheet.company = company - return timesheet - -@frappe.whitelist() -def add_timesheet_detail(timesheet, args): - if isinstance(timesheet, string_types): - timesheet = frappe.get_doc('Timesheet', timesheet) - - if isinstance(args, string_types): - args = json.loads(args) - - timesheet.append('time_logs', args) - return timesheet - @frappe.whitelist() def get_default_warehouse(): wip_warehouse = frappe.db.get_single_value("Manufacturing Settings", @@ -694,16 +631,6 @@ def get_default_warehouse(): "default_fg_warehouse") return {"wip_warehouse": wip_warehouse, "fg_warehouse": fg_warehouse} -@frappe.whitelist() -def make_new_timesheet(source_name, target_doc=None): - po = frappe.get_doc('Work Order', source_name) - ts = po.make_time_logs(open_new=True) - - if not ts or not ts.get('time_logs'): - frappe.throw(_("Already completed")) - - return ts - @frappe.whitelist() def stop_unstop(work_order, status): """ Called from client side on Stop/Unstop event""" @@ -730,3 +657,40 @@ def query_sales_order(production_item): """, (production_item, production_item)) return out + +@frappe.whitelist() +def make_job_card(work_order, operation, workstation, qty=0): + work_order = frappe.get_doc('Work Order', work_order) + row = get_work_order_operation_data(work_order, operation, workstation) + if row: + return create_job_card(work_order, row, qty) + +def create_job_card(work_order, row, qty=0, auto_create=False): + doc = frappe.new_doc("Job Card") + doc.update({ + 'work_order': work_order.name, + 'operation': row.operation, + 'workstation': row.workstation, + 'posting_date': nowdate(), + 'for_quantity': qty or work_order.get('qty', 0), + 'operation_id': row.name, + 'bom_no': work_order.bom_no, + 'project': work_order.project, + 'company': work_order.company, + 'wip_warehouse': work_order.wip_warehouse + }) + + if work_order.transfer_material_against_job_card and not work_order.skip_transfer: + doc.get_required_items() + + if auto_create: + doc.flags.ignore_mandatory = True + doc.insert() + frappe.msgprint(_("Job card {0} created").format(doc.name)) + + return doc + +def get_work_order_operation_data(work_order, operation, workstation): + for d in work_order.operations: + if d.operation == operation and d.workstation == workstation: + return d diff --git a/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py b/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py index 9b7c9a3de0..02fbfcdeab 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py +++ b/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py @@ -5,7 +5,7 @@ def get_data(): 'fieldname': 'work_order', 'transactions': [ { - 'items': ['Stock Entry', 'Timesheet'] + 'items': ['Stock Entry', 'Job Card'] } ] } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/work_order_item/work_order_item.json b/erpnext/manufacturing/doctype/work_order_item/work_order_item.json index badeb91478..6dbb494483 100644 --- a/erpnext/manufacturing/doctype/work_order_item/work_order_item.json +++ b/erpnext/manufacturing/doctype/work_order_item/work_order_item.json @@ -12,6 +12,39 @@ "editable_grid": 1, "engine": "InnoDB", "fields": [ + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "operation", + "fieldtype": "Link", + "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": "Operation", + "length": 0, + "no_copy": 0, + "options": "Operation", + "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 + }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 0fe66e3005..a83dfd679c 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -563,4 +563,5 @@ erpnext.patches.v11_0.reset_publish_in_hub_for_all_items erpnext.patches.v11_0.update_hub_url # 2018-08-31 # 2018-09-03 erpnext.patches.v10_0.set_discount_amount erpnext.patches.v10_0.recalculate_gross_margin_for_project +erpnext.patches.v11_0.make_job_card erpnext.patches.v11_0.redesign_healthcare_billing_work_flow diff --git a/erpnext/patches/v11_0/make_job_card.py b/erpnext/patches/v11_0/make_job_card.py new file mode 100644 index 0000000000..ab9c7c414a --- /dev/null +++ b/erpnext/patches/v11_0/make_job_card.py @@ -0,0 +1,18 @@ +# Copyright (c) 2017, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe + +def execute(): + frappe.reload_doc('manufacturing', 'doctype', 'work_order') + frappe.reload_doc('manufacturing', 'doctype', 'work_order_item') + frappe.reload_doc('manufacturing', 'doctype', 'job_card') + frappe.reload_doc('manufacturing', 'doctype', 'job_card_item') + + for d in frappe.db.sql("""select work_order, name from tabTimesheet + where (work_order is not null and work_order != '') and docstatus = 0""", as_dict=1): + if d.work_order: + doc = frappe.get_doc('Work Order', d.work_order) + doc.make_job_card() + frappe.delete_doc('Timesheet', d.name) \ No newline at end of file diff --git a/erpnext/projects/doctype/timesheet/timesheet.json b/erpnext/projects/doctype/timesheet/timesheet.json index d1ec38c3cf..e5198dea63 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.json +++ b/erpnext/projects/doctype/timesheet/timesheet.json @@ -444,6 +444,39 @@ "translatable": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "start_date", + "fieldtype": "Date", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 1, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "User", + "length": 0, + "no_copy": 0, + "options": "User", + "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 + }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, @@ -508,73 +541,6 @@ "translatable": 0, "unique": 0 }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": "", - "columns": 0, - "depends_on": "work_order", - "fieldname": "work_detail", - "fieldtype": "Section Break", - "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": "Work Detail", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "work_order", - "fieldtype": "Link", - "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": "Work Order", - "length": 0, - "no_copy": 1, - "options": "Work Order", - "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 - }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, @@ -1066,7 +1032,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-08-21 14:44:32.912004", + "modified": "2018-08-28 14:44:32.912004", "modified_by": "Administrator", "module": "Projects", "name": "Timesheet", diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index 7dc121c7e3..f48c0c634b 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -97,19 +97,13 @@ class Timesheet(Document): self.set_status() def on_cancel(self): - self.update_work_order(None) self.update_task_and_project() def on_submit(self): self.validate_mandatory_fields() - self.update_work_order(self.name) self.update_task_and_project() def validate_mandatory_fields(self): - if self.work_order: - work_order = frappe.get_doc("Work Order", self.work_order) - pending_qty = flt(work_order.qty) - flt(work_order.produced_qty) - for data in self.time_logs: if not data.from_time and not data.to_time: frappe.throw(_("Row {0}: From Time and To Time is mandatory.").format(data.idx)) @@ -120,41 +114,6 @@ class Timesheet(Document): if flt(data.hours) == 0.0: frappe.throw(_("Row {0}: Hours value must be greater than zero.").format(data.idx)) - if self.work_order and flt(data.completed_qty) == 0: - frappe.throw(_("Row {0}: Completed Qty must be greater than zero.").format(data.idx)) - - if self.work_order and flt(pending_qty) < flt(data.completed_qty) and flt(pending_qty) > 0: - frappe.throw(_("Row {0}: Completed Qty cannot be more than {1} for operation {2}").format(data.idx, pending_qty, data.operation), - OverWorkLoggedError) - - def update_work_order(self, time_sheet): - if self.work_order: - pro = frappe.get_doc('Work Order', self.work_order) - - for timesheet in self.time_logs: - for data in pro.operations: - if data.name == timesheet.operation_id: - summary = self.get_actual_timesheet_summary(timesheet.operation_id) - data.time_sheet = time_sheet - data.completed_qty = summary.completed_qty - data.actual_operation_time = summary.mins - data.actual_start_time = summary.from_time - data.actual_end_time = summary.to_time - - pro.flags.ignore_validate_update_after_submit = True - pro.update_operation_status() - pro.calculate_operating_cost() - pro.set_actual_dates() - pro.save() - - def get_actual_timesheet_summary(self, operation_id): - """Returns 'Actual Operating Time'. """ - return frappe.db.sql("""select - sum(tsd.hours*60) as mins, sum(tsd.completed_qty) as completed_qty, min(tsd.from_time) as from_time, - max(tsd.to_time) as to_time from `tabTimesheet Detail` as tsd, `tabTimesheet` as ts where - ts.work_order = %s and tsd.operation_id = %s and ts.docstatus=1 and ts.name = tsd.parent""", - (self.work_order, operation_id), as_dict=1)[0] - def update_task_and_project(self): tasks, projects = [], [] @@ -176,16 +135,12 @@ class Timesheet(Document): def validate_time_logs(self): for data in self.get('time_logs'): - self.check_workstation_timings(data) self.validate_overlap(data) def validate_overlap(self, data): settings = frappe.get_single('Projects Settings') - if self.work_order: - self.validate_overlap_for("workstation", data, data.workstation, settings.ignore_workstation_time_overlap) - else: - 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) + 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_overlap_for(self, fieldname, args, value, ignore_validation=False): if not value or ignore_validation: @@ -227,48 +182,6 @@ class Timesheet(Document): return existing[0] if existing else None - def check_workstation_timings(self, args): - """Checks if **Time Log** is between operating hours of the **Workstation**.""" - if args.workstation and args.from_time and args.to_time: - check_if_within_operating_hours(args.workstation, args.operation, args.from_time, args.to_time) - - def schedule_for_work_order(self, index): - for data in self.time_logs: - if data.idx == index: - self.move_to_next_day(data) #check for workstation holiday - self.move_to_next_non_overlapping_slot(data) #check for overlap - break - - def move_to_next_non_overlapping_slot(self, data): - overlapping = self.get_overlap_for("workstation", data, data.workstation) - if overlapping: - time_sheet = self.get_last_working_slot(overlapping.name, data.workstation) - data.from_time = get_datetime(time_sheet.to_time) + get_mins_between_operations() - data.to_time = self.get_to_time(data) - self.check_workstation_working_day(data) - - def get_last_working_slot(self, time_sheet, workstation): - return frappe.db.sql(""" select max(from_time) as from_time, max(to_time) as to_time - from `tabTimesheet Detail` where workstation = %(workstation)s""", - {'workstation': workstation}, as_dict=True)[0] - - def move_to_next_day(self, data): - """Move start and end time one day forward""" - self.check_workstation_working_day(data) - - def check_workstation_working_day(self, data): - while True: - try: - self.check_workstation_timings(data) - break - except WorkstationHolidayError: - if frappe.message_log: frappe.message_log.pop() - data.from_time = get_datetime(data.from_time) + timedelta(hours=24) - data.to_time = self.get_to_time(data) - - def get_to_time(self, data): - return get_datetime(data.from_time) + timedelta(hours=data.hours) - def update_cost(self): for data in self.time_logs: if data.activity_type or data.billable: diff --git a/erpnext/stock/doctype/material_request/material_request.json b/erpnext/stock/doctype/material_request/material_request.json index 9927265e3f..c0285cbe14 100644 --- a/erpnext/stock/doctype/material_request/material_request.json +++ b/erpnext/stock/doctype/material_request/material_request.json @@ -779,6 +779,39 @@ "set_only_once": 0, "translatable": 0, "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "job_card", + "fieldtype": "Link", + "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": "Job Card", + "length": 0, + "no_copy": 0, + "options": "Job Card", + "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 } ], "has_web_view": 0, @@ -793,7 +826,7 @@ "istable": 0, "max_attachments": 0, "menu_index": 0, - "modified": "2018-08-30 07:28:01.070112", + "modified": "2018-09-05 07:28:01.070112", "modified_by": "Administrator", "module": "Stock", "name": "Material Request", diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 730ec3eb43..42c837091c 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -15,6 +15,7 @@ from erpnext.controllers.buying_controller import BuyingController from erpnext.manufacturing.doctype.work_order.work_order import get_item_details from erpnext.buying.utils import check_for_closed_status, validate_for_items from erpnext.stock.doctype.item.item import get_item_defaults +from erpnext.manufacturing.doctype.job_card.job_card import update_job_card_reference from six import string_types @@ -92,6 +93,9 @@ class MaterialRequest(BuyingController): if self.material_request_type == 'Purchase': self.validate_budget() + if self.job_card: + update_job_card_reference(self.job_card, 'material_request', self.name) + def before_save(self): self.set_status(update=True) @@ -144,6 +148,8 @@ class MaterialRequest(BuyingController): def on_cancel(self): self.update_requested_qty() self.update_requested_qty_in_production_plan() + if self.job_card: + update_job_card_reference(self.job_card, 'material_request', None) def update_completed_qty(self, mr_items=None, update_modified=True): if self.material_request_type == "Purchase": @@ -407,7 +413,11 @@ def make_stock_entry(source_name, target_doc=None): def set_missing_values(source, target): target.purpose = source.material_request_type + if source.job_card: + target.purpose = 'Material Transfer for Manufacture' + target.run_method("calculate_rate_and_amount") + target.set_job_card_data() doclist = get_mapped_doc("Material Request", source_name, { "Material Request": { diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index fa3501a132..0356b0e14f 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -654,7 +654,7 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({ work_order: function() { var me = this; this.toggle_enable_bom(); - if(!me.frm.doc.work_order) { + if(!me.frm.doc.work_order || me.frm.doc.job_card) { return; } diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index 0e3fbec6e5..35f8c27344 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -1936,6 +1936,39 @@ "translatable": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "job_card", + "fieldtype": "Link", + "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": "Job Card", + "length": 0, + "no_copy": 0, + "options": "Job Card", + "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_bulk_edit": 0, "allow_in_quick_entry": 0, @@ -2015,7 +2048,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-08-29 06:27:59.630826", + "modified": "2018-09-05 06:27:59.630826", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index f723fcf359..b7dbda2647 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -17,6 +17,7 @@ from erpnext.stock.utils import get_bin from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit, get_serial_nos import json +from erpnext.manufacturing.doctype.job_card.job_card import update_job_card_reference from six import string_types, itervalues, iteritems @@ -59,6 +60,7 @@ class StockEntry(StockController): self.validate_batch() self.validate_inspection() self.validate_fg_completed_qty() + self.set_job_card_data() if not self.from_bom: self.fg_completed_qty = 0.0 @@ -88,6 +90,9 @@ class StockEntry(StockController): self.update_so_in_serial_number() + if self.job_card: + update_job_card_reference(self.job_card, 'stock_entry', self.name) + def on_cancel(self): if self.purchase_order and self.purpose == "Subcontract": @@ -102,6 +107,18 @@ class StockEntry(StockController): self.make_gl_entries_on_cancel() self.update_cost_in_project() + if self.job_card: + update_job_card_reference(self.job_card, 'stock_entry', None) + + def set_job_card_data(self): + if self.job_card and not self.work_order: + data = frappe.db.get_value('Job Card', + self.job_card, ['for_quantity', 'work_order', 'bom_no'], as_dict=1) + self.fg_completed_qty = data.for_quantity + self.work_order = data.work_order + self.from_bom = 1 + self.bom_no = data.bom_no + def validate_work_order_status(self): pro_doc = frappe.get_doc("Work Order", self.work_order) if pro_doc.status == 'Completed': @@ -584,6 +601,10 @@ class StockEntry(StockController): if pro_doc.status == 'Stopped': frappe.throw(_("Transaction not allowed against stopped Work Order {0}").format(self.work_order)) + if self.job_card: + job_doc = frappe.get_doc('Job Card', self.job_card) + job_doc.set_transferred_qty() + if self.work_order: pro_doc = frappe.get_doc("Work Order", self.work_order) _validate_work_order(pro_doc)