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)