feat: Job Card Enhancements

This commit is contained in:
Rohit Waghchaure 2021-01-05 14:18:26 +05:30
parent 81d164134d
commit 6e81489095
25 changed files with 834 additions and 299 deletions

View File

@ -13,10 +13,10 @@
"col_break1",
"hour_rate",
"time_in_mins",
"batch_size",
"operating_cost",
"base_hour_rate",
"base_operating_cost",
"batch_size",
"image"
],
"fields": [
@ -61,6 +61,8 @@
},
{
"description": "In minutes",
"fetch_from": "operation.total_operation_time",
"fetch_if_empty": 1,
"fieldname": "time_in_mins",
"fieldtype": "Float",
"in_list_view": 1,
@ -104,7 +106,8 @@
"label": "Image"
},
{
"default": "1",
"fetch_from": "operation.batch_size",
"fetch_if_empty": 1,
"fieldname": "batch_size",
"fieldtype": "Int",
"label": "Batch Size"
@ -120,7 +123,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-10-13 18:14:10.018774",
"modified": "2020-12-14 15:01:33.142869",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Operation",

View File

@ -11,6 +11,16 @@ frappe.ui.form.on('Job Card', {
}
};
});
frm.set_indicator_formatter('sub_operation',
function(doc) {
if (doc.status == "Pending") {
return "red";
} else {
return doc.status === "Complete" ? "green" : "orange";
}
}
);
},
refresh: function(frm) {
@ -97,81 +107,76 @@ frappe.ui.form.on('Job Card', {
prepare_timer_buttons: function(frm) {
frm.trigger("make_dashboard");
if (!frm.doc.job_started) {
frm.add_custom_button(__("Start"), () => {
if (!frm.doc.employee) {
frappe.prompt({fieldtype: 'Link', label: __('Employee'), options: "Employee",
if (!frm.doc.started_time && !frm.doc.current_time) {
frm.add_custom_button(__("Start Job"), () => {
frappe.prompt({fieldtype: 'Table MultiSelect', label: __('Employee'), options: "Job Card Time Log",
fieldname: 'employee'}, d => {
if (d.employee) {
frm.set_value("employee", d.employee);
} else {
frm.events.start_job(frm);
}
}, __("Enter Value"), __("Start"));
} else {
frm.events.start_job(frm);
}
debugger
frm.events.start_job(frm, "Work In Progress", d.employee);
}, __("Assign Job to Employee"));
}).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);
frm.add_custom_button(__("Resume Job"), () => {
frm.events.start_job(frm, "Resume Job");
}).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(__("Pause Job"), () => {
frm.events.complete_job(frm, "On Hold");
});
frm.add_custom_button(__("Complete"), () => {
let completed_time = frappe.datetime.now_datetime();
frm.trigger("hide_timer");
if (frm.doc.for_quantity) {
frm.add_custom_button(__("Complete Job"), () => {
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"));
} else {
frm.events.complete_job(frm, completed_time, 0);
}
fieldname: 'qty', default: frm.doc.for_quantity}, data => {
frm.events.complete_job(frm, "Complete", data.qty);
}, __("Enter Value"));
}).addClass("btn-primary");
}
},
start_job: function(frm) {
let row = frappe.model.add_child(frm.doc, 'Job Card Time Log', 'time_logs');
row.from_time = frappe.datetime.now_datetime();
frm.set_value('job_started', 1);
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();
start_job: function(frm, status, employee) {
const args = {
job_card_id: frm.doc.name,
start_time: frappe.datetime.now_datetime(),
employee: employee,
status: status
};
frm.events.make_time_log(frm, args);
},
complete_job: function(frm, completed_time, completed_qty) {
frm.doc.time_logs.forEach(d => {
if (d.from_time && !d.to_time) {
d.to_time = completed_time || frappe.datetime.now_datetime();
d.completed_qty = completed_qty || 0;
complete_job: function(frm, status, completed_qty) {
const args = {
job_card_id: frm.doc.name,
complete_time: frappe.datetime.now_datetime(),
status: status,
completed_qty: completed_qty
};
frm.events.make_time_log(frm, args);
},
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('job_started', 0);
frm.set_value('current_time' , 0);
}
make_time_log: function(frm, args) {
frm.events.update_sub_operation(frm, args);
frm.save();
frappe.call({
method: "erpnext.manufacturing.doctype.job_card.job_card.make_time_log",
args: {
args: args
},
freeze: true,
callback: function (r) {
frm.reload_doc();
frm.trigger("make_dashboard");
}
})
},
update_sub_operation: function(frm, args) {
if (frm.doc.sub_operations && frm.doc.sub_operations.length) {
let sub_operations = frm.doc.sub_operations.filter(d => d.status != 'Complete');
if (sub_operations && sub_operations.length) {
args["sub_operation"] = sub_operations[0].sub_operation;
}
}
});
},
validate: function(frm) {
@ -180,18 +185,8 @@ frappe.ui.form.on('Job Card', {
}
},
employee: function(frm) {
if (frm.doc.job_started && !frm.doc.current_time) {
frm.trigger("reset_timer");
} else {
frm.events.start_job(frm);
}
},
reset_timer: function(frm) {
frm.set_value('started_time' , '');
frm.set_value('job_started', 0);
frm.set_value('current_time' , 0);
},
make_dashboard: function(frm) {
@ -297,7 +292,6 @@ frappe.ui.form.on('Job Card Time Log', {
},
to_time: function(frm) {
frm.set_value('job_started', 0);
frm.set_value('started_time', '');
}
})

View File

@ -9,30 +9,30 @@
"naming_series",
"work_order",
"bom_no",
"workstation",
"operation",
"operation_row_number",
"column_break_4",
"posting_date",
"company",
"remarks",
"production_section",
"production_item",
"item_name",
"for_quantity",
"quality_inspection",
"wip_warehouse",
"column_break_12",
"employee",
"employee_name",
"status",
"wip_warehouse",
"quality_inspection",
"project",
"operation_section_section",
"operation",
"operation_row_number",
"column_break_18",
"workstation",
"section_break_21",
"sub_operations",
"timing_detail",
"time_logs",
"section_break_13",
"total_completed_qty",
"total_time_in_mins",
"column_break_15",
"total_time_in_mins",
"section_break_8",
"items",
"more_information",
@ -40,7 +40,9 @@
"sequence_id",
"transferred_qty",
"requested_qty",
"status",
"column_break_20",
"remarks",
"barcode",
"job_started",
"started_time",
@ -117,13 +119,6 @@
"fieldtype": "Section Break",
"label": "Timing Detail"
},
{
"fieldname": "employee",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Employee",
"options": "Employee"
},
{
"allow_bulk_edit": 1,
"fieldname": "time_logs",
@ -133,9 +128,11 @@
},
{
"fieldname": "section_break_13",
"fieldtype": "Section Break"
"fieldtype": "Section Break",
"hide_border": 1
},
{
"default": "0",
"fieldname": "total_completed_qty",
"fieldtype": "Float",
"label": "Total Completed Qty",
@ -251,12 +248,7 @@
"reqd": 1
},
{
"fetch_from": "employee.employee_name",
"fieldname": "employee_name",
"fieldtype": "Read Only",
"label": "Employee Name"
},
{
"collapsible": 1,
"fieldname": "production_section",
"fieldtype": "Section Break",
"label": "Production"
@ -314,11 +306,33 @@
"label": "Quality Inspection",
"no_copy": 1,
"options": "Quality Inspection"
},
{
"allow_bulk_edit": 1,
"fieldname": "sub_operations",
"fieldtype": "Table",
"label": "Sub Operations",
"options": "Job Card Operation",
"read_only": 1
},
{
"fieldname": "operation_section_section",
"fieldtype": "Section Break",
"label": "Operation Section"
},
{
"fieldname": "column_break_18",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_21",
"fieldtype": "Section Break",
"hide_border": 1
}
],
"is_submittable": 1,
"links": [],
"modified": "2020-11-19 18:26:50.531664",
"modified": "2020-12-14 15:14:05.566271",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Job Card",

View File

@ -4,12 +4,13 @@
from __future__ import unicode_literals
import frappe
import datetime
import datetime, json
from frappe import _, bold
from six import string_types
from frappe.model.mapper import get_mapped_doc
from frappe.model.document import Document
from frappe.utils import (flt, cint, time_diff_in_hours, get_datetime, getdate,
get_time, add_to_date, time_diff, add_days, get_datetime_str, get_link_to_form)
get_time, add_to_date, time_diff, add_days, get_datetime_str, get_link_to_form, time_diff_in_seconds)
from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations
@ -25,9 +26,20 @@ class JobCard(Document):
self.set_status()
self.validate_operation_id()
self.validate_sequence_id()
self.get_sub_operations()
self.update_sub_operation_status()
def get_sub_operations(self):
if self.operation:
self.sub_operations = []
for row in frappe.get_all("Sub Operation",
filters = {"parent": self.operation}, fields=["operation"]):
self.append("sub_operations", {
"sub_operation": row.operation,
"status": "Pending"
})
def validate_time_logs(self):
self.total_completed_qty = 0.0
self.total_time_in_mins = 0.0
if self.get('time_logs'):
@ -46,6 +58,8 @@ class JobCard(Document):
if d.completed_qty:
self.total_completed_qty += d.completed_qty
else:
self.total_completed_qty = 0.0
self.total_completed_qty = flt(self.total_completed_qty, self.precision("total_completed_qty"))
@ -57,7 +71,7 @@ class JobCard(Document):
self.workstation, 'production_capacity') or 1
validate_overlap_for = " and jc.workstation = %(workstation)s "
if self.employee:
if args.get("employee"):
# override capacity for employee
production_capacity = 1
validate_overlap_for = " and jc.employee = %(employee)s "
@ -80,7 +94,7 @@ class JobCard(Document):
"to_time": args.to_time,
"name": args.name or "No Name",
"parent": args.parent or "No Name",
"employee": self.employee,
"employee": args.get("employee"),
"workstation": self.workstation
}, as_dict=True)
@ -158,6 +172,66 @@ class JobCard(Document):
row.planned_start_time = datetime.datetime.combine(start_date,
get_time(workstation_doc.working_hours[0].start_time))
def add_time_log(self, args):
last_row = []
if self.time_logs and len(self.time_logs) > 0:
last_row = self.time_logs[-1]
self.reset_timer_value(args)
if last_row and args.get("complete_time"):
last_row.update({
"to_time": get_datetime(args.get("complete_time")),
"operation": args.get("sub_operation"),
"completed_qty": args.get("completed_qty") or 0.0
})
elif args.get("start_time"):
self.append("time_logs", {
"from_time": get_datetime(args.get("start_time")),
"employee": args.get("employee"),
"operation": args.get("sub_operation"),
"completed_qty": 0.0
})
if self.status == "On Hold":
self.current_time = time_diff_in_seconds(last_row.to_time, last_row.from_time)
self.save()
def reset_timer_value(self, args):
self.started_time = None
if args.get("status") in ["Work In Progress", "Complete"]:
self.current_time = 0.0
if args.get("status") == "Work In Progress":
self.started_time = get_datetime(args.get("start_time"))
if args.get("status") == "Resume Job":
args["status"] = "Work In Progress"
if args.get("status"):
self.status = args.get("status")
def update_sub_operation_status(self):
if not (self.sub_operations and self.time_logs): return
operation_wise_completed_time = {}
for time_log in self.time_logs:
if time_log.operation not in operation_wise_completed_time:
operation_wise_completed_time.setdefault(time_log.operation,
frappe._dict({"status": "Pending", "completed_time": 0.0}))
op_row = operation_wise_completed_time[time_log.operation]
op_row.status = "Work In Progress" if not time_log.time_in_mins else "Complete"
if time_log.time_in_mins:
op_row.completed_time += time_log.time_in_mins
for row in self.sub_operations:
operation_deatils = operation_wise_completed_time.get(row.sub_operation)
if operation_deatils:
row.status = operation_deatils.status
row.completed_time = operation_deatils.completed_time
def update_time_logs(self, row):
self.append("time_logs", {
"from_time": row.planned_start_time,
@ -376,6 +450,17 @@ class JobCard(Document):
frappe.throw(_("{0}, complete the operation {1} before the operation {2}.")
.format(message, bold(row.operation), bold(self.operation)), OperationSequenceError)
@frappe.whitelist()
def make_time_log(args):
if isinstance(args, string_types):
args = json.loads(args)
args = frappe._dict(args)
doc = frappe.get_doc("Job Card", args.job_card_id)
doc.validate_sequence_id()
doc.add_time_log(args)
@frappe.whitelist()
def get_operation_details(work_order, operation):
if work_order and operation:

View File

@ -0,0 +1,52 @@
{
"actions": [],
"creation": "2020-12-07 16:58:38.449041",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"sub_operation",
"completed_time",
"status"
],
"fields": [
{
"default": "Pending",
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "Complete\nPause\nPending\nWork In Progress",
"read_only": 1
},
{
"description": "In mins",
"fieldname": "completed_time",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Completed Time",
"read_only": 1
},
{
"fieldname": "sub_operation",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Operation",
"options": "Operation",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-12-14 17:08:25.992957",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Job Card Operation",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class JobCardOperation(Document):
pass

View File

@ -1,14 +1,17 @@
{
"actions": [],
"creation": "2019-03-08 23:56:43.187569",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"employee",
"from_time",
"to_time",
"column_break_2",
"time_in_mins",
"completed_qty"
"completed_qty",
"operation"
],
"fields": [
{
@ -41,10 +44,27 @@
"in_list_view": 1,
"label": "Completed Qty",
"reqd": 1
},
{
"fieldname": "employee",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Employee",
"options": "Employee"
},
{
"fieldname": "operation",
"fieldtype": "Link",
"label": "Operation",
"no_copy": 1,
"options": "Operation",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"modified": "2019-12-03 12:56:02.285448",
"links": [],
"modified": "2020-12-23 14:30:00.970916",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Job Card Time Log",

View File

@ -26,7 +26,9 @@
"column_break_16",
"overproduction_percentage_for_work_order",
"other_settings_section",
"update_bom_costs_automatically"
"update_bom_costs_automatically",
"column_break_23",
"make_serial_no_batch_from_work_order"
],
"fields": [
{
@ -155,13 +157,24 @@
{
"fieldname": "column_break_5",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_23",
"fieldtype": "Column Break"
},
{
"default": "0",
"description": "System will automatically create the serial numbers / batch for the Finished Good on submission of work order",
"fieldname": "make_serial_no_batch_from_work_order",
"fieldtype": "Check",
"label": "Make Serial No / Batch from Work Order"
}
],
"icon": "icon-wrench",
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2020-10-13 10:55:43.996581",
"modified": "2020-12-08 13:37:40.325838",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Manufacturing Settings",

View File

@ -2,7 +2,5 @@
// For license information, please see license.txt
frappe.ui.form.on('Operation', {
refresh: function(frm) {
}
});

View File

@ -1,167 +1,133 @@
{
"allow_copy": 0,
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "Prompt",
"beta": 0,
"creation": "2014-11-07 16:20:30.683186",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "Setup",
"editable_grid": 0,
"engine": "InnoDB",
"field_order": [
"workstation",
"data_2",
"cost_of_poor_quality_operation",
"job_card_section",
"create_job_card_based_on_batch_size",
"column_break_6",
"batch_size",
"sub_operations_section",
"sub_operations",
"total_operation_time",
"section_break_4",
"description"
],
"fields": [
{
"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_list_view": 1,
"in_standard_filter": 1,
"label": "Default 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": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"options": "Workstation"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"collapsible": 1,
"fieldname": "section_break_4",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 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,
"unique": 0
"label": "Operation Description"
},
{
"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_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": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"label": "Description"
},
{
"collapsible": 1,
"fieldname": "sub_operations_section",
"fieldtype": "Section Break",
"label": "Sub Operations"
},
{
"fieldname": "sub_operations",
"fieldtype": "Table",
"options": "Sub Operation"
},
{
"description": "Time in mins.",
"fieldname": "total_operation_time",
"fieldtype": "Float",
"label": "Total Operation Time",
"read_only": 1
},
{
"fieldname": "data_2",
"fieldtype": "Column Break"
},
{
"default": "1",
"depends_on": "create_job_card_based_on_batch_size",
"fieldname": "batch_size",
"fieldtype": "Int",
"label": "Batch Size",
"mandatory_depends_on": "create_job_card_based_on_batch_size"
},
{
"default": "0",
"fieldname": "create_job_card_based_on_batch_size",
"fieldtype": "Check",
"label": "Create Job Card based on Batch Size"
},
{
"default": "0",
"description": "Cost of poor quality operation",
"fieldname": "cost_of_poor_quality_operation",
"fieldtype": "Check",
"label": "Is COPQ Operation"
},
{
"collapsible": 1,
"fieldname": "job_card_section",
"fieldtype": "Section Break",
"label": "Job Card"
},
{
"fieldname": "column_break_6",
"fieldtype": "Column Break"
}
],
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-wrench",
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2016-11-07 05:28:27.462413",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-12-24 14:25:03.428303",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Operation",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 0,
"export": 1,
"if_owner": 0,
"import": 1,
"is_custom": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 0,
"role": "Manufacturing User",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
},
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 0,
"export": 1,
"if_owner": 0,
"import": 1,
"is_custom": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 1,
"role": "Manufacturing Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_seen": 0
"track_changes": 1
}

View File

@ -2,9 +2,35 @@
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import flt
from frappe.model.document import Document
class Operation(Document):
def validate(self):
if not self.description:
self.description = self.name
self.duplicate_sub_operation()
self.set_total_time()
def duplicate_sub_operation(self):
operation_list = []
for row in self.sub_operations:
if row.operation in operation_list:
frappe.throw(_("The operation {0} can not add multiple times")
.format(frappe.bold(row.operation)))
if self.name == row.operation:
frappe.throw(_("The operation {0} can not be the sub operation")
.format(frappe.bold(row.operation)))
operation_list.append(row.operation)
def set_total_time(self):
self.total_operation_time = 0.0
for row in self.sub_operations:
self.total_operation_time += row.time_in_mins

View File

@ -0,0 +1,8 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Sub Operation', {
// refresh: function(frm) {
// }
});

View File

@ -0,0 +1,51 @@
{
"actions": [],
"creation": "2020-12-07 15:39:47.488519",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"operation",
"time_in_mins",
"column_break_5",
"description"
],
"fields": [
{
"fieldname": "operation",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Operation",
"options": "Operation"
},
{
"description": "Time in mins",
"fieldname": "time_in_mins",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Operation Time"
},
{
"fieldname": "column_break_5",
"fieldtype": "Column Break"
},
{
"fieldname": "description",
"fieldtype": "Small Text",
"label": "Description"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-12-07 18:09:18.005578",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Sub Operation",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class SubOperation(Document):
pass

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestSubOperation(unittest.TestCase):
pass

View File

@ -141,8 +141,7 @@ frappe.ui.form.on("Work Order", {
}
if (frm.doc.docstatus === 1
&& frm.doc.operations && frm.doc.operations.length
&& frm.doc.qty != frm.doc.material_transferred_for_manufacturing) {
&& frm.doc.operations && frm.doc.operations.length) {
const not_completed = frm.doc.operations.filter(d => {
if(d.status != 'Completed') {

View File

@ -21,6 +21,13 @@
"produced_qty",
"sales_order",
"project",
"serial_no_and_batch_for_finished_good_section",
"has_serial_no",
"has_batch_no",
"column_break_17",
"serial_no",
"batch_size",
"batches",
"settings_section",
"allow_alternative_item",
"use_multi_level_bom",
@ -488,6 +495,54 @@
"fieldtype": "Float",
"label": "Lead Time",
"read_only": 1
},
{
"collapsible": 1,
"depends_on": "eval:!doc.__islocal",
"fieldname": "serial_no_and_batch_for_finished_good_section",
"fieldtype": "Section Break",
"label": "Serial No and Batch for Finished Good"
},
{
"fieldname": "column_break_17",
"fieldtype": "Column Break"
},
{
"default": "0",
"fetch_from": "production_item.has_serial_no",
"fieldname": "has_serial_no",
"fieldtype": "Check",
"label": "Has Serial No",
"read_only": 1
},
{
"default": "0",
"fetch_from": "production_item.has_batch_no",
"fieldname": "has_batch_no",
"fieldtype": "Check",
"label": "Has Batch No",
"read_only": 1
},
{
"depends_on": "has_serial_no",
"fieldname": "serial_no",
"fieldtype": "Small Text",
"label": "Serial Nos"
},
{
"default": "0",
"depends_on": "has_batch_no",
"fieldname": "batch_size",
"fieldtype": "Float",
"label": "Batch Size"
},
{
"depends_on": "has_batch_no",
"fieldname": "batches",
"fieldtype": "Table",
"label": "Batches",
"options": "Work Order Batch",
"read_only": 1
}
],
"icon": "fa fa-cogs",

View File

@ -19,6 +19,8 @@ from frappe.utils.csvutils import getlink
from erpnext.stock.utils import get_bin, validate_warehouse_company, get_latest_stock_qty
from erpnext.utilities.transaction_base import validate_uom_is_integer
from frappe.model.mapper import get_mapped_doc
from erpnext.stock.doctype.batch.batch import make_batch
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos, get_auto_serial_nos, auto_make_serial_nos
class OverProductionError(frappe.ValidationError): pass
class CapacityError(frappe.ValidationError): pass
@ -40,6 +42,7 @@ class WorkOrder(Document):
self.set_onload("overproduction_percentage", ms.overproduction_percentage_for_work_order)
def validate(self):
self.set("batches", [])
self.validate_production_item()
if self.bom_no:
validate_bom_no(self.production_item, self.bom_no)
@ -235,6 +238,9 @@ class WorkOrder(Document):
production_plan.run_method("update_produced_qty", produced_qty, self.production_plan_item)
def before_submit(self):
self.create_serial_no_batch_no()
def on_submit(self):
if not self.wip_warehouse:
frappe.throw(_("Work-in-Progress Warehouse is required before Submit"))
@ -266,6 +272,67 @@ class WorkOrder(Document):
self.update_planned_qty()
self.update_ordered_qty()
self.update_reserved_qty_for_production()
self.delete_auto_created_batch_and_serial_no()
def create_serial_no_batch_no(self):
if not (self.has_serial_no or self.has_batch_no): return
if not cint(frappe.db.get_single_value("Manufacturing Settings",
"make_serial_no_batch_from_work_order")): return
if self.has_batch_no:
self.set("batches", [])
self.create_batch_for_finished_good()
args = {"item_code": self.production_item}
if self.has_serial_no:
self.make_serial_nos(args)
def create_batch_for_finished_good(self):
total_qty = self.qty
if not self.batch_size:
self.batch_size = total_qty
while total_qty > 0:
qty = self.batch_size
if self.batch_size >= total_qty:
qty = total_qty
if total_qty > self.batch_size:
total_qty -= self.batch_size
else:
qty = total_qty
total_qty = 0
batch = make_batch(self.production_item)
self.append("batches", {
"batch_no": batch,
"qty": qty,
})
def delete_auto_created_batch_and_serial_no(self):
if self.serial_no:
for d in get_serial_nos(self.serial_no):
frappe.delete_doc("Serial No", d)
for row in self.batches:
batch_no = row.batch_no
row.db_set("batch_no", None)
frappe.delete_doc("Batch", batch_no)
def make_serial_nos(self, args):
serial_no_series = frappe.get_cached_value("Item", self.production_item, "serial_no_series")
if serial_no_series:
self.serial_no = get_auto_serial_nos(serial_no_series, self.qty)
elif self.serial_no:
args.update({"serial_no": self.serial_no, "actual_qty": self.qty, "batch_no": self.batch_no})
self.serial_no = auto_make_serial_nos(args)
serial_nos_length = len(get_serial_nos(self.serial_no))
if serial_nos_length != self.qty:
frappe.throw(_("{0} Serial Numbers required for Item {1}. You have provided {2}.")
.format(self.qty, self.production_item, serial_nos_length), SerialNoQtyError)
def create_job_card(self):
manufacturing_settings_doc = frappe.get_doc("Manufacturing Settings")
@ -273,15 +340,38 @@ class WorkOrder(Document):
enable_capacity_planning = not cint(manufacturing_settings_doc.disable_capacity_planning)
plan_days = cint(manufacturing_settings_doc.capacity_planning_for_days) or 30
for i, row in enumerate(self.operations):
self.set_operation_start_end_time(i, row)
for index, row in enumerate(self.operations):
qty = self.qty
i=0
while qty > 0:
i += 1
if not cint(frappe.db.get_value("Operation",
row.operation, "create_job_card_based_on_batch_size")):
row.batch_size = self.qty
job_card_qty = row.batch_size
if row.batch_size and qty >= row.batch_size:
qty -= row.batch_size
elif qty > 0:
job_card_qty = qty
if job_card_qty > 0:
self.prepare_data_for_job_card(row, job_card_qty, index,
plan_days, enable_capacity_planning)
planned_end_date = self.operations and self.operations[-1].planned_end_time
if planned_end_date:
self.db_set("planned_end_date", planned_end_date)
def prepare_data_for_job_card(self, row, job_card_qty, index, plan_days, enable_capacity_planning):
self.set_operation_start_end_time(index, row)
if not row.workstation:
frappe.throw(_("Row {0}: select the workstation against the operation {1}")
.format(row.idx, row.operation))
original_start_time = row.planned_start_time
job_card_doc = create_job_card(self, row,
job_card_doc = create_job_card(self, row, qty=job_card_qty,
enable_capacity_planning=enable_capacity_planning, auto_create=True)
if enable_capacity_planning and job_card_doc:
@ -295,10 +385,6 @@ class WorkOrder(Document):
row.db_update()
planned_end_date = self.operations and self.operations[-1].planned_end_time
if planned_end_date:
self.db_set("planned_end_date", planned_end_date)
def set_operation_start_end_time(self, idx, row):
"""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."""
@ -669,6 +755,15 @@ class WorkOrder(Document):
bom.set_bom_material_details()
return bom
def update_batch_qty(self):
if self.has_batch_no and self.batches:
for row in self.batches:
qty = frappe.get_all("Stock Entry Detail", fields = ["sum(transfer_qty)"],
filters = {"docstatus": 1, "batch_no": row.batch_no, "is_finished_item": 1}, as_list=1)
if qty:
frappe.db.set_value("Work Order Batch", row.name, "produced_qty", flt(qty[0][0]))
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_bom_operations(doctype, txt, searchfield, start, page_len, filters):
@ -826,6 +921,7 @@ def make_stock_entry(work_order_id, purpose, qty=None):
stock_entry.set_stock_entry_type()
stock_entry.get_items()
stock_entry.set_serial_no_batch_for_finished_good()
return stock_entry.as_dict()
@frappe.whitelist()

View File

@ -0,0 +1,49 @@
{
"actions": [],
"creation": "2021-01-04 16:42:39.347528",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"batch_no",
"qty",
"produced_qty"
],
"fields": [
{
"fieldname": "batch_no",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Batch No",
"options": "Batch"
},
{
"fieldname": "qty",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Qty",
"non_negative": 1
},
{
"default": "0",
"fieldname": "produced_qty",
"fieldtype": "Float",
"label": "Produced Qty",
"no_copy": 1,
"print_hide": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-01-05 10:57:07.278399",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Work Order Batch",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class WorkOrderBatch(Document):
pass

View File

@ -309,3 +309,10 @@ def validate_serial_no_with_batch(serial_nos, item_code):
message = "Serial Nos" if len(serial_nos) > 1 else "Serial No"
frappe.throw(_("There is no batch found against the {0}: {1}")
.format(message, serial_no_link))
def make_batch(item_code):
if frappe.db.get_value("Item", item_code, "has_batch_no"):
doc = frappe.new_doc("Batch")
doc.item = item_code
doc.save()
return doc.name

View File

@ -498,6 +498,7 @@ class StockEntry(StockController):
d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount"))
if not d.t_warehouse:
outgoing_items_cost += flt(d.basic_amount)
return outgoing_items_cost
def get_args_for_incoming_rate(self, item):
@ -854,6 +855,7 @@ class StockEntry(StockController):
pro_doc.run_method("update_work_order_qty")
if self.purpose == "Manufacture":
pro_doc.run_method("update_planned_qty")
pro_doc.update_batch_qty()
if not pro_doc.operations:
pro_doc.set_actual_dates()
@ -1076,8 +1078,7 @@ class StockEntry(StockController):
# in case of BOM
to_warehouse = item.get("default_warehouse")
self.add_to_stock_entry_detail({
item.name: {
args = {
"to_warehouse": to_warehouse,
"from_warehouse": "",
"qty": self.fg_completed_qty,
@ -1088,6 +1089,34 @@ class StockEntry(StockController):
"cost_center": item.get("buying_cost_center"),
"is_finished_item": 1
}
if self.work_order and self.pro_doc.batches:
self.set_batchwise_finished_goods(args, item)
else:
self.add_finisged_goods(args, item)
def set_batchwise_finished_goods(self, args, item):
qty = flt(self.fg_completed_qty)
for row in self.pro_doc.batches:
batch_qty = flt(row.qty) - flt(row.produced_qty)
if not batch_qty: continue
if qty <=0:
break
fg_qty = batch_qty
if batch_qty >= qty:
fg_qty = qty
qty -= batch_qty
args["qty"] = fg_qty
args["batch_no"] = row.batch_no
self.add_finisged_goods(args, item)
def add_finisged_goods(self, args, item):
self.add_to_stock_entry_detail({
item.name: args
}, bom_no = self.bom_no)
def get_bom_raw_materials(self, qty):
@ -1524,6 +1553,36 @@ class StockEntry(StockController):
material_requests.append(material_request)
frappe.db.set_value('Material Request', material_request, 'transfer_status', status)
def set_serial_no_batch_for_finished_good(self):
args = {}
if self.pro_doc.serial_no or self.pro_doc.batch_no:
self.get_serial_nos_for_fg(args)
for row in self.items:
if row.is_finished_item and row.item_code == self.pro_doc.production_item:
if args.get("serial_no"):
row.serial_no = '\n'.join(args["serial_no"][0: cint(row.qty)])
def get_serial_nos_for_fg(self, args):
fields = ["`tabStock Entry`.`name`", "`tabStock Entry Detail`.`qty`",
"`tabStock Entry Detail`.`serial_no`", "`tabStock Entry Detail`.`batch_no`"]
filters = [["Stock Entry","work_order","=",self.work_order], ["Stock Entry","purpose","=","Manufacture"],
["Stock Entry","docstatus","=",1], ["Stock Entry Detail","item_code","=",self.pro_doc.production_item]]
stock_entries = frappe.get_all("Stock Entry", fields=fields, filters=filters)
if self.pro_doc.serial_no:
args["serial_no"] = self.get_available_serial_nos(stock_entries)
def get_available_serial_nos(self, stock_entries):
used_serial_nos = []
for row in stock_entries:
if row.serial_no:
used_serial_nos.extend(get_serial_nos(row.serial_no))
return sorted(list(set(get_serial_nos(self.pro_doc.serial_no)) - set(used_serial_nos)))
@frappe.whitelist()
def move_sample_to_retention_warehouse(company, items):
if isinstance(items, string_types):