fix: Manufacturing UX, added calendar view for job card

This commit is contained in:
Rohit Waghchaure 2019-12-02 09:37:09 +05:30
parent 36b60dbbef
commit 8ab8376975
17 changed files with 715 additions and 1685 deletions

View File

@ -6,7 +6,6 @@ frappe.provide("erpnext.bom");
frappe.ui.form.on("BOM", { frappe.ui.form.on("BOM", {
setup: function(frm) { setup: function(frm) {
frm.custom_make_buttons = { frm.custom_make_buttons = {
'BOM': 'Duplicate BOM',
'Work Order': 'Work Order', 'Work Order': 'Work Order',
'Quality Inspection': 'Quality Inspection' 'Quality Inspection': 'Quality Inspection'
}; };
@ -91,10 +90,6 @@ frappe.ui.form.on("BOM", {
} }
if(frm.doc.docstatus!=0) { if(frm.doc.docstatus!=0) {
frm.add_custom_button(__("Duplicate BOM"), function() {
frm.copy_doc();
}, __("Create"));
frm.add_custom_button(__("Work Order"), function() { frm.add_custom_button(__("Work Order"), function() {
frm.trigger("make_work_order"); frm.trigger("make_work_order");
}, __("Create")); }, __("Create"));

View File

@ -25,5 +25,5 @@ def get_data():
} }
], ],
'disable_create_buttons': ["Item", "Purchase Order", "Purchase Receipt", 'disable_create_buttons': ["Item", "Purchase Order", "Purchase Receipt",
"Purchase Invoice", "Job Card", "Stock Entry"] "Purchase Invoice", "Job Card", "Stock Entry", "BOM"]
} }

View File

@ -3,6 +3,9 @@
frappe.ui.form.on('Job Card', { frappe.ui.form.on('Job Card', {
refresh: function(frm) { refresh: function(frm) {
frappe.flags.pause_job = 0;
frappe.flags.resume_job = 0;
if(!frm.doc.__islocal && frm.doc.items && frm.doc.items.length) { if(!frm.doc.__islocal && frm.doc.items && frm.doc.items.length) {
if (frm.doc.for_quantity != frm.doc.transferred_qty) { if (frm.doc.for_quantity != frm.doc.transferred_qty) {
frm.add_custom_button(__("Material Request"), () => { frm.add_custom_button(__("Material Request"), () => {
@ -13,35 +16,89 @@ frappe.ui.form.on('Job Card', {
if (frm.doc.for_quantity != frm.doc.transferred_qty) { if (frm.doc.for_quantity != frm.doc.transferred_qty) {
frm.add_custom_button(__("Material Transfer"), () => { frm.add_custom_button(__("Material Transfer"), () => {
frm.trigger("make_stock_entry"); frm.trigger("make_stock_entry");
}); }).addClass("btn-primary");
} }
} }
if (frm.doc.docstatus == 0) { if (frm.doc.docstatus == 0 && frm.doc.for_quantity > frm.doc.total_completed_qty
&& (!frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) {
frm.trigger("prepare_timer_buttons");
}
},
prepare_timer_buttons: function(frm) {
frm.trigger("make_dashboard"); frm.trigger("make_dashboard");
if (!frm.doc.job_started) { if (!frm.doc.job_started) {
frm.add_custom_button(__("Start Job"), () => { frm.add_custom_button(__("Start"), () => {
if (!frm.doc.employee) {
frappe.prompt({fieldtype: 'Link', label: __('Employee'), options: "Employee",
fieldname: 'employee'}, d => {
if (d.employee) {
frm.set_value("employee", d.employee);
}
frm.events.start_job(frm);
}, __("Enter Value"), __("Start"));
} else {
frm.events.start_job(frm);
}
}).addClass("btn-primary");
} else if (frm.doc.status == "On Hold") {
frm.add_custom_button(__("Resume"), () => {
frappe.flags.resume_job = 1;
frm.events.start_job(frm);
}).addClass("btn-primary");
} else {
frm.add_custom_button(__("Pause"), () => {
frappe.flags.pause_job = 1;
frm.set_value("status", "On Hold");
frm.events.complete_job(frm);
});
frm.add_custom_button(__("Complete"), () => {
let completed_time = frappe.datetime.now_datetime();
frm.trigger("hide_timer");
frappe.prompt({fieldtype: 'Float', label: __('Completed Quantity'),
fieldname: 'qty', reqd: 1, default: frm.doc.for_quantity}, data => {
frm.events.complete_job(frm, completed_time, data.qty);
}, __("Enter Value"), __("Complete"));
}).addClass("btn-primary");
}
},
start_job: function(frm) {
let row = frappe.model.add_child(frm.doc, 'Job Card Time Log', 'time_logs'); let row = frappe.model.add_child(frm.doc, 'Job Card Time Log', 'time_logs');
row.from_time = frappe.datetime.now_datetime(); row.from_time = frappe.datetime.now_datetime();
frm.set_value('job_started', 1); frm.set_value('job_started', 1);
frm.set_value('started_time' , row.from_time); frm.set_value('started_time' , row.from_time);
frm.set_value("status", "Work In Progress");
if (!frappe.flags.resume_job) {
frm.set_value('current_time' , 0);
}
frm.save(); frm.save();
}); },
} else {
frm.add_custom_button(__("Complete Job"), () => { complete_job: function(frm, completed_time, completed_qty) {
let completed_time = frappe.datetime.now_datetime();
frm.doc.time_logs.forEach(d => { frm.doc.time_logs.forEach(d => {
if (d.from_time && !d.to_time) { if (d.from_time && !d.to_time) {
d.to_time = completed_time; d.to_time = completed_time || frappe.datetime.now_datetime();
d.completed_qty = completed_qty || 0;
if(frappe.flags.pause_job) {
let currentIncrement = moment(d.to_time).diff(moment(d.from_time),"seconds") || 0;
frm.set_value('current_time' , currentIncrement + (frm.doc.current_time || 0));
} else {
frm.set_value('started_time' , ''); frm.set_value('started_time' , '');
frm.set_value('job_started', 0); frm.set_value('job_started', 0);
frm.set_value('current_time' , 0);
}
frm.save(); frm.save();
} }
})
}); });
}
}
}, },
make_dashboard: function(frm) { make_dashboard: function(frm) {
@ -50,7 +107,8 @@ frappe.ui.form.on('Job Card', {
frm.dashboard.refresh(); frm.dashboard.refresh();
const timer = ` const timer = `
<div class="stopwatch" style="font-weight:bold"> <div class="stopwatch" style="font-weight:bold;margin:0px 13px 0px 2px;
color:#545454;font-size:18px;display:inline-block;vertical-align:text-bottom;>
<span class="hours">00</span> <span class="hours">00</span>
<span class="colon">:</span> <span class="colon">:</span>
<span class="minutes">00</span> <span class="minutes">00</span>
@ -58,11 +116,16 @@ frappe.ui.form.on('Job Card', {
<span class="seconds">00</span> <span class="seconds">00</span>
</div>`; </div>`;
var section = frm.dashboard.add_section(timer); var section = frm.toolbar.page.add_inner_message(timer);
if (frm.doc.started_time) { let currentIncrement = frm.doc.current_time || 0;
let currentIncrement = moment(frappe.datetime.now_datetime()).diff(moment(frm.doc.started_time),"seconds"); if (frm.doc.started_time || frm.doc.current_time) {
if (frm.doc.status == "On Hold") {
updateStopwatch(currentIncrement);
} else {
currentIncrement += moment(frappe.datetime.now_datetime()).diff(moment(frm.doc.started_time),"seconds");
initialiseTimer(); initialiseTimer();
}
function initialiseTimer() { function initialiseTimer() {
const interval = setInterval(function() { const interval = setInterval(function() {
@ -88,6 +151,10 @@ frappe.ui.form.on('Job Card', {
} }
}, },
hide_timer: function(frm) {
frm.toolbar.page.inner_toolbar.find(".stopwatch").remove();
},
for_quantity: function(frm) { for_quantity: function(frm) {
frm.doc.items = []; frm.doc.items = [];
frm.call({ frm.call({
@ -117,5 +184,22 @@ frappe.ui.form.on('Job Card', {
timer: function(frm) { timer: function(frm) {
return `<button> Start </button>` return `<button> Start </button>`
},
set_total_completed_qty: function(frm) {
frm.doc.total_completed_qty = 0;
frm.doc.time_logs.forEach(d => {
if (d.completed_qty) {
frm.doc.total_completed_qty += d.completed_qty;
}
});
refresh_field("total_completed_qty");
} }
}); });
frappe.ui.form.on('Job Card Time Log', {
completed_qty: function(frm) {
frm.events.set_total_completed_qty(frm);
}
})

View File

@ -13,10 +13,18 @@
"column_break_4", "column_break_4",
"posting_date", "posting_date",
"company", "company",
"remarks",
"production_section",
"production_item",
"item_name",
"for_quantity", "for_quantity",
"wip_warehouse", "wip_warehouse",
"timing_detail", "column_break_12",
"employee", "employee",
"employee_name",
"status",
"project",
"timing_detail",
"time_logs", "time_logs",
"section_break_13", "section_break_13",
"total_completed_qty", "total_completed_qty",
@ -28,12 +36,11 @@
"operation_id", "operation_id",
"transferred_qty", "transferred_qty",
"requested_qty", "requested_qty",
"project",
"remarks",
"column_break_20", "column_break_20",
"status", "barcode",
"job_started", "job_started",
"started_time", "started_time",
"current_time",
"amended_from" "amended_from"
], ],
"fields": [ "fields": [
@ -41,13 +48,14 @@
"fieldname": "work_order", "fieldname": "work_order",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1,
"label": "Work Order", "label": "Work Order",
"options": "Work Order", "options": "Work Order",
"read_only": 1,
"reqd": 1, "reqd": 1,
"search_index": 1 "search_index": 1
}, },
{ {
"fetch_from": "work_order.bom_no",
"fieldname": "bom_no", "fieldname": "bom_no",
"fieldtype": "Link", "fieldtype": "Link",
"label": "BOM No", "label": "BOM No",
@ -91,7 +99,7 @@
"fieldname": "for_quantity", "fieldname": "for_quantity",
"fieldtype": "Float", "fieldtype": "Float",
"in_list_view": 1, "in_list_view": 1,
"label": "For Quantity", "label": "Qty To Manufacture",
"reqd": 1 "reqd": 1
}, },
{ {
@ -109,6 +117,7 @@
{ {
"fieldname": "employee", "fieldname": "employee",
"fieldtype": "Link", "fieldtype": "Link",
"in_standard_filter": 1,
"label": "Employee", "label": "Employee",
"options": "Employee" "options": "Employee"
}, },
@ -198,7 +207,7 @@
"fieldtype": "Select", "fieldtype": "Select",
"label": "Status", "label": "Status",
"no_copy": 1, "no_copy": 1,
"options": "Open\nWork In Progress\nMaterial Transferred\nSubmitted\nCancelled\nCompleted", "options": "Open\nWork In Progress\nMaterial Transferred\nOn Hold\nSubmitted\nCancelled\nCompleted",
"read_only": 1 "read_only": 1
}, },
{ {
@ -236,10 +245,52 @@
"label": "Naming Series", "label": "Naming Series",
"options": "PO-JOB.#####", "options": "PO-JOB.#####",
"reqd": 1 "reqd": 1
},
{
"fetch_from": "employee.employee_name",
"fieldname": "employee_name",
"fieldtype": "Read Only",
"label": "Employee Name"
},
{
"fieldname": "production_section",
"fieldtype": "Section Break",
"label": "Production"
},
{
"fieldname": "column_break_12",
"fieldtype": "Column Break"
},
{
"fetch_from": "work_order.production_item",
"fieldname": "production_item",
"fieldtype": "Read Only",
"label": "Production Item"
},
{
"fieldname": "barcode",
"fieldtype": "Barcode",
"label": "Barcode",
"read_only": 1
},
{
"fetch_from": "work_order.item_name",
"fieldname": "item_name",
"fieldtype": "Read Only",
"label": "Item Name"
},
{
"fieldname": "current_time",
"fieldtype": "Int",
"hidden": 1,
"label": "Current Time",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
} }
], ],
"is_submittable": 1, "is_submittable": 1,
"modified": "2019-10-30 01:49:19.606178", "modified": "2019-12-03 13:08:57.926201",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Job Card", "name": "Job Card",

View File

@ -194,8 +194,9 @@ class JobCard(Document):
if self.total_completed_qty <= 0.0: if self.total_completed_qty <= 0.0:
frappe.throw(_("Total completed qty must be greater than zero")) frappe.throw(_("Total completed qty must be greater than zero"))
if self.total_completed_qty > self.for_quantity: if self.total_completed_qty != self.for_quantity:
frappe.throw(_("Total completed qty can not be greater than for quantity")) frappe.throw(_("The total completed qty({0}) must be equal to qty to manufacture({1})"
.format(frappe.bold(self.total_completed_qty),frappe.bold(self.for_quantity))))
def update_work_order(self): def update_work_order(self):
if not self.work_order: if not self.work_order:
@ -271,6 +272,8 @@ class JobCard(Document):
self.set_status(update_status) self.set_status(update_status)
def set_status(self, update_status=False): def set_status(self, update_status=False):
if self.status == "On Hold": return
self.status = { self.status = {
0: "Open", 0: "Open",
1: "Submitted", 1: "Submitted",
@ -329,6 +332,7 @@ def make_stock_entry(source_name, target_doc=None):
target.fg_completed_qty = source.get('for_quantity', 0) - source.get('transferred_qty', 0) target.fg_completed_qty = source.get('for_quantity', 0) - source.get('transferred_qty', 0)
target.calculate_rate_and_amount() target.calculate_rate_and_amount()
target.set_missing_values() target.set_missing_values()
target.set_stock_entry_type()
doclist = get_mapped_doc("Job Card", source_name, { doclist = get_mapped_doc("Job Card", source_name, {
"Job Card": { "Job Card": {
@ -353,3 +357,45 @@ def make_stock_entry(source_name, target_doc=None):
def time_diff_in_minutes(string_ed_date, string_st_date): def time_diff_in_minutes(string_ed_date, string_st_date):
return time_diff(string_ed_date, string_st_date).total_seconds() / 60 return time_diff(string_ed_date, string_st_date).total_seconds() / 60
@frappe.whitelist()
def get_job_details(start, end, filters=None):
events = []
event_color = {
"Completed": "#cdf5a6",
"Material Transferred": "#ffdd9e",
"Work In Progress": "#D3D3D3"
}
from frappe.desk.reportview import get_filters_cond
conditions = get_filters_cond("Job Card", filters, [])
job_cards = frappe.db.sql(""" SELECT `tabJob Card`.name, `tabJob Card`.work_order,
`tabJob Card`.employee_name, `tabJob Card`.status, ifnull(`tabJob Card`.remarks, ''),
min(`tabJob Card Time Log`.from_time) as from_time,
max(`tabJob Card Time Log`.to_time) as to_time
FROM `tabJob Card` , `tabJob Card Time Log`
WHERE
`tabJob Card`.name = `tabJob Card Time Log`.parent {0}
group by `tabJob Card`.name""".format(conditions), as_dict=1)
for d in job_cards:
subject_data = []
for field in ["name", "work_order", "remarks", "employee_name"]:
if not d.get(field): continue
subject_data.append(d.get(field))
color = event_color.get(d.status)
job_card_data = {
'from_time': d.from_time,
'to_time': d.to_time,
'name': d.name,
'subject': '\n'.join(subject_data),
'color': color if color else "#89bcde"
}
events.append(job_card_data)
return events

View File

@ -0,0 +1,21 @@
frappe.views.calendar["Job Card"] = {
field_map: {
"start": "from_time",
"end": "to_time",
"id": "name",
"title": "subject",
"color": "color",
"allDay": "allDay",
"progress": "progress"
},
gantt: true,
filters: [
{
"fieldtype": "Link",
"fieldname": "employee",
"options": "Employee",
"label": __("Employee")
}
],
get_events_method: "erpnext.manufacturing.doctype.job_card.job_card.get_job_details"
};

View File

@ -1,208 +1,57 @@
{ {
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2019-03-08 23:56:43.187569", "creation": "2019-03-08 23:56:43.187569",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [
"from_time",
"to_time",
"column_break_2",
"time_in_mins",
"completed_qty"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "from_time", "fieldname": "from_time",
"fieldtype": "Datetime", "fieldtype": "Datetime",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0, "label": "From Time"
"label": "From Time",
"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,
"fetch_if_empty": 0,
"fieldname": "to_time", "fieldname": "to_time",
"fieldtype": "Datetime", "fieldtype": "Datetime",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0, "label": "To Time"
"label": "To Time",
"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,
"fetch_if_empty": 0,
"fieldname": "column_break_2", "fieldname": "column_break_2",
"fieldtype": "Column Break", "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,
"fetch_if_empty": 0,
"fieldname": "time_in_mins", "fieldname": "time_in_mins",
"fieldtype": "Float", "fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Time In Mins", "label": "Time In Mins",
"length": 0, "read_only": 1
"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,
"default": "0", "default": "0",
"fetch_if_empty": 0,
"fieldname": "completed_qty", "fieldname": "completed_qty",
"fieldtype": "Float", "fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Completed Qty", "label": "Completed Qty",
"length": 0, "reqd": 1
"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
} }
], ],
"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, "istable": 1,
"max_attachments": 0, "modified": "2019-12-03 12:56:02.285448",
"modified": "2019-03-10 17:08:46.504910",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Job Card Time Log", "name": "Job Card Time Log",
"name_case": "",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"quick_entry": 1, "quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "ASC", "sort_order": "ASC",
"track_changes": 1, "track_changes": 1
"track_seen": 0,
"track_views": 0
} }

View File

@ -71,12 +71,13 @@ frappe.ui.form.on('Production Plan', {
}, __('Create')); }, __('Create'));
} }
frm.page.set_inner_btn_group_as_primary(__('Create'));
frm.trigger("material_requirement"); frm.trigger("material_requirement");
const projected_qty_formula = ` <table class="table table-bordered" style="background-color: #f9f9f9;"> const projected_qty_formula = ` <table class="table table-bordered" style="background-color: #f9f9f9;">
<tr><td style="padding-left:25px"> <tr><td style="padding-left:25px">
<div> <div>
<h3> <h3 style="text-decoration: underline;">
<a href = "https://erpnext.com/docs/user/manual/en/stock/projected-quantity"> <a href = "https://erpnext.com/docs/user/manual/en/stock/projected-quantity">
${__("Projected Quantity Formula")} ${__("Projected Quantity Formula")}
</a> </a>

View File

@ -6,7 +6,7 @@ def get_data():
'fieldname': 'production_plan', 'fieldname': 'production_plan',
'transactions': [ 'transactions': [
{ {
'label': _('Related'), 'label': _('Transactions'),
'items': ['Work Order', 'Material Request'] 'items': ['Work Order', 'Material Request']
}, },
] ]

View File

@ -6,6 +6,7 @@ frappe.ui.form.on("Work Order", {
frm.custom_make_buttons = { frm.custom_make_buttons = {
'Stock Entry': 'Start', 'Stock Entry': 'Start',
'Pick List': 'Create Pick List', 'Pick List': 'Create Pick List',
'Job Card': 'Create Job Card'
}; };
// Set query for warehouses // Set query for warehouses
@ -131,7 +132,8 @@ frappe.ui.form.on("Work Order", {
} }
if (frm.doc.docstatus===1) { if (frm.doc.docstatus===1) {
frm.trigger('show_progress'); frm.trigger('show_progress_for_items');
frm.trigger('show_progress_for_operations');
} }
if (frm.doc.docstatus === 1 if (frm.doc.docstatus === 1
@ -179,89 +181,72 @@ frappe.ui.form.on("Work Order", {
make_job_card: function(frm) { make_job_card: function(frm) {
let qty = 0; let qty = 0;
const fields = [{ let operations_data = [];
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 { const dialog = frappe.prompt({fieldname: 'operations', fieldtype: 'Table', label: __('Operations'),
filters: { fields: [
name: ["in", (filter_workstation || []).map(d => d.operation)] {
} fieldtype:'Link',
}; fieldname:'operation',
label: __('Operation'),
read_only:1,
in_list_view:1
}, },
reqd: true {
}, { fieldtype:'Link',
fieldtype: "Link", fieldname:'workstation',
fieldname: "workstation", label: __('Workstation'),
options: "Workstation", read_only:1,
label: __("Workstation"), in_list_view:1
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"); fieldtype:'Data',
const workstation = dialog.get_value("workstation"); fieldname:'name',
if (operation && workstation) { label: __('Operation Id')
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',
fieldtype: "Float", fieldname:'pending_qty',
fieldname: "qty", label: __('Pending Qty'),
label: __("For Quantity"), },
reqd: true {
}]; fieldtype:'Float',
fieldname:'qty',
const dialog = frappe.prompt(fields, function(data) { label: __('Quantity to Manufacture'),
if (data.qty > qty) { read_only:0,
frappe.throw(__("For Quantity must be less than quantity {0}", [qty])); in_list_view:1,
},
],
data: operations_data,
in_place_edit: true,
get_data: function() {
return operations_data;
} }
}, function(data) {
if (data.qty <= 0) {
frappe.throw(__("For Quantity must be greater than zero"));
}
frappe.call({ frappe.call({
method: "erpnext.manufacturing.doctype.work_order.work_order.make_job_card", method: "erpnext.manufacturing.doctype.work_order.work_order.make_job_card",
args: { args: {
work_order: frm.doc.name, work_order: frm.doc.name,
operation: data.operation, operations: data.operations,
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")); }, __("Job Card"), __("Create"));
var pending_qty = 0;
frm.doc.operations.forEach(data => {
if(data.completed_qty != frm.doc.qty) {
pending_qty = frm.doc.qty - flt(data.completed_qty);
dialog.fields_dict.operations.df.data.push({
'name': data.name,
'operation': data.operation,
'workstation': data.workstation,
'qty': pending_qty,
'pending_qty': pending_qty,
});
}
});
dialog.fields_dict.operations.grid.refresh();
}, },
make_bom: function(frm) { make_bom: function(frm) {
@ -277,7 +262,7 @@ frappe.ui.form.on("Work Order", {
}); });
}, },
show_progress: function(frm) { show_progress_for_items: function(frm) {
var bars = []; var bars = [];
var message = ''; var message = '';
var added_min = false; var added_min = false;
@ -311,6 +296,44 @@ frappe.ui.form.on("Work Order", {
frm.dashboard.add_progress(__('Status'), bars, message); frm.dashboard.add_progress(__('Status'), bars, message);
}, },
show_progress_for_operations: function(frm) {
if (frm.doc.operations && frm.doc.operations.length) {
let progress_class = {
"Work in Progress": "progress-bar-warning",
"Completed": "progress-bar-success"
};
let bars = [];
let message = '';
let title = '';
let status_wise_oprtation_data = {};
let total_completed_qty = frm.doc.qty * frm.doc.operations.length;
frm.doc.operations.forEach(d => {
if (!status_wise_oprtation_data[d.status]) {
status_wise_oprtation_data[d.status] = [d.completed_qty, d.operation];
} else {
status_wise_oprtation_data[d.status][0] += d.completed_qty;
status_wise_oprtation_data[d.status][1] += ', ' + d.operation;
}
});
for (let key in status_wise_oprtation_data) {
title = __("{0} Operations: {1}", [key, status_wise_oprtation_data[key][1].bold()]);
bars.push({
'title': title,
'width': status_wise_oprtation_data[key][0] / total_completed_qty * 100 + '%',
'progress_class': progress_class[key]
});
message += title + '. ';
}
frm.dashboard.add_progress(__('Status'), bars, message);
}
},
production_item: function(frm) { production_item: function(frm) {
if (frm.doc.production_item) { if (frm.doc.production_item) {
frappe.call({ frappe.call({

View File

@ -1,4 +1,5 @@
{ {
"actions": [],
"allow_import": 1, "allow_import": 1,
"autoname": "naming_series:", "autoname": "naming_series:",
"creation": "2013-01-10 16:34:16", "creation": "2013-01-10 16:34:16",
@ -468,7 +469,8 @@
"idx": 1, "idx": 1,
"image_field": "image", "image_field": "image",
"is_submittable": 1, "is_submittable": 1,
"modified": "2019-08-28 12:29:35.315239", "links": [],
"modified": "2019-12-04 11:20:04.695123",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Work Order", "name": "Work Order",

View File

@ -6,7 +6,7 @@ import frappe
import json import json
import math import math
from frappe import _ from frappe import _
from frappe.utils import flt, get_datetime, getdate, date_diff, cint, nowdate from frappe.utils import flt, get_datetime, getdate, date_diff, cint, nowdate, get_link_to_form
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.manufacturing.doctype.bom.bom import validate_bom_no, get_bom_items_as_dict from erpnext.manufacturing.doctype.bom.bom import validate_bom_no, get_bom_items_as_dict
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
@ -755,21 +755,41 @@ def query_sales_order(production_item):
return out return out
@frappe.whitelist() @frappe.whitelist()
def make_job_card(work_order, operation, workstation, qty=0): def make_job_card(work_order, operations):
if isinstance(operations, string_types):
operations = json.loads(operations)
work_order = frappe.get_doc('Work Order', work_order) work_order = frappe.get_doc('Work Order', work_order)
row = get_work_order_operation_data(work_order, operation, workstation) for row in operations:
if row: validate_operation_data(row)
return create_job_card(work_order, row, qty) create_job_card(work_order, row, row.get("qty"), auto_create=True)
def validate_operation_data(row):
if row.get("qty") <= 0:
frappe.throw(_("Quantity to Manufacture can not be zero for the operation {0}")
.format(
frappe.bold(row.get("operation"))
)
)
if row.get("qty") > row.get("pending_qty"):
frappe.throw(_("For operation {0}: Quantity ({1}) can not be greter than pending quantity({2})")
.format(
frappe.bold(row.get("operation")),
frappe.bold(row.get("qty")),
frappe.bold(row.get("pending_qty"))
)
)
def create_job_card(work_order, row, qty=0, enable_capacity_planning=False, auto_create=False): def create_job_card(work_order, row, qty=0, enable_capacity_planning=False, auto_create=False):
doc = frappe.new_doc("Job Card") doc = frappe.new_doc("Job Card")
doc.update({ doc.update({
'work_order': work_order.name, 'work_order': work_order.name,
'operation': row.operation, 'operation': row.get("operation"),
'workstation': row.workstation, 'workstation': row.get("workstation"),
'posting_date': nowdate(), 'posting_date': nowdate(),
'for_quantity': qty or work_order.get('qty', 0), 'for_quantity': qty or work_order.get('qty', 0),
'operation_id': row.name, 'operation_id': row.get("name"),
'bom_no': work_order.bom_no, 'bom_no': work_order.bom_no,
'project': work_order.project, 'project': work_order.project,
'company': work_order.company, 'company': work_order.company,
@ -785,7 +805,7 @@ def create_job_card(work_order, row, qty=0, enable_capacity_planning=False, auto
doc.schedule_time_logs(row) doc.schedule_time_logs(row)
doc.insert() doc.insert()
frappe.msgprint(_("Job card {0} created").format(doc.name)) frappe.msgprint(_("Job card {0} created").format(get_link_to_form("Job Card", doc.name)))
return doc return doc

View File

@ -2,11 +2,13 @@
// For license information, please see license.txt // For license information, please see license.txt
frappe.views.calendar["Work Order"] = { frappe.views.calendar["Work Order"] = {
fields: ["planned_start_date", "planned_end_date", "status", "produced_qty", "qty", "name", "name"],
field_map: { field_map: {
"start": "planned_start_date", "start": "planned_start_date",
"end": "planned_end_date", "end": "planned_end_date",
"id": "name", "id": "name",
"title": "name", "title": "name",
"status": "status",
"allDay": "allDay", "allDay": "allDay",
"progress": function(data) { "progress": function(data) {
return flt(data.produced_qty) / data.qty * 100; return flt(data.produced_qty) / data.qty * 100;

View File

@ -6,7 +6,8 @@ def get_data():
'fieldname': 'work_order', 'fieldname': 'work_order',
'transactions': [ 'transactions': [
{ {
'items': ['Pick List', 'Stock Entry', 'Job Card'] 'label': _('Transactions'),
'items': ['Stock Entry', 'Job Card', 'Pick List']
} }
] ]
} }

View File

@ -1,4 +1,5 @@
{ {
"actions": [],
"creation": "2014-10-16 14:35:41.950175", "creation": "2014-10-16 14:35:41.950175",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
@ -68,6 +69,7 @@
"description": "Operation completed for how many finished goods?", "description": "Operation completed for how many finished goods?",
"fieldname": "completed_qty", "fieldname": "completed_qty",
"fieldtype": "Float", "fieldtype": "Float",
"in_list_view": 1,
"label": "Completed Qty", "label": "Completed Qty",
"no_copy": 1, "no_copy": 1,
"read_only": 1 "read_only": 1
@ -188,8 +190,9 @@
} }
], ],
"istable": 1, "istable": 1,
"modified": "2019-07-16 23:01:07.720337", "links": [],
"modified_by": "govindsmenokee@gmail.com", "modified": "2019-12-03 19:24:29.594189",
"modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Work Order Operation", "name": "Work Order Operation",
"owner": "Administrator", "owner": "Administrator",

View File

@ -13,5 +13,7 @@ def get_data():
'label': _('Transaction'), 'label': _('Transaction'),
'items': ['Work Order', 'Job Card', 'Timesheet'] 'items': ['Work Order', 'Job Card', 'Timesheet']
} }
] ],
'disable_create_buttons': ['BOM', 'Routing', 'Operation',
'Work Order', 'Job Card', 'Timesheet']
} }