What is the best way to duplicate a DocType of ERPNext

Hello,

I want to duplicate the Work Order DocType completely with all its functionality.

I managed to duplicate the DocType by using duplicating function but this only duplicates the entry screen and data structure but no other functionality like for example when a user selects an Item to Manufacture it automatically fetches the relevant active BOM of that item or when a Work Order is Submitted to show the Start button and when user clicks on Start button to prompt user to enter Quantity, etc.

What is the best way to duplicate a DocType of ERPNext will all it functionality that is server side and client side?

TIA

Yogi Yang

@YogiYang It can be done by creating a Client Script for the doctype and using the same code as ERPNext. Based on the JavaScript code of ERPNext, there are 3 doctypes involved:

  1. Work Order
  2. Work Order Item
  3. Work Order Operation

Did you duplicate them all or not?

Yes I duplicated all of them.

TIA

Yogi Yang

@YogiYang This is the JS code used by ERPNext and don’t forget to change the names of doctypes to yours:

frappe.ui.form.on("Work Order", {
	setup: function(frm) {
		frm.custom_make_buttons = {
			'Stock Entry': 'Start',
			'Pick List': 'Create Pick List',
			'Job Card': 'Create Job Card'
		};

		// Set query for warehouses
		frm.set_query("wip_warehouse", function() {
			return {
				filters: {
					'company': frm.doc.company,
				}
			};
		});

		frm.set_query("source_warehouse", function() {
			return {
				filters: {
					'company': frm.doc.company,
				}
			};
		});

		frm.set_query("source_warehouse", "required_items", function() {
			return {
				filters: {
					'company': frm.doc.company,
				}
			};
		});

		frm.set_query("sales_order", function() {
			return {
				filters: {
					"status": ["not in", ["Closed", "On Hold"]]
				}
			};
		});

		frm.set_query("fg_warehouse", function() {
			return {
				filters: {
					'company': frm.doc.company,
					'is_group': 0
				}
			};
		});

		frm.set_query("scrap_warehouse", function() {
			return {
				filters: {
					'company': frm.doc.company,
					'is_group': 0
				}
			};
		});

		// Set query for BOM
		frm.set_query("bom_no", function() {
			if (frm.doc.production_item) {
				return {
					query: "erpnext.controllers.queries.bom",
					filters: {item: cstr(frm.doc.production_item)}
				};
			} else {
				frappe.msgprint(__("Please enter Production Item first"));
			}
		});

		// Set query for FG Item
		frm.set_query("production_item", function() {
			return {
				query: "erpnext.controllers.queries.item_query",
				filters: {
					"is_stock_item": 1,
				}
			};
		});

		// Set query for FG Item
		frm.set_query("project", function() {
			return{
				filters:[
					['Project', 'status', 'not in', 'Completed, Cancelled']
				]
			};
		});

		frm.set_query("operation", "required_items", function() {
			return {
				query: "erpnext.manufacturing.doctype.work_order.work_order.get_bom_operations",
				filters: {
					'parent': frm.doc.bom_no,
					'parenttype': 'BOM'
				}
			};
		});

		// formatter for work order operation
		frm.set_indicator_formatter('operation',
			function(doc) { return (frm.doc.qty==doc.completed_qty) ? "green" : "orange"; });
	},

	onload: function(frm) {
		if (!frm.doc.status)
			frm.doc.status = 'Draft';

		frm.add_fetch("sales_order", "project", "project");

		if(frm.doc.__islocal) {
			frm.set_value({
				"actual_start_date": "",
				"actual_end_date": ""
			});
			erpnext.work_order.set_default_warehouse(frm);
		}
	},

	source_warehouse: function(frm) {
		let transaction_controller = new erpnext.TransactionController();
		transaction_controller.autofill_warehouse(frm.doc.required_items, "source_warehouse", frm.doc.source_warehouse);
	},

	refresh: function(frm) {
		erpnext.toggle_naming_series();
		erpnext.work_order.set_custom_buttons(frm);
		frm.set_intro("");

		if (frm.doc.docstatus === 0 && !frm.is_new()) {
			frm.set_intro(__("Submit this Work Order for further processing."));
		} else {
			frm.trigger("show_progress_for_items");
			frm.trigger("show_progress_for_operations");
		}

		if (frm.doc.status != "Closed") {
			if (frm.doc.docstatus === 1
				&& frm.doc.operations && frm.doc.operations.length) {

				const not_completed = frm.doc.operations.filter(d => {
					if (d.status != 'Completed') {
						return true;
					}
				});

				if (not_completed && not_completed.length) {
					frm.add_custom_button(__('Create Job Card'), () => {
						frm.trigger("make_job_card");
					}).addClass('btn-primary');
				}
			}
		}

		if(frm.doc.required_items && frm.doc.allow_alternative_item) {
			const has_alternative = frm.doc.required_items.find(i => i.allow_alternative_item === 1);
			if (frm.doc.docstatus == 0 && has_alternative) {
				frm.add_custom_button(__('Alternate Item'), () => {
					erpnext.utils.select_alternate_items({
						frm: frm,
						child_docname: "required_items",
						warehouse_field: "source_warehouse",
						child_doctype: "Work Order Item",
						original_item_field: "original_item",
						condition: (d) => {
							if (d.allow_alternative_item) {return true;}
						}
					});
				});
			}
		}

		if (frm.doc.status == "Completed" &&
			frm.doc.__onload.backflush_raw_materials_based_on == "Material Transferred for Manufacture") {
			frm.add_custom_button(__('Create BOM'), () => {
				frm.trigger("make_bom");
			});
		}
	},

	make_job_card: function(frm) {
		let qty = 0;
		let operations_data = [];

		const dialog = frappe.prompt({fieldname: 'operations', fieldtype: 'Table', label: __('Operations'),
			fields: [
				{
					fieldtype: 'Link',
					fieldname: 'operation',
					label: __('Operation'),
					read_only: 1,
					in_list_view: 1
				},
				{
					fieldtype: 'Link',
					fieldname: 'workstation',
					label: __('Workstation'),
					read_only: 1,
					in_list_view: 1
				},
				{
					fieldtype: 'Data',
					fieldname: 'name',
					label: __('Operation Id')
				},
				{
					fieldtype: 'Float',
					fieldname: 'pending_qty',
					label: __('Pending Qty'),
				},
				{
					fieldtype: 'Float',
					fieldname: 'qty',
					label: __('Quantity to Manufacture'),
					read_only: 0,
					in_list_view: 1,
				},
				{
					fieldtype: 'Float',
					fieldname: 'batch_size',
					label: __('Batch Size'),
					read_only: 1
				},
			],
			data: operations_data,
			in_place_edit: true,
			get_data: function() {
				return operations_data;
			}
		}, function(data) {
			frappe.call({
				method: "erpnext.manufacturing.doctype.work_order.work_order.make_job_card",
				freeze: true,
				args: {
					work_order: frm.doc.name,
					operations: data.operations,
				},
				callback: function() {
					frm.reload_doc();
				}
			});
		}, __("Job Card"), __("Create"));

		dialog.fields_dict["operations"].grid.wrapper.find('.grid-add-row').hide();

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

				if (pending_qty) {
					dialog.fields_dict.operations.df.data.push({
						'name': data.name,
						'operation': data.operation,
						'workstation': data.workstation,
						'batch_size': data.batch_size,
						'qty': pending_qty,
						'pending_qty': pending_qty
					});
				}
			}
		});
		dialog.fields_dict.operations.grid.refresh();
	},

	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_for_items: function(frm) {
		var bars = [];
		var message = '';
		var added_min = false;

		// produced qty
		var title = __('{0} items produced', [frm.doc.produced_qty]);
		bars.push({
			'title': title,
			'width': (frm.doc.produced_qty / frm.doc.qty * 100) + '%',
			'progress_class': 'progress-bar-success'
		});
		if (bars[0].width == '0%') {
			bars[0].width = '0.5%';
			added_min = 0.5;
		}
		message = title;
		// pending qty
		if(!frm.doc.skip_transfer){
			var pending_complete = frm.doc.material_transferred_for_manufacturing - frm.doc.produced_qty;
			if(pending_complete) {
				var width = ((pending_complete / frm.doc.qty * 100) - added_min);
				title = __('{0} items in progress', [pending_complete]);
				bars.push({
					'title': title,
					'width': (width > 100 ? "99.5" : width)  + '%',
					'progress_class': 'progress-bar-warning'
				});
				message = message + '. ' + title;
			}
		}
		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) {
		if (frm.doc.production_item) {
			frappe.call({
				method: "erpnext.manufacturing.doctype.work_order.work_order.get_item_details",
				args: {
					item: frm.doc.production_item,
					project: frm.doc.project
				},
				freeze: true,
				callback: function(r) {
					if(r.message) {
						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",
							"transfer_material_against", "item_name"], function(i, field) {
							frm.set_value(field, r.message[field]);
						});

						if(r.message["set_scrap_wh_mandatory"]){
							frm.toggle_reqd("scrap_warehouse", true);
						}
						erpnext.in_production_item_onchange = false;
					}
				}
			});
		}
	},

	project: function(frm) {
		if(!erpnext.in_production_item_onchange && !frm.doc.bom_no) {
			frm.trigger("production_item");
		}
	},

	bom_no: function(frm) {
		return frm.call({
			doc: frm.doc,
			method: "get_items_and_operations_from_bom",
			freeze: true,
			callback: function(r) {
				if(r.message["set_scrap_wh_mandatory"]){
					frm.toggle_reqd("scrap_warehouse", true);
				}
			}
		});
	},

	use_multi_level_bom: function(frm) {
		if(frm.doc.bom_no) {
			frm.trigger("bom_no");
		}
	},

	qty: function(frm) {
		frm.trigger('bom_no');
	},

	before_submit: function(frm) {
		frm.fields_dict.required_items.grid.toggle_reqd("source_warehouse", true);
		frm.toggle_reqd("transfer_material_against",
			frm.doc.operations && frm.doc.operations.length > 0);
		frm.fields_dict.operations.grid.toggle_reqd("workstation", frm.doc.operations);
	},

	set_sales_order: function(frm) {
		if(frm.doc.production_item) {
			frappe.call({
				method: "erpnext.manufacturing.doctype.work_order.work_order.query_sales_order",
				args: { production_item: frm.doc.production_item },
				callback: function(r) {
					frm.set_query("sales_order", function() {
						erpnext.in_production_item_onchange = true;
						return {
							filters: [
								["Sales Order","name", "in", r.message]
							]
						};
					});
				}
			});
		}
	},

	additional_operating_cost: function(frm) {
		erpnext.work_order.calculate_cost(frm.doc);
		erpnext.work_order.calculate_total_cost(frm);
	},
});

frappe.ui.form.on("Work Order Item", {
	source_warehouse: function(frm, cdt, cdn) {
		var row = locals[cdt][cdn];
		if(!row.item_code) {
			frappe.throw(__("Please set the Item Code first"));
		} else if(row.source_warehouse) {
			frappe.call({
				"method": "erpnext.stock.utils.get_latest_stock_qty",
				args: {
					item_code: row.item_code,
					warehouse: row.source_warehouse
				},
				callback: function (r) {
					frappe.model.set_value(row.doctype, row.name,
						"available_qty_at_source_warehouse", r.message);
				}
			});
		}
	},

	item_code: function(frm, cdt, cdn) {
		let row = locals[cdt][cdn];

		if (row.item_code) {
			frappe.call({
				method: "erpnext.stock.doctype.item.item.get_item_details",
				args: {
					item_code: row.item_code,
					company: frm.doc.company
				},
				callback: function(r) {
					if (r.message) {
						frappe.model.set_value(cdt, cdn, {
							"required_qty": 1,
							"item_name": r.message.item_name,
							"description": r.message.description,
							"source_warehouse": r.message.default_warehouse,
							"allow_alternative_item": r.message.allow_alternative_item,
							"include_item_in_manufacturing": r.message.include_item_in_manufacturing
						});
					}
				}
			});
		}
	}
});

frappe.ui.form.on("Work Order Operation", {
	workstation: function(frm, cdt, cdn) {
		var d = locals[cdt][cdn];
		if (d.workstation) {
			frappe.call({
				"method": "frappe.client.get",
				args: {
					doctype: "Workstation",
					name: d.workstation
				},
				callback: function (data) {
					frappe.model.set_value(d.doctype, d.name, "hour_rate", data.message.hour_rate);
					erpnext.work_order.calculate_cost(frm.doc);
					erpnext.work_order.calculate_total_cost(frm);
				}
			});
		}
	},
	time_in_mins: function(frm, cdt, cdn) {
		erpnext.work_order.calculate_cost(frm.doc);
		erpnext.work_order.calculate_total_cost(frm);
	},
});

erpnext.work_order = {
	set_custom_buttons: function(frm) {
		var doc = frm.doc;
		if (doc.docstatus === 1 && doc.status != "Closed") {
			frm.add_custom_button(__('Close'), function() {
				frappe.confirm(__("Once the Work Order is Closed. It can't be resumed."),
					() => {
						erpnext.work_order.change_work_order_status(frm, "Closed");
					}
				);
			}, __("Status"));

			if (doc.status != 'Stopped' && doc.status != 'Completed') {
				frm.add_custom_button(__('Stop'), function() {
					erpnext.work_order.change_work_order_status(frm, "Stopped");
				}, __("Status"));
			} else if (doc.status == 'Stopped') {
				frm.add_custom_button(__('Re-open'), function() {
					erpnext.work_order.change_work_order_status(frm, "Resumed");
				}, __("Status"));
			}

			const show_start_btn = (frm.doc.skip_transfer
				|| frm.doc.transfer_material_against == 'Job Card') ? 0 : 1;

			if (show_start_btn) {
				let pending_to_transfer = frm.doc.required_items.some(
					item => flt(item.transferred_qty) < flt(item.required_qty)
				);
				if (pending_to_transfer && frm.doc.status != 'Stopped') {
					frm.has_start_btn = true;
					frm.add_custom_button(__('Create Pick List'), function() {
						erpnext.work_order.create_pick_list(frm);
					});
					var start_btn = frm.add_custom_button(__('Start'), function() {
						erpnext.work_order.make_se(frm, 'Material Transfer for Manufacture');
					});
					start_btn.addClass('btn-primary');
				}
			}

			if(!frm.doc.skip_transfer){
				// If "Material Consumption is check in Manufacturing Settings, allow Material Consumption
				if ((flt(doc.produced_qty) < flt(doc.material_transferred_for_manufacturing))
				&& frm.doc.status != 'Stopped') {
					frm.has_finish_btn = true;

					if (frm.doc.__onload && frm.doc.__onload.material_consumption == 1) {
						// Only show "Material Consumption" when required_qty > consumed_qty
						var counter = 0;
						var tbl = frm.doc.required_items || [];
						var tbl_lenght = tbl.length;
						for (var i = 0, len = tbl_lenght; i < len; i++) {
							let wo_item_qty = frm.doc.required_items[i].transferred_qty || frm.doc.required_items[i].required_qty;
							if (flt(wo_item_qty) > flt(frm.doc.required_items[i].consumed_qty)) {
								counter += 1;
							}
						}
						if (counter > 0) {
							var consumption_btn = frm.add_custom_button(__('Material Consumption'), function() {
								const backflush_raw_materials_based_on = frm.doc.__onload.backflush_raw_materials_based_on;
								erpnext.work_order.make_consumption_se(frm, backflush_raw_materials_based_on);
							});
							consumption_btn.addClass('btn-primary');
						}
					}

					var finish_btn = frm.add_custom_button(__('Finish'), function() {
						erpnext.work_order.make_se(frm, 'Manufacture');
					});

					if(doc.material_transferred_for_manufacturing>=doc.qty) {
						// all materials transferred for manufacturing, make this primary
						finish_btn.addClass('btn-primary');
					}
				}
			} else {
				if ((flt(doc.produced_qty) < flt(doc.qty)) && frm.doc.status != 'Stopped') {
					var finish_btn = frm.add_custom_button(__('Finish'), function() {
						erpnext.work_order.make_se(frm, 'Manufacture');
					});
					finish_btn.addClass('btn-primary');
				}
			}
		}

	},
	calculate_cost: function(doc) {
		if (doc.operations){
			var op = doc.operations;
			doc.planned_operating_cost = 0.0;
			for(var i=0;i<op.length;i++) {
				var planned_operating_cost = flt(flt(op[i].hour_rate) * flt(op[i].time_in_mins) / 60, 2);
				frappe.model.set_value('Work Order Operation', op[i].name,
					"planned_operating_cost", planned_operating_cost);
				doc.planned_operating_cost += planned_operating_cost;
			}
			refresh_field('planned_operating_cost');
		}
	},

	calculate_total_cost: function(frm) {
		let variable_cost = flt(frm.doc.actual_operating_cost) || flt(frm.doc.planned_operating_cost);
		frm.set_value("total_operating_cost", (flt(frm.doc.additional_operating_cost) + variable_cost));
	},

	set_default_warehouse: function(frm) {
		if (!(frm.doc.wip_warehouse || frm.doc.fg_warehouse)) {
			frappe.call({
				method: "erpnext.manufacturing.doctype.work_order.work_order.get_default_warehouse",
				callback: function(r) {
					if (!r.exe) {
						frm.set_value("wip_warehouse", r.message.wip_warehouse);
						frm.set_value("fg_warehouse", r.message.fg_warehouse);
						frm.set_value("scrap_warehouse", r.message.scrap_warehouse);
					}
				}
			});
		}
	},

	get_max_transferable_qty: (frm, purpose) => {
		let max = 0;
		if (frm.doc.skip_transfer) {
			max = flt(frm.doc.qty) - flt(frm.doc.produced_qty);
		} else {
			if (purpose === 'Manufacture') {
				max = flt(frm.doc.material_transferred_for_manufacturing) - flt(frm.doc.produced_qty);
			} else {
				max = flt(frm.doc.qty) - flt(frm.doc.material_transferred_for_manufacturing);
			}
		}
		return flt(max, precision('qty'));
	},

	show_prompt_for_qty_input: function(frm, purpose) {
		let max = this.get_max_transferable_qty(frm, purpose);
		return new Promise((resolve, reject) => {
			frappe.prompt({
				fieldtype: 'Float',
				label: __('Qty for {0}', [purpose]),
				fieldname: 'qty',
				description: __('Max: {0}', [max]),
				default: max
			}, data => {
				max += (frm.doc.qty * (frm.doc.__onload.overproduction_percentage || 0.0)) / 100;

				if (data.qty > max) {
					frappe.msgprint(__('Quantity must not be more than {0}', [max]));
					reject();
				}
				data.purpose = purpose;
				resolve(data);
			}, __('Select Quantity'), __('Create'));
		});
	},

	make_se: function(frm, purpose) {
		this.show_prompt_for_qty_input(frm, purpose)
			.then(data => {
				return frappe.xcall('erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry', {
					'work_order_id': frm.doc.name,
					'purpose': purpose,
					'qty': data.qty
				});
			}).then(stock_entry => {
				frappe.model.sync(stock_entry);
				frappe.set_route('Form', stock_entry.doctype, stock_entry.name);
			});

	},

	create_pick_list: function(frm, purpose='Material Transfer for Manufacture') {
		this.show_prompt_for_qty_input(frm, purpose)
			.then(data => {
				return frappe.xcall('erpnext.manufacturing.doctype.work_order.work_order.create_pick_list', {
					'source_name': frm.doc.name,
					'for_qty': data.qty
				});
			}).then(pick_list => {
				frappe.model.sync(pick_list);
				frappe.set_route('Form', pick_list.doctype, pick_list.name);
			});
	},

	make_consumption_se: function(frm, backflush_raw_materials_based_on) {
		if(!frm.doc.skip_transfer){
			var max = (backflush_raw_materials_based_on === "Material Transferred for Manufacture") ?
				flt(frm.doc.material_transferred_for_manufacturing) - flt(frm.doc.produced_qty) :
				flt(frm.doc.qty) - flt(frm.doc.produced_qty);
				// flt(frm.doc.qty) - flt(frm.doc.material_transferred_for_manufacturing);
		} else {
			var max = flt(frm.doc.qty) - flt(frm.doc.produced_qty);
		}

		frappe.call({
			method:"erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry",
			args: {
				"work_order_id": frm.doc.name,
				"purpose": "Material Consumption for Manufacture",
				"qty": max
			},
			callback: function(r) {
				var doclist = frappe.model.sync(r.message);
				frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
			}
		});
	},

	change_work_order_status: function(frm, status) {
		let method_name = status=="Closed" ? "close_work_order" : "stop_unstop";
		frappe.call({
			method: `erpnext.manufacturing.doctype.work_order.work_order.${method_name}`,
			freeze: true,
			freeze_message: __("Updating Work Order status"),
			args: {
				work_order: frm.doc.name,
				status: status
			},
			callback: function(r) {
				if(r.message) {
					frm.set_value("status", r.message);
					frm.reload_doc();
				}
			}
		});
	}
};

frappe.tour['Work Order'] = [
	{
		fieldname: "production_item",
		title: "Item to Manufacture",
		description: __("Select the Item to be manufactured.")
	},
	{
		fieldname: "bom_no",
		title: "BOM No",
		description: __("The default BOM for that item will be fetched by the system. You can also change the BOM.")
	},
	{
		fieldname: "qty",
		title: "Qty to Manufacture",
		description: __("Enter the quantity to manufacture. Raw material Items will be fetched only when this is set.")
	},
	{
		fieldname: "use_multi_level_bom",
		title: "Use Multi-Level BOM",
		description: __("This is enabled by default. If you want to plan materials for sub-assemblies of the Item you're manufacturing leave this enabled. If you plan and manufacture the sub-assemblies separately, you can disable this checkbox.")
	},
	{
		fieldname: "source_warehouse",
		title: "Source Warehouse",
		description: __("The warehouse where you store your raw materials. Each required item can have a separate source warehouse. Group warehouse also can be selected as source warehouse. On submission of the Work Order, the raw materials will be reserved in these warehouses for production usage.")
	},
	{
		fieldname: "fg_warehouse",
		title: "Target Warehouse",
		description: __("The warehouse where you store finished Items before they are shipped.")
	},
	{
		fieldname: "wip_warehouse",
		title: "Work-in-Progress Warehouse",
		description: __("The warehouse where your Items will be transferred when you begin production. Group Warehouse can also be selected as a Work in Progress warehouse.")
	},
	{
		fieldname: "scrap_warehouse",
		title: "Scrap Warehouse",
		description: __("If the BOM results in Scrap material, the Scrap Warehouse needs to be selected.")
	},
	{
		fieldname: "required_items",
		title: "Required Items",
		description: __("All the required items (raw materials) will be fetched from BOM and populated in this table. Here you can also change the Source Warehouse for any item. And during the production, you can track transferred raw materials from this table.")
	},
	{
		fieldname: "planned_start_date",
		title: "Planned Start Date",
		description: __("Set the Planned Start Date (an Estimated Date at which you want the Production to begin)")
	},
	{
		fieldname: "operations",
		title: "Operations",
		description: __("If the selected BOM has Operations mentioned in it, the system will fetch all Operations from BOM, these values can be changed.")
	},


];

Hello,

I opened the original work_order.js file and have copy pasted the code but still it is not working.

Have I to also copy and paste the code in work_order.py file?

TIA

Yogi Yang

@YogiYang Yes, but you have to be careful with the changes you make because the py file contains a lot of checking.

Also, you can tell me what are you exactly trying to achieve and let us see if there is a much simpler way to do that.

Hello,

Thanks for your help.

Actually what I want to do is a bit complex but let me try and explain.

When user clicks on Start in a Work Order I want to ask user for Quantity to Manufacture. This feature is already there.

Now when user clicks on Finish I want to ask user to enter two values:

  1. Qty Manufactured
  2. Qty Rejected

If the total of Qty Manufactured + Qty Rejected is less or greater than what the user had entered at time of Start I want to show message and ask user to correct it.

If the total matches with Quantity to Manufacture I want to save the values respectively in two fields:

  1. Manufactured Qty
  2. Rejected Qty

From what I understand currently ERPNext does not have facility to enter rejection quantity so I have added field called Qty Rejected.

Once a user competes a Work Order I want to auto update the Quantity to Manufacture of all the work orders that follows current Work Order as per the value in Manufactured Qty.

I hope I am making some sense here.

TIA

Yogi Yang

@YogiYang I believe that this can be done using a client script only. Before I give you the code I want to discuss several things with you.

  1. There is a field already for the Manufactured Qty. Its fieldname is produced_qty. Is the already defined field has the data you want for the Manufactured Qty field?

  2. The quantity to be produced, is it the value of Qty To Manufacture or not?

  3. The doctype you want to update, is it Work Order Operation? If not, then what is the name of the doctype?

  4. What is the fieldname that you want to update in the doctype mentioned in the previous question? And how to calculate the value of that field?

@YogiYang If you are good in JS then this is the client script that you must apply to the form of Work Order doctype:

The fieldname of the Rejected Qty custom field you created is assumed to be rejected_qty.


frappe.ui.form.on("Work Order", {
    refresh: function(frm) {
        var doc = frm.doc;
    	if (doc.docstatus === 1 && doc.status != "Closed") {
            // The finish dialog
    	    let dialog = new frappe.ui.Dialog({
                title: 'Validating the quantity',
                fields: [
                    {
                        label: 'Rejected Qty',
                        fieldname: 'rejected_qty',
                        fieldtype: 'Float'
                    }
                ],
                primary_action_label: 'Proceed',
                primary_action(vals) {
                    dialog.hide();
                    if (flt(flt(doc.produced_qty) + flt(vals.rejected_qty)) !== flt(doc.qty)) {
                        // The quantity mismatch message
                        frappe.msgprint({
                            title: __('Wrong Quantity'),
                            indicator: 'red',
                            message: __('The quantity of the work order is not equal to the total of manufactured and rejected quantities')
                        });
                    } else {
                        // set the value of rejected quantity field
                        doc.rejected_qty = flt(vals.rejected_qty);
                        // insert the code to update whatever you want here

                        // continue with the finish
                        erpnext.work_order.make_se(frm, 'Manufacture');
                    }
                }
            });
            
    		if(!frm.doc.skip_transfer){
    			// If "Material Consumption is check in Manufacturing Settings, allow Material Consumption
    			if ((flt(doc.produced_qty) < flt(doc.material_transferred_for_manufacturing))
    			&& frm.doc.status != 'Stopped') {
    				frm.remove_custom_button(__('Finish'));
    
    				var finish_btn = frm.add_custom_button(__('Finish'), function() {
    					dialog.show();
    				});
    
    				if(doc.material_transferred_for_manufacturing>=doc.qty) {
    					// all materials transferred for manufacturing, make this primary
    					finish_btn.addClass('btn-primary');
    				}
    			}
    		} else {
    			if ((flt(doc.produced_qty) < flt(doc.qty)) && frm.doc.status != 'Stopped') {
    			    frm.remove_custom_button(__('Finish'));
    			    
    				var finish_btn = frm.add_custom_button(__('Finish'), function() {
    					dialog.show();
    				});
    				finish_btn.addClass('btn-primary');
    			}
    		}
    	}
    }
});

Hello,

Yes this field works for me but if the qty is less, then the Work Order will not get closed (Finished) so I have added two fields:

  • Actual Mfg Qty
  • Rejected Qty

I am not touching the field Qty To Manufacture

Yes. Qty To Manufacture = Qty to Produce

Let me try and explain this with an example. Suppose we create a Production Plan to manufacture 10,000 pieces and submit it.
Now when we click on create Work Order ERPNext will create WOs for all the items (sub BOMs) in BOM with the Qty to Manufacture set to 10,000. So this case there will be 7 WOs created.

In the first WO we manufacture 9500 pieces and reject 500 pieces. So when we Finish the WO rest of the WOs’s Qty to Manufacture should be changed to 9500.

TIA

Yogi Yang

Hello,

While we are discussing WO I want to also ask if we can automate entry of Material Transfer when we Start a WO and Finish a WO?

TIA

Yogi Yang

@YogiYang How are those WOs linked? Is it using BOM No, Sales Order or Project? Sorry for asking, but, I’m not good at Manufacturing workflow.

And regarding the automation, I believe you can.

@YogiYang Sorry, I missed out the Production Plan, which links all the WOs.

I will modify the code to your needs now.

@YogiYang To clarify things for both of us:

Custom Doctype Fields

  1. Label: Actual Mfg Qty, fieldname: actual_mfg_qty
  2. Label: Rejected Qty, fieldname: rejected_qty

I think that these fields should be readonly and depends_on doc.status === ‘Completed’

Finish Dialog

  1. Show same fields as the custom ones mentioned above
  2. If actual_mfg_qty + rejected_qty is not equal to Qty To Manufactureqty then update the Qty To Manufacture of all the other WOs of the same Production Plan
  3. Set the actual_mfg_qty and rejected_qty of the current WO
  4. Continue the finish process without showing the normal finish prompt

Q. What should happen to the first WO if the second WO also has rejected quantity?


My suggestion for the whole finish process is:

  1. Keep the default finish prompt and qty validation
  2. Treat the qty entered in prompt as actual_mfg_qty
  3. Calculate rejected_qty by deducting the qty entered in prompt from WO qty
  4. Update the actual_mfg_qty and rejected_qty fields in current WO
  5. Update the Qty To Manufactureqty in other Production Plan WOs
  6. Continue with the remaining finish process as ERPNext

Moreover, the code of my proposal is smaller than the one I posted before.

Proposed Code
var _super_make_se = erpnext.work_order.make_se;
erpnext.work_order.make_se = function(frm, purpose) {
    if (purpose !== 'Manufacture') {
        _super_make_se(frm, purpose);
    } else {
        this.show_prompt_for_qty_input(frm, purpose)
		.then(data => {
		    frappe.db.set_value('Work Order', frm.doc.name, {
		        actual_mfg_qty: flt(data.qty),
		        rejected_qty: flt(frm.doc.qty) - flt(data.qty)
		    });
			// update other work orders related to the linked production plan
            frappe.db.get_list('Work Order', {
                fields: ['name'],
                filter: {
                    name: ['!=', frm.doc.name],
                    production_plan: frm.doc.production_plan,
                    status: frm.doc.status
                }
            }).then(function(rows) {
                $.each(rows, function(i, row) {
                    frappe.db.set_value('Work Order', row.name, 'qty', data.qty);
                });
            });
            return frappe.xcall('erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry', {
				'work_order_id': frm.doc.name,
				'purpose': purpose,
				'qty': data.qty
            });
		}).then(stock_entry => {
			frappe.model.sync(stock_entry);
			frappe.set_route('Form', stock_entry.doctype, stock_entry.name);
		});
    }
};

Hello @kid1194,

Actually nothing because the production process has passed that stage. At the most what they can do is issue a Rework Order.

I have not yet tried your code practically. But will test it today.

But in Work Order a user can click on Start and then enter value for Qty to manufacture and then click on Finish to complete manufacturing of the same Qty. So here a user may do manufacturing is smaller Quantity based on the capacity of the machine/workstation in question.

For example the Work Order is to manufacture 10,000 items. But the machine which is going to be used has the capacity to handle only 3000 items at a time so the user will perform manufacturing in smaller quantity multiple times till the Qty of the Work Order is not completed.

And after trying out various scenarios I have come to the understanding that the Work Order gets completed only and only after the whole Qty has been manufactured.

And when every smaller chunk is Started & after every smaller chunk is Finished it adds and entries of Material Transfer from Source Warehouse to Target Warehouse. So there are two entries here.

Q. Will the code handle this situation and then only update all of the following Work Orders?

TIA

Yogi Yang

@YogiYang Thanks, for explaining the process. Throughout this topic, I have learned a lot about work order.

To answer your question, I have further improved the code and what it will do is:

  1. Check if the action is Finish otherwise it will proceed as normal
  2. Show the normal qty check as normal (Default)
  3. Create the stock entry for the work Order (Default)
  4. Update the actual_mfg_qty and rejected_qty fields of current WO, but not the qty fields
  5. Get the other WOs related to the same Production Plan
  6. Update the qty of other WOs related to the same Production Plan
  7. Go to the stock entry (Default)
var _super_make_se = erpnext.work_order.make_se;
erpnext.work_order.make_se = function(frm, purpose) {
    if (purpose !== 'Manufacture') {
        _super_make_se(frm, purpose);
    } else {
        this.show_prompt_for_qty_input(frm, purpose)
		.then(data => {
		    data.stock_entry = frappe.xcall('erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry', {
				'work_order_id': frm.doc.name,
				'purpose': purpose,
				'qty': data.qty
            });
		    return data;
        }).then(data => {
		    frappe.db.set_value('Work Order', frm.doc.name, {
		        actual_mfg_qty: flt(data.qty),
		        rejected_qty: flt(frm.doc.qty) - flt(data.qty)
		    });
		    return data;
        }).then(data => {
			// get other work orders related to the linked production plan
            data.other_work_orders = frappe.db.get_list('Work Order', {
                fields: ['name'],
                filter: {
                    name: ['!=', frm.doc.name],
                    production_plan: frm.doc.production_plan,
                    status: frm.doc.status
                }
            });
            return data;
        }).then(data => {
            // update other work orders related to the linked production plan
            $.each(data.other_work_orders, function(i, row) {
                frappe.db.set_value('Work Order', row.name, 'qty', data.qty);
            });
            return data;
        }).then(data => {
			frappe.model.sync(data.stock_entry);
			frappe.set_route('Form', data.stock_entry.doctype, data.stock_entry.name);
		});
    }
};

@kid1194,

Let me play with your code.

I will get back to you soon.

TIA

Yogi Yang