[Enhance] Job Card (#15244)

* [Enhance] Added job card against the work order

* removed work order from timesheet

* Fixed codacy

* Added patch to make job card from the timesheet

* Timer in job card

* Dates validation in job card

* Added button to make job card from work order

* Added sub-assembly operation in the work order
This commit is contained in:
rohitwaghchaure 2018-09-06 19:21:48 +05:30 committed by Nabin Hait
parent 6f77abe0dd
commit 32dc3bf082
37 changed files with 2580 additions and 388 deletions

View File

@ -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);
}
}
});
}
}
});

View File

@ -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
}

View File

@ -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:

View File

@ -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",

View File

@ -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",

View File

@ -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 = `
<div class="stopwatch" style="font-weight:bold">
<span class="hours">00</span>
<span class="colon">:</span>
<span class="minutes">00</span>
<span class="colon">:</span>
<span class="seconds">00</span>
</div>`;
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 `<button> Start </button>`
}
});

View File

@ -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
}

View File

@ -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 <a href='#Form/Job Card/{0}'>{1}</a>")
.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

View File

@ -0,0 +1,12 @@
from frappe import _
def get_data():
return {
'fieldname': 'job_card',
'transactions': [
{
'label': _('Transactions'),
'items': ['Material Request', 'Stock Entry']
}
]
}

View File

@ -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"];
}
}
};

View File

@ -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()
]);
});

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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);
}
});

View File

@ -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
}

View File

@ -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

View File

@ -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()
]);
});

View File

@ -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

View File

@ -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)

View File

@ -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;

View File

@ -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",

View File

@ -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

View File

@ -5,7 +5,7 @@ def get_data():
'fieldname': 'work_order',
'transactions': [
{
'items': ['Stock Entry', 'Timesheet']
'items': ['Stock Entry', 'Job Card']
}
]
}

View File

@ -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,

View File

@ -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

View File

@ -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)

View File

@ -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",

View File

@ -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:

View File

@ -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",

View File

@ -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": {

View File

@ -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;
}

View File

@ -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",

View File

@ -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)