Merge pull request #24523 from rohitwaghchaure/sub-operation-new-branch

Feat: Job Card Enhancements
This commit is contained in:
rohitwaghchaure 2021-06-24 18:19:42 +05:30 committed by GitHub
commit 27797ffa46
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 1484 additions and 380 deletions

View File

@ -71,7 +71,6 @@ frappe.ui.form.on("BOM", {
refresh: function(frm) { refresh: function(frm) {
frm.toggle_enable("item", frm.doc.__islocal); frm.toggle_enable("item", frm.doc.__islocal);
toggle_operations(frm);
frm.set_indicator_formatter('item_code', frm.set_indicator_formatter('item_code',
function(doc) { function(doc) {
@ -651,15 +650,8 @@ frappe.ui.form.on("BOM Item", "items_remove", function(frm) {
erpnext.bom.calculate_total(frm.doc); erpnext.bom.calculate_total(frm.doc);
}); });
var toggle_operations = function(frm) {
frm.toggle_display("operations_section", cint(frm.doc.with_operations) == 1);
frm.toggle_display("transfer_material_against", cint(frm.doc.with_operations) == 1);
frm.toggle_reqd("transfer_material_against", cint(frm.doc.with_operations) == 1);
};
frappe.ui.form.on("BOM", "with_operations", function(frm) { frappe.ui.form.on("BOM", "with_operations", function(frm) {
if(!cint(frm.doc.with_operations)) { if(!cint(frm.doc.with_operations)) {
frm.set_value("operations", []); frm.set_value("operations", []);
} }
toggle_operations(frm);
}); });

View File

@ -193,6 +193,7 @@
}, },
{ {
"default": "Work Order", "default": "Work Order",
"depends_on": "with_operations",
"fieldname": "transfer_material_against", "fieldname": "transfer_material_against",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Transfer Material Against", "label": "Transfer Material Against",
@ -235,6 +236,7 @@
{ {
"fieldname": "operations_section", "fieldname": "operations_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hide_border": 1,
"oldfieldtype": "Section Break" "oldfieldtype": "Section Break"
}, },
{ {
@ -245,6 +247,7 @@
"options": "Routing" "options": "Routing"
}, },
{ {
"depends_on": "with_operations",
"fieldname": "operations", "fieldname": "operations",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Operations", "label": "Operations",
@ -517,7 +520,7 @@
"image_field": "image", "image_field": "image",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-05-21 12:29:32.634952", "modified": "2021-03-16 12:25:09.081968",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "BOM", "name": "BOM",

View File

@ -590,7 +590,7 @@ class BOM(WebsiteGenerator):
self.get_routing() self.get_routing()
def validate_operations(self): def validate_operations(self):
if self.with_operations and not self.get('operations'): if self.with_operations and not self.get('operations') and self.docstatus == 1:
frappe.throw(_("Operations cannot be left blank")) frappe.throw(_("Operations cannot be left blank"))
if self.with_operations: if self.with_operations:

View File

@ -13,10 +13,10 @@
"col_break1", "col_break1",
"hour_rate", "hour_rate",
"time_in_mins", "time_in_mins",
"batch_size",
"operating_cost", "operating_cost",
"base_hour_rate", "base_hour_rate",
"base_operating_cost", "base_operating_cost",
"batch_size",
"image" "image"
], ],
"fields": [ "fields": [
@ -61,6 +61,8 @@
}, },
{ {
"description": "In minutes", "description": "In minutes",
"fetch_from": "operation.total_operation_time",
"fetch_if_empty": 1,
"fieldname": "time_in_mins", "fieldname": "time_in_mins",
"fieldtype": "Float", "fieldtype": "Float",
"in_list_view": 1, "in_list_view": 1,
@ -104,7 +106,8 @@
"label": "Image" "label": "Image"
}, },
{ {
"default": "1", "fetch_from": "operation.batch_size",
"fetch_if_empty": 1,
"fieldname": "batch_size", "fieldname": "batch_size",
"fieldtype": "Int", "fieldtype": "Int",
"label": "Batch Size" "label": "Batch Size"
@ -120,7 +123,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-10-13 18:14:10.018774", "modified": "2021-01-12 14:48:09.596843",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "BOM Operation", "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) { refresh: function(frm) {
@ -31,6 +41,10 @@ frappe.ui.form.on('Job Card', {
} }
} }
if (frm.doc.docstatus == 1 && !frm.doc.is_corrective_job_card) {
frm.trigger('setup_corrective_job_card');
}
frm.set_query("quality_inspection", function() { frm.set_query("quality_inspection", function() {
return { return {
query: "erpnext.stock.doctype.quality_inspection.quality_inspection.quality_inspection_query", query: "erpnext.stock.doctype.quality_inspection.quality_inspection.quality_inspection_query",
@ -43,12 +57,62 @@ frappe.ui.form.on('Job Card', {
frm.trigger("toggle_operation_number"); frm.trigger("toggle_operation_number");
if (frm.doc.docstatus == 0 && (frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity) if (frm.doc.docstatus == 0 && !frm.is_new() &&
(frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity)
&& (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) { && (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) {
frm.trigger("prepare_timer_buttons"); frm.trigger("prepare_timer_buttons");
} }
}, },
setup_corrective_job_card: function(frm) {
frm.add_custom_button(__('Corrective Job Card'), () => {
let operations = frm.doc.sub_operations.map(d => d.sub_operation).concat(frm.doc.operation);
let fields = [
{
fieldtype: 'Link', label: __('Corrective Operation'), options: 'Operation',
fieldname: 'operation', get_query() {
return {
filters: {
"is_corrective_operation": 1
}
};
}
}, {
fieldtype: 'Link', label: __('For Operation'), options: 'Operation',
fieldname: 'for_operation', get_query() {
return {
filters: {
"name": ["in", operations]
}
};
}
}
];
frappe.prompt(fields, d => {
frm.events.make_corrective_job_card(frm, d.operation, d.for_operation);
}, __("Select Corrective Operation"));
}, __('Make'));
},
make_corrective_job_card: function(frm, operation, for_operation) {
frappe.call({
method: 'erpnext.manufacturing.doctype.job_card.job_card.make_corrective_job_card',
args: {
source_name: frm.doc.name,
operation: operation,
for_operation: for_operation
},
callback: function(r) {
if (r.message) {
frappe.model.sync(r.message);
frappe.set_route("Form", r.message.doctype, r.message.name);
}
}
});
},
operation: function(frm) { operation: function(frm) {
frm.trigger("toggle_operation_number"); frm.trigger("toggle_operation_number");
@ -97,101 +161,105 @@ frappe.ui.form.on('Job Card', {
prepare_timer_buttons: function(frm) { prepare_timer_buttons: function(frm) {
frm.trigger("make_dashboard"); frm.trigger("make_dashboard");
if (!frm.doc.job_started) {
frm.add_custom_button(__("Start"), () => { if (!frm.doc.started_time && !frm.doc.current_time) {
if (!frm.doc.employee) { frm.add_custom_button(__("Start Job"), () => {
frappe.prompt({fieldtype: 'Link', label: __('Employee'), options: "Employee", if ((frm.doc.employee && !frm.doc.employee.length) || !frm.doc.employee) {
fieldname: 'employee'}, d => { frappe.prompt({fieldtype: 'Table MultiSelect', label: __('Select Employees'),
if (d.employee) { options: "Job Card Time Log", fieldname: 'employees'}, d => {
frm.set_value("employee", d.employee); frm.events.start_job(frm, "Work In Progress", d.employees);
} else { }, __("Assign Job to Employee"));
frm.events.start_job(frm);
}
}, __("Enter Value"), __("Start"));
} else { } else {
frm.events.start_job(frm); frm.events.start_job(frm, "Work In Progress", frm.doc.employee);
} }
}).addClass("btn-primary"); }).addClass("btn-primary");
} else if (frm.doc.status == "On Hold") { } else if (frm.doc.status == "On Hold") {
frm.add_custom_button(__("Resume"), () => { frm.add_custom_button(__("Resume Job"), () => {
frappe.flags.resume_job = 1; frm.events.start_job(frm, "Resume Job", frm.doc.employee);
frm.events.start_job(frm);
}).addClass("btn-primary"); }).addClass("btn-primary");
} else { } else {
frm.add_custom_button(__("Pause"), () => { frm.add_custom_button(__("Pause Job"), () => {
frappe.flags.pause_job = 1; frm.events.complete_job(frm, "On Hold");
frm.set_value("status", "On Hold");
frm.events.complete_job(frm);
}); });
frm.add_custom_button(__("Complete"), () => { frm.add_custom_button(__("Complete Job"), () => {
let completed_time = frappe.datetime.now_datetime(); var sub_operations = frm.doc.sub_operations;
frm.trigger("hide_timer");
if (frm.doc.for_quantity) { let set_qty = true;
if (sub_operations && sub_operations.length > 1) {
set_qty = false;
let last_op_row = sub_operations[sub_operations.length - 2];
if (last_op_row.status == 'Complete') {
set_qty = true;
}
}
if (set_qty) {
frappe.prompt({fieldtype: 'Float', label: __('Completed Quantity'), frappe.prompt({fieldtype: 'Float', label: __('Completed Quantity'),
fieldname: 'qty', reqd: 1, default: frm.doc.for_quantity}, data => { fieldname: 'qty', default: frm.doc.for_quantity}, data => {
frm.events.complete_job(frm, completed_time, data.qty); frm.events.complete_job(frm, "Complete", data.qty);
}, __("Enter Value"), __("Complete")); }, __("Enter Value"));
} else { } else {
frm.events.complete_job(frm, completed_time, 0); frm.events.complete_job(frm, "Complete", 0.0);
} }
}).addClass("btn-primary"); }).addClass("btn-primary");
} }
}, },
start_job: function(frm) { start_job: function(frm, status, employee) {
let row = frappe.model.add_child(frm.doc, 'Job Card Time Log', 'time_logs'); const args = {
row.from_time = frappe.datetime.now_datetime(); job_card_id: frm.doc.name,
frm.set_value('job_started', 1); start_time: frappe.datetime.now_datetime(),
frm.set_value('started_time' , row.from_time); employees: employee,
frm.set_value("status", "Work In Progress"); status: status
};
if (!frappe.flags.resume_job) { frm.events.make_time_log(frm, args);
frm.set_value('current_time' , 0);
}
frm.save();
}, },
complete_job: function(frm, completed_time, completed_qty) { complete_job: function(frm, status, completed_qty) {
frm.doc.time_logs.forEach(d => { const args = {
if (d.from_time && !d.to_time) { job_card_id: frm.doc.name,
d.to_time = completed_time || frappe.datetime.now_datetime(); complete_time: frappe.datetime.now_datetime(),
d.completed_qty = completed_qty || 0; status: status,
completed_qty: completed_qty
};
frm.events.make_time_log(frm, args);
},
if(frappe.flags.pause_job) { make_time_log: function(frm, args) {
let currentIncrement = moment(d.to_time).diff(moment(d.from_time),"seconds") || 0; frm.events.update_sub_operation(frm, args);
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);
}
frm.save(); frappe.call({
method: "erpnext.manufacturing.doctype.job_card.job_card.make_time_log",
args: {
args: args
},
freeze: true,
callback: function () {
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) { validate: function(frm) {
if ((!frm.doc.time_logs || !frm.doc.time_logs.length) && frm.doc.started_time) { if ((!frm.doc.time_logs || !frm.doc.time_logs.length) && frm.doc.started_time) {
frm.trigger("reset_timer"); frm.trigger("reset_timer");
} }
}, },
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) { reset_timer: function(frm) {
frm.set_value('started_time' , ''); frm.set_value('started_time' , '');
frm.set_value('job_started', 0);
frm.set_value('current_time' , 0);
}, },
make_dashboard: function(frm) { make_dashboard: function(frm) {
@ -297,7 +365,6 @@ frappe.ui.form.on('Job Card Time Log', {
}, },
to_time: function(frm) { to_time: function(frm) {
frm.set_value('job_started', 0);
frm.set_value('started_time', ''); frm.set_value('started_time', '');
} }
}) })

View File

@ -9,38 +9,49 @@
"naming_series", "naming_series",
"work_order", "work_order",
"bom_no", "bom_no",
"workstation",
"operation",
"operation_row_number",
"column_break_4", "column_break_4",
"posting_date", "posting_date",
"company", "company",
"remarks",
"production_section", "production_section",
"production_item", "production_item",
"item_name", "item_name",
"for_quantity", "for_quantity",
"quality_inspection", "serial_no",
"wip_warehouse",
"column_break_12", "column_break_12",
"employee", "wip_warehouse",
"employee_name", "quality_inspection",
"status",
"project", "project",
"batch_no",
"operation_section_section",
"operation",
"operation_row_number",
"column_break_18",
"workstation",
"employee",
"section_break_21",
"sub_operations",
"timing_detail", "timing_detail",
"time_logs", "time_logs",
"section_break_13", "section_break_13",
"total_completed_qty", "total_completed_qty",
"total_time_in_mins",
"column_break_15", "column_break_15",
"total_time_in_mins",
"section_break_8", "section_break_8",
"items", "items",
"corrective_operation_section",
"for_job_card",
"is_corrective_job_card",
"column_break_33",
"hour_rate",
"for_operation",
"more_information", "more_information",
"operation_id", "operation_id",
"sequence_id", "sequence_id",
"transferred_qty", "transferred_qty",
"requested_qty", "requested_qty",
"status",
"column_break_20", "column_break_20",
"remarks",
"barcode", "barcode",
"job_started", "job_started",
"started_time", "started_time",
@ -117,13 +128,6 @@
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Timing Detail" "label": "Timing Detail"
}, },
{
"fieldname": "employee",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Employee",
"options": "Employee"
},
{ {
"allow_bulk_edit": 1, "allow_bulk_edit": 1,
"fieldname": "time_logs", "fieldname": "time_logs",
@ -133,9 +137,11 @@
}, },
{ {
"fieldname": "section_break_13", "fieldname": "section_break_13",
"fieldtype": "Section Break" "fieldtype": "Section Break",
"hide_border": 1
}, },
{ {
"default": "0",
"fieldname": "total_completed_qty", "fieldname": "total_completed_qty",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Total Completed Qty", "label": "Total Completed Qty",
@ -160,8 +166,7 @@
"fieldname": "items", "fieldname": "items",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Items", "label": "Items",
"options": "Job Card Item", "options": "Job Card Item"
"read_only": 1
}, },
{ {
"collapsible": 1, "collapsible": 1,
@ -251,12 +256,7 @@
"reqd": 1 "reqd": 1
}, },
{ {
"fetch_from": "employee.employee_name", "collapsible": 1,
"fieldname": "employee_name",
"fieldtype": "Read Only",
"label": "Employee Name"
},
{
"fieldname": "production_section", "fieldname": "production_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Production" "label": "Production"
@ -314,11 +314,89 @@
"label": "Quality Inspection", "label": "Quality Inspection",
"no_copy": 1, "no_copy": 1,
"options": "Quality Inspection" "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
},
{
"depends_on": "is_corrective_job_card",
"fieldname": "hour_rate",
"fieldtype": "Currency",
"label": "Hour Rate"
},
{
"collapsible": 1,
"depends_on": "is_corrective_job_card",
"fieldname": "corrective_operation_section",
"fieldtype": "Section Break",
"label": "Corrective Operation"
},
{
"default": "0",
"fieldname": "is_corrective_job_card",
"fieldtype": "Check",
"label": "Is Corrective Job Card",
"read_only": 1
},
{
"fieldname": "column_break_33",
"fieldtype": "Column Break"
},
{
"fieldname": "for_job_card",
"fieldtype": "Link",
"label": "For Job Card",
"options": "Job Card",
"read_only": 1
},
{
"fetch_from": "for_job_card.operation",
"fetch_if_empty": 1,
"fieldname": "for_operation",
"fieldtype": "Link",
"label": "For Operation",
"options": "Operation"
},
{
"fieldname": "employee",
"fieldtype": "Table MultiSelect",
"label": "Employee",
"options": "Job Card Time Log"
},
{
"fieldname": "serial_no",
"fieldtype": "Small Text",
"label": "Serial No"
},
{
"fieldname": "batch_no",
"fieldtype": "Link",
"label": "Batch No",
"options": "Batch"
} }
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-11-19 18:26:50.531664", "modified": "2021-03-16 15:59:32.766484",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Job Card", "name": "Job Card",

View File

@ -5,11 +5,12 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
import datetime import datetime
import json
from frappe import _, bold from frappe import _, bold
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import (flt, cint, time_diff_in_hours, get_datetime, getdate, 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 from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations
@ -25,10 +26,21 @@ class JobCard(Document):
self.set_status() self.set_status()
self.validate_operation_id() self.validate_operation_id()
self.validate_sequence_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", "idx"]):
row.status = "Pending"
row.sub_operation = row.operation
self.append("sub_operations", row)
def validate_time_logs(self): def validate_time_logs(self):
self.total_completed_qty = 0.0
self.total_time_in_mins = 0.0 self.total_time_in_mins = 0.0
self.total_completed_qty = 0.0
if self.get('time_logs'): if self.get('time_logs'):
for d in self.get('time_logs'): for d in self.get('time_logs'):
@ -44,11 +56,14 @@ class JobCard(Document):
d.time_in_mins = time_diff_in_hours(d.to_time, d.from_time) * 60 d.time_in_mins = time_diff_in_hours(d.to_time, d.from_time) * 60
self.total_time_in_mins += d.time_in_mins self.total_time_in_mins += d.time_in_mins
if d.completed_qty: if d.completed_qty and not self.sub_operations:
self.total_completed_qty += d.completed_qty self.total_completed_qty += d.completed_qty
self.total_completed_qty = flt(self.total_completed_qty, self.precision("total_completed_qty")) self.total_completed_qty = flt(self.total_completed_qty, self.precision("total_completed_qty"))
for row in self.sub_operations:
self.total_completed_qty += row.completed_qty
def get_overlap_for(self, args, check_next_available_slot=False): def get_overlap_for(self, args, check_next_available_slot=False):
production_capacity = 1 production_capacity = 1
@ -57,7 +72,7 @@ class JobCard(Document):
self.workstation, 'production_capacity') or 1 self.workstation, 'production_capacity') or 1
validate_overlap_for = " and jc.workstation = %(workstation)s " validate_overlap_for = " and jc.workstation = %(workstation)s "
if self.employee: if args.get("employee"):
# override capacity for employee # override capacity for employee
production_capacity = 1 production_capacity = 1
validate_overlap_for = " and jc.employee = %(employee)s " validate_overlap_for = " and jc.employee = %(employee)s "
@ -80,7 +95,7 @@ class JobCard(Document):
"to_time": args.to_time, "to_time": args.to_time,
"name": args.name or "No Name", "name": args.name or "No Name",
"parent": args.parent or "No Name", "parent": args.parent or "No Name",
"employee": self.employee, "employee": args.get("employee"),
"workstation": self.workstation "workstation": self.workstation
}, as_dict=True) }, as_dict=True)
@ -158,6 +173,100 @@ class JobCard(Document):
row.planned_start_time = datetime.datetime.combine(start_date, row.planned_start_time = datetime.datetime.combine(start_date,
get_time(workstation_doc.working_hours[0].start_time)) get_time(workstation_doc.working_hours[0].start_time))
def add_time_log(self, args):
last_row = []
employees = args.employees
if isinstance(employees, str):
employees = json.loads(employees)
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"):
for row in self.time_logs:
if not row.to_time:
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"):
for name in employees:
self.append("time_logs", {
"from_time": get_datetime(args.get("start_time")),
"employee": name.get('employee'),
"operation": args.get("sub_operation"),
"completed_qty": 0.0
})
if not self.employee:
self.set_employees(employees)
if self.status == "On Hold":
self.current_time = time_diff_in_seconds(last_row.to_time, last_row.from_time)
self.save()
def set_employees(self, employees):
for name in employees:
self.append('employee', {
'employee': name.get('employee'),
'completed_qty': 0.0
})
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_qty":0.0, "completed_time": 0.0, "employee": []}))
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 self.status == 'On Hold':
op_row.status = 'Pause'
op_row.employee.append(time_log.employee)
if time_log.time_in_mins:
op_row.completed_time += time_log.time_in_mins
op_row.completed_qty += time_log.completed_qty
for row in self.sub_operations:
operation_deatils = operation_wise_completed_time.get(row.sub_operation)
if operation_deatils:
if row.status != 'Complete':
row.status = operation_deatils.status
row.completed_time = operation_deatils.completed_time
if operation_deatils.employee:
row.completed_time = row.completed_time / len(set(operation_deatils.employee))
if operation_deatils.completed_qty:
row.completed_qty = operation_deatils.completed_qty / len(set(operation_deatils.employee))
else:
row.status = 'Pending'
row.completed_time = 0.0
row.completed_qty = 0.0
def update_time_logs(self, row): def update_time_logs(self, row):
self.append("time_logs", { self.append("time_logs", {
"from_time": row.planned_start_time, "from_time": row.planned_start_time,
@ -182,15 +291,18 @@ class JobCard(Document):
if self.get('operation') == d.operation: if self.get('operation') == d.operation:
self.append('items', { self.append('items', {
'item_code': d.item_code, "item_code": d.item_code,
'source_warehouse': d.source_warehouse, "source_warehouse": d.source_warehouse,
'uom': frappe.db.get_value("Item", d.item_code, 'stock_uom'), "uom": frappe.db.get_value("Item", d.item_code, 'stock_uom'),
'item_name': d.item_name, "item_name": d.item_name,
'description': d.description, "description": d.description,
'required_qty': (d.required_qty * flt(self.for_quantity)) / doc.qty "required_qty": (d.required_qty * flt(self.for_quantity)) / doc.qty,
"rate": d.rate,
"amount": d.amount
}) })
def on_submit(self): def on_submit(self):
self.validate_transfer_qty()
self.validate_job_card() self.validate_job_card()
self.update_work_order() self.update_work_order()
self.set_transferred_qty() self.set_transferred_qty()
@ -199,7 +311,16 @@ class JobCard(Document):
self.update_work_order() self.update_work_order()
self.set_transferred_qty() self.set_transferred_qty()
def validate_transfer_qty(self):
if self.items and self.transferred_qty < self.for_quantity:
frappe.throw(_('Materials needs to be transferred to the work in progress warehouse for the job card {0}')
.format(self.name))
def validate_job_card(self): def validate_job_card(self):
if self.work_order and frappe.get_cached_value('Work Order', self.work_order, 'status') == 'Stopped':
frappe.throw(_("Transaction not allowed against stopped Work Order {0}")
.format(get_link_to_form('Work Order', self.work_order)))
if not self.time_logs: if not self.time_logs:
frappe.throw(_("Time logs are required for {0} {1}") frappe.throw(_("Time logs are required for {0} {1}")
.format(bold("Job Card"), get_link_to_form("Job Card", self.name))) .format(bold("Job Card"), get_link_to_form("Job Card", self.name)))
@ -215,6 +336,10 @@ class JobCard(Document):
if not self.work_order: if not self.work_order:
return return
if self.is_corrective_job_card and not cint(frappe.db.get_single_value('Manufacturing Settings',
'add_corrective_operation_cost_in_finished_good_valuation')):
return
for_quantity, time_in_mins = 0, 0 for_quantity, time_in_mins = 0, 0
from_time_list, to_time_list = [], [] from_time_list, to_time_list = [], []
@ -225,10 +350,24 @@ class JobCard(Document):
time_in_mins = flt(data[0].time_in_mins) time_in_mins = flt(data[0].time_in_mins)
wo = frappe.get_doc('Work Order', self.work_order) wo = frappe.get_doc('Work Order', self.work_order)
if self.operation_id:
if self.is_corrective_job_card:
self.update_corrective_in_work_order(wo)
elif self.operation_id:
self.validate_produced_quantity(for_quantity, wo) self.validate_produced_quantity(for_quantity, wo)
self.update_work_order_data(for_quantity, time_in_mins, wo) self.update_work_order_data(for_quantity, time_in_mins, wo)
def update_corrective_in_work_order(self, wo):
wo.corrective_operation_cost = 0.0
for row in frappe.get_all('Job Card', fields = ['total_time_in_mins', 'hour_rate'],
filters = {'is_corrective_job_card': 1, 'docstatus': 1, 'work_order': self.work_order}):
wo.corrective_operation_cost += flt(row.total_time_in_mins) * flt(row.hour_rate)
wo.calculate_operating_cost()
wo.flags.ignore_validate_update_after_submit = True
wo.save()
def validate_produced_quantity(self, for_quantity, wo): def validate_produced_quantity(self, for_quantity, wo):
if self.docstatus < 2: return if self.docstatus < 2: return
@ -248,8 +387,8 @@ class JobCard(Document):
min(from_time) as start_time, max(to_time) as end_time min(from_time) as start_time, max(to_time) as end_time
FROM `tabJob Card` jc, `tabJob Card Time Log` jctl FROM `tabJob Card` jc, `tabJob Card Time Log` jctl
WHERE WHERE
jctl.parent = jc.name and jc.work_order = %s jctl.parent = jc.name and jc.work_order = %s and jc.operation_id = %s
and jc.operation_id = %s and jc.docstatus = 1 and jc.docstatus = 1 and IFNULL(jc.is_corrective_job_card, 0) = 0
""", (self.work_order, self.operation_id), as_dict=1) """, (self.work_order, self.operation_id), as_dict=1)
for data in wo.operations: for data in wo.operations:
@ -271,7 +410,8 @@ class JobCard(Document):
def get_current_operation_data(self): def get_current_operation_data(self):
return frappe.get_all('Job Card', return frappe.get_all('Job Card',
fields = ["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"], fields = ["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"],
filters = {"docstatus": 1, "work_order": self.work_order, "operation_id": self.operation_id}) filters = {"docstatus": 1, "work_order": self.work_order, "operation_id": self.operation_id,
"is_corrective_job_card": 0})
def set_transferred_qty_in_job_card(self, ste_doc): def set_transferred_qty_in_job_card(self, ste_doc):
for row in ste_doc.items: for row in ste_doc.items:
@ -354,7 +494,11 @@ class JobCard(Document):
.format(bold(self.operation), work_order), OperationMismatchError) .format(bold(self.operation), work_order), OperationMismatchError)
def validate_sequence_id(self): def validate_sequence_id(self):
if not (self.work_order and self.sequence_id): return if self.is_corrective_job_card:
return
if not (self.work_order and self.sequence_id):
return
current_operation_qty = 0.0 current_operation_qty = 0.0
data = self.get_current_operation_data() data = self.get_current_operation_data()
@ -376,6 +520,17 @@ class JobCard(Document):
frappe.throw(_("{0}, complete the operation {1} before the operation {2}.") frappe.throw(_("{0}, complete the operation {1} before the operation {2}.")
.format(message, bold(row.operation), bold(self.operation)), OperationSequenceError) .format(message, bold(row.operation), bold(self.operation)), OperationSequenceError)
@frappe.whitelist()
def make_time_log(args):
if isinstance(args, str):
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() @frappe.whitelist()
def get_operation_details(work_order, operation): def get_operation_details(work_order, operation):
if work_order and operation: if work_order and operation:
@ -511,3 +666,28 @@ def get_job_details(start, end, filters=None):
events.append(job_card_data) events.append(job_card_data)
return events return events
@frappe.whitelist()
def make_corrective_job_card(source_name, operation=None, for_operation=None, target_doc=None):
def set_missing_values(source, target):
target.is_corrective_job_card = 1
target.operation = operation
target.for_operation = for_operation
target.set('time_logs', [])
target.set('employee', [])
target.set('items', [])
target.get_sub_operations()
target.get_required_items()
target.validate_time_logs()
doclist = get_mapped_doc("Job Card", source_name, {
"Job Card": {
"doctype": "Job Card",
"field_map": {
"name": "for_job_card",
},
}
}, target_doc, set_missing_values)
return doclist

View File

@ -25,8 +25,7 @@
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
"label": "Item Code", "label": "Item Code",
"options": "Item", "options": "Item"
"read_only": 1
}, },
{ {
"fieldname": "source_warehouse", "fieldname": "source_warehouse",
@ -67,8 +66,7 @@
"fieldname": "required_qty", "fieldname": "required_qty",
"fieldtype": "Float", "fieldtype": "Float",
"in_list_view": 1, "in_list_view": 1,
"label": "Required Qty", "label": "Required Qty"
"read_only": 1
}, },
{ {
"fieldname": "column_break_9", "fieldname": "column_break_9",
@ -107,7 +105,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-02-11 13:50:13.804108", "modified": "2021-04-22 18:50:00.003444",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Job Card Item", "name": "Job Card Item",

View File

@ -0,0 +1,59 @@
{
"actions": [],
"creation": "2020-12-07 16:58:38.449041",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"sub_operation",
"completed_time",
"status",
"completed_qty"
],
"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
},
{
"fieldname": "completed_qty",
"fieldtype": "Float",
"label": "Completed Qty",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-03-16 18:24:35.399593",
"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", "creation": "2019-03-08 23:56:43.187569",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"employee",
"from_time", "from_time",
"to_time", "to_time",
"column_break_2", "column_break_2",
"time_in_mins", "time_in_mins",
"completed_qty" "completed_qty",
"operation"
], ],
"fields": [ "fields": [
{ {
@ -41,10 +44,27 @@
"in_list_view": 1, "in_list_view": 1,
"label": "Completed Qty", "label": "Completed Qty",
"reqd": 1 "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, "istable": 1,
"modified": "2019-12-03 12:56:02.285448", "links": [],
"modified": "2020-12-23 14:30:00.970916",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Job Card Time Log", "name": "Job Card Time Log",

View File

@ -26,7 +26,10 @@
"column_break_16", "column_break_16",
"overproduction_percentage_for_work_order", "overproduction_percentage_for_work_order",
"other_settings_section", "other_settings_section",
"update_bom_costs_automatically" "update_bom_costs_automatically",
"add_corrective_operation_cost_in_finished_good_valuation",
"column_break_23",
"make_serial_no_batch_from_work_order"
], ],
"fields": [ "fields": [
{ {
@ -155,13 +158,30 @@
{ {
"fieldname": "column_break_5", "fieldname": "column_break_5",
"fieldtype": "Column Break" "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"
},
{
"default": "0",
"fieldname": "add_corrective_operation_cost_in_finished_good_valuation",
"fieldtype": "Check",
"label": "Add Corrective Operation Cost in Finished Good Valuation"
} }
], ],
"icon": "icon-wrench", "icon": "icon-wrench",
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2020-10-13 10:55:43.996581", "modified": "2021-03-16 15:54:38.967341",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Manufacturing Settings", "name": "Manufacturing Settings",

View File

@ -2,7 +2,13 @@
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on('Operation', { frappe.ui.form.on('Operation', {
refresh: function(frm) { setup: function(frm) {
frm.set_query('operation', 'sub_operations', function() {
return {
filters: {
'name': ['not in', [frm.doc.name]]
}
};
});
} }
}); });

View File

@ -1,167 +1,132 @@
{ {
"allow_copy": 0, "actions": [],
"allow_import": 1, "allow_import": 1,
"allow_rename": 1, "allow_rename": 1,
"autoname": "Prompt", "autoname": "Prompt",
"beta": 0,
"creation": "2014-11-07 16:20:30.683186", "creation": "2014-11-07 16:20:30.683186",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "Setup", "document_type": "Setup",
"editable_grid": 0,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [
"workstation",
"data_2",
"is_corrective_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": [ "fields": [
{ {
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "workstation", "fieldname": "workstation",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Default Workstation", "label": "Default Workstation",
"length": 0, "options": "Workstation"
"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
}, },
{ {
"allow_on_submit": 0, "collapsible": 1,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_4", "fieldname": "section_break_4",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "label": "Operation Description"
"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
}, },
{ {
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "description", "fieldname": "description",
"fieldtype": "Text", "fieldtype": "Text",
"hidden": 0, "label": "Description"
"ignore_user_permissions": 0, },
"ignore_xss_filter": 0, {
"in_filter": 0, "collapsible": 1,
"in_list_view": 0, "fieldname": "sub_operations_section",
"in_standard_filter": 0, "fieldtype": "Section Break",
"label": "Description", "label": "Sub Operations"
"length": 0, },
"no_copy": 0, {
"permlevel": 0, "fieldname": "sub_operations",
"precision": "", "fieldtype": "Table",
"print_hide": 0, "options": "Sub Operation"
"print_hide_if_no_value": 0, },
"read_only": 0, {
"remember_last_selected_value": 0, "description": "Time in mins.",
"report_hide": 0, "fieldname": "total_operation_time",
"reqd": 0, "fieldtype": "Float",
"search_index": 0, "label": "Total Operation Time",
"set_only_once": 0, "read_only": 1
"unique": 0 },
{
"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"
},
{
"collapsible": 1,
"fieldname": "job_card_section",
"fieldtype": "Section Break",
"label": "Job Card"
},
{
"fieldname": "column_break_6",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "is_corrective_operation",
"fieldtype": "Check",
"label": "Is Corrective Operation"
} }
], ],
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-wrench", "icon": "fa fa-wrench",
"idx": 0, "index_web_pages_for_search": 1,
"image_view": 0, "links": [],
"in_create": 0, "modified": "2021-01-12 15:09:23.593338",
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2016-11-07 05:28:27.462413",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Operation", "name": "Operation",
"name_case": "",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 0,
"export": 1, "export": 1,
"if_owner": 0,
"import": 1, "import": 1,
"is_custom": 0,
"permlevel": 0,
"print": 0,
"read": 1, "read": 1,
"report": 0,
"role": "Manufacturing User", "role": "Manufacturing User",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 0,
"write": 1 "write": 1
}, },
{ {
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 0,
"export": 1, "export": 1,
"if_owner": 0,
"import": 1, "import": 1,
"is_custom": 0,
"permlevel": 0,
"print": 0,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "Manufacturing Manager", "role": "Manufacturing Manager",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 0,
"write": 1 "write": 1
} }
], ],
"quick_entry": 1, "quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_seen": 0 "track_changes": 1
} }

View File

@ -2,9 +2,34 @@
# For license information, please see license.txt # For license information, please see license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
class Operation(Document): class Operation(Document):
def validate(self): def validate(self):
if not self.description: if not self.description:
self.description = self.name 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 if (frm.doc.docstatus === 1
&& frm.doc.operations && frm.doc.operations.length && frm.doc.operations && frm.doc.operations.length) {
&& frm.doc.qty != frm.doc.material_transferred_for_manufacturing) {
const not_completed = frm.doc.operations.filter(d => { const not_completed = frm.doc.operations.filter(d => {
if(d.status != 'Completed') { if(d.status != 'Completed') {
@ -190,35 +189,41 @@ frappe.ui.form.on("Work Order", {
const dialog = frappe.prompt({fieldname: 'operations', fieldtype: 'Table', label: __('Operations'), const dialog = frappe.prompt({fieldname: 'operations', fieldtype: 'Table', label: __('Operations'),
fields: [ fields: [
{ {
fieldtype:'Link', fieldtype: 'Link',
fieldname:'operation', fieldname: 'operation',
label: __('Operation'), label: __('Operation'),
read_only:1, read_only: 1,
in_list_view:1 in_list_view: 1
}, },
{ {
fieldtype:'Link', fieldtype: 'Link',
fieldname:'workstation', fieldname: 'workstation',
label: __('Workstation'), label: __('Workstation'),
read_only:1, read_only: 1,
in_list_view:1 in_list_view: 1
}, },
{ {
fieldtype:'Data', fieldtype: 'Data',
fieldname:'name', fieldname: 'name',
label: __('Operation Id') label: __('Operation Id')
}, },
{ {
fieldtype:'Float', fieldtype: 'Float',
fieldname:'pending_qty', fieldname: 'pending_qty',
label: __('Pending Qty'), label: __('Pending Qty'),
}, },
{ {
fieldtype:'Float', fieldtype: 'Float',
fieldname:'qty', fieldname: 'qty',
label: __('Quantity to Manufacture'), label: __('Quantity to Manufacture'),
read_only:0, read_only: 0,
in_list_view:1, in_list_view: 1,
},
{
fieldtype: 'Float',
fieldname: 'batch_size',
label: __('Batch Size'),
read_only: 1
}, },
], ],
data: operations_data, data: operations_data,
@ -229,9 +234,13 @@ frappe.ui.form.on("Work Order", {
}, function(data) { }, function(data) {
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",
freeze: true,
args: { args: {
work_order: frm.doc.name, work_order: frm.doc.name,
operations: data.operations, operations: data.operations,
},
callback: function() {
frm.reload_doc();
} }
}); });
}, __("Job Card"), __("Create")); }, __("Job Card"), __("Create"));
@ -243,13 +252,16 @@ frappe.ui.form.on("Work Order", {
if(data.completed_qty != frm.doc.qty) { if(data.completed_qty != frm.doc.qty) {
pending_qty = frm.doc.qty - flt(data.completed_qty); pending_qty = frm.doc.qty - flt(data.completed_qty);
dialog.fields_dict.operations.df.data.push({ if (pending_qty) {
'name': data.name, dialog.fields_dict.operations.df.data.push({
'operation': data.operation, 'name': data.name,
'workstation': data.workstation, 'operation': data.operation,
'qty': pending_qty, 'workstation': data.workstation,
'pending_qty': pending_qty, 'batch_size': data.batch_size,
}); 'qty': pending_qty,
'pending_qty': pending_qty
});
}
} }
}); });
dialog.fields_dict.operations.grid.refresh(); dialog.fields_dict.operations.grid.refresh();

View File

@ -21,6 +21,12 @@
"produced_qty", "produced_qty",
"sales_order", "sales_order",
"project", "project",
"serial_no_and_batch_for_finished_good_section",
"has_serial_no",
"has_batch_no",
"column_break_17",
"serial_no",
"batch_size",
"settings_section", "settings_section",
"allow_alternative_item", "allow_alternative_item",
"use_multi_level_bom", "use_multi_level_bom",
@ -52,6 +58,7 @@
"actual_operating_cost", "actual_operating_cost",
"additional_operating_cost", "additional_operating_cost",
"column_break_24", "column_break_24",
"corrective_operation_cost",
"total_operating_cost", "total_operating_cost",
"more_info", "more_info",
"description", "description",
@ -488,6 +495,57 @@
"fieldtype": "Float", "fieldtype": "Float",
"label": "Lead Time", "label": "Lead Time",
"read_only": 1 "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",
"no_copy": 1
},
{
"default": "0",
"depends_on": "has_batch_no",
"fieldname": "batch_size",
"fieldtype": "Float",
"label": "Batch Size"
},
{
"allow_on_submit": 1,
"description": "From Corrective Job Card",
"fieldname": "corrective_operation_cost",
"fieldtype": "Currency",
"label": "Corrective Operation Cost",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
} }
], ],
"icon": "fa fa-cogs", "icon": "fa fa-cogs",
@ -495,7 +553,7 @@
"image_field": "image", "image_field": "image",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-03-16 13:27:51.116484", "modified": "2021-06-20 15:19:14.902699",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Work Order", "name": "Work Order",

View File

@ -19,14 +19,16 @@ from frappe.utils.csvutils import getlink
from erpnext.stock.utils import get_bin, validate_warehouse_company, get_latest_stock_qty 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 erpnext.utilities.transaction_base import validate_uom_is_integer
from frappe.model.mapper import get_mapped_doc 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 OverProductionError(frappe.ValidationError): pass
class CapacityError(frappe.ValidationError): pass class CapacityError(frappe.ValidationError): pass
class StockOverProductionError(frappe.ValidationError): pass class StockOverProductionError(frappe.ValidationError): pass
class OperationTooLongError(frappe.ValidationError): pass class OperationTooLongError(frappe.ValidationError): pass
class ItemHasVariantError(frappe.ValidationError): pass class ItemHasVariantError(frappe.ValidationError): pass
class SerialNoQtyError(frappe.ValidationError):
from six import string_types pass
form_grid_templates = { form_grid_templates = {
"operations": "templates/form_grid/work_order_grid.html" "operations": "templates/form_grid/work_order_grid.html"
@ -127,7 +129,9 @@ class WorkOrder(Document):
variable_cost = self.actual_operating_cost if self.actual_operating_cost \ variable_cost = self.actual_operating_cost if self.actual_operating_cost \
else self.planned_operating_cost else self.planned_operating_cost
self.total_operating_cost = flt(self.additional_operating_cost) + flt(variable_cost)
self.total_operating_cost = (flt(self.additional_operating_cost)
+ flt(variable_cost) + flt(self.corrective_operation_cost))
def validate_work_order_against_so(self): def validate_work_order_against_so(self):
# already ordered qty # already ordered qty
@ -235,6 +239,9 @@ class WorkOrder(Document):
production_plan.run_method("update_produced_qty", produced_qty, self.production_plan_item) 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): def on_submit(self):
if not self.wip_warehouse: if not self.wip_warehouse:
frappe.throw(_("Work-in-Progress Warehouse is required before Submit")) frappe.throw(_("Work-in-Progress Warehouse is required before Submit"))
@ -266,6 +273,70 @@ class WorkOrder(Document):
self.update_planned_qty() self.update_planned_qty()
self.update_ordered_qty() self.update_ordered_qty()
self.update_reserved_qty_for_production() 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.create_batch_for_finished_good()
args = {
"item_code": self.production_item,
"work_order": self.name
}
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
make_batch(frappe._dict({
"item": self.production_item,
"qty_to_produce": qty,
"reference_doctype": self.doctype,
"reference_name": self.name
}))
def delete_auto_created_batch_and_serial_no(self):
for row in frappe.get_all("Serial No", filters = {"work_order": self.name}):
frappe.delete_doc("Serial No", row.name)
self.db_set("serial_no", "")
for row in frappe.get_all("Batch", filters = {"reference_name": self.name}):
frappe.delete_doc("Batch", row.name)
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)
if self.serial_no:
args.update({"serial_no": self.serial_no, "actual_qty": self.qty})
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): def create_job_card(self):
manufacturing_settings_doc = frappe.get_doc("Manufacturing Settings") manufacturing_settings_doc = frappe.get_doc("Manufacturing Settings")
@ -273,32 +344,40 @@ class WorkOrder(Document):
enable_capacity_planning = not cint(manufacturing_settings_doc.disable_capacity_planning) enable_capacity_planning = not cint(manufacturing_settings_doc.disable_capacity_planning)
plan_days = cint(manufacturing_settings_doc.capacity_planning_for_days) or 30 plan_days = cint(manufacturing_settings_doc.capacity_planning_for_days) or 30
for i, row in enumerate(self.operations): for index, row in enumerate(self.operations):
self.set_operation_start_end_time(i, row) qty = self.qty
while qty > 0:
if not row.workstation: qty = split_qty_based_on_batch_size(self, row, qty)
frappe.throw(_("Row {0}: select the workstation against the operation {1}") if row.job_card_qty > 0:
.format(row.idx, row.operation)) self.prepare_data_for_job_card(row, index,
plan_days, enable_capacity_planning)
original_start_time = row.planned_start_time
job_card_doc = create_job_card(self, row,
enable_capacity_planning=enable_capacity_planning, auto_create=True)
if enable_capacity_planning and job_card_doc:
row.planned_start_time = job_card_doc.time_logs[-1].from_time
row.planned_end_time = job_card_doc.time_logs[-1].to_time
if date_diff(row.planned_start_time, original_start_time) > plan_days:
frappe.message_log.pop()
frappe.throw(_("Unable to find the time slot in the next {0} days for the operation {1}.")
.format(plan_days, row.operation), CapacityError)
row.db_update()
planned_end_date = self.operations and self.operations[-1].planned_end_time planned_end_date = self.operations and self.operations[-1].planned_end_time
if planned_end_date: if planned_end_date:
self.db_set("planned_end_date", planned_end_date) self.db_set("planned_end_date", planned_end_date)
def prepare_data_for_job_card(self, row, 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, auto_create=True,
enable_capacity_planning=enable_capacity_planning)
if enable_capacity_planning and job_card_doc:
row.planned_start_time = job_card_doc.time_logs[-1].from_time
row.planned_end_time = job_card_doc.time_logs[-1].to_time
if date_diff(row.planned_start_time, original_start_time) > plan_days:
frappe.message_log.pop()
frappe.throw(_("Unable to find the time slot in the next {0} days for the operation {1}.")
.format(plan_days, row.operation), CapacityError)
row.db_update()
def set_operation_start_end_time(self, idx, row): def set_operation_start_end_time(self, idx, row):
"""Set start and end time for given operation. If first operation, set start as """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.""" `planned_start_date`, else add time diff to end time of earlier operation."""
@ -669,6 +748,17 @@ class WorkOrder(Document):
bom.set_bom_material_details() bom.set_bom_material_details()
return bom return bom
def update_batch_produced_qty(self, stock_entry_doc):
if not cint(frappe.db.get_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order")):
return
for row in stock_entry_doc.items:
if row.batch_no and (row.is_finished_item or row.is_scrap_item):
qty = frappe.get_all("Stock Entry Detail", filters = {"batch_no": row.batch_no, "docstatus": 1},
or_filters= {"is_finished_item": 1, "is_scrap_item": 1}, fields = ["sum(qty)"], as_list=1)[0][0]
frappe.db.set_value("Batch", row.batch_no, "produced_qty", flt(qty))
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def get_bom_operations(doctype, txt, searchfield, start, page_len, filters): def get_bom_operations(doctype, txt, searchfield, start, page_len, filters):
@ -746,7 +836,7 @@ def make_work_order(bom_no, item, qty=0, project=None, variant_items=None):
return wo_doc return wo_doc
def add_variant_item(variant_items, wo_doc, bom_no, table_name="items"): def add_variant_item(variant_items, wo_doc, bom_no, table_name="items"):
if isinstance(variant_items, string_types): if isinstance(variant_items, str):
variant_items = json.loads(variant_items) variant_items = json.loads(variant_items)
for item in variant_items: for item in variant_items:
@ -826,6 +916,7 @@ def make_stock_entry(work_order_id, purpose, qty=None):
stock_entry.set_stock_entry_type() stock_entry.set_stock_entry_type()
stock_entry.get_items() stock_entry.get_items()
stock_entry.set_serial_no_batch_for_finished_good()
return stock_entry.as_dict() return stock_entry.as_dict()
@frappe.whitelist() @frappe.whitelist()
@ -867,13 +958,47 @@ def query_sales_order(production_item):
@frappe.whitelist() @frappe.whitelist()
def make_job_card(work_order, operations): def make_job_card(work_order, operations):
if isinstance(operations, string_types): if isinstance(operations, str):
operations = json.loads(operations) operations = json.loads(operations)
work_order = frappe.get_doc('Work Order', work_order) work_order = frappe.get_doc('Work Order', work_order)
for row in operations: for row in operations:
row = frappe._dict(row)
validate_operation_data(row) validate_operation_data(row)
create_job_card(work_order, row, row.get("qty"), auto_create=True) qty = row.get("qty")
while qty > 0:
qty = split_qty_based_on_batch_size(work_order, row, qty)
if row.job_card_qty > 0:
create_job_card(work_order, row, auto_create=True)
def split_qty_based_on_batch_size(wo_doc, row, qty):
if not cint(frappe.db.get_value("Operation",
row.operation, "create_job_card_based_on_batch_size")):
row.batch_size = row.get("qty") or wo_doc.qty
row.job_card_qty = row.batch_size
if row.batch_size and qty >= row.batch_size:
qty -= row.batch_size
elif qty > 0:
row.job_card_qty = qty
qty = 0
get_serial_nos_for_job_card(row, wo_doc)
return qty
def get_serial_nos_for_job_card(row, wo_doc):
if not wo_doc.serial_no:
return
serial_nos = get_serial_nos(wo_doc.serial_no)
used_serial_nos = []
for d in frappe.get_all('Job Card', fields=['serial_no'],
filters={'docstatus': ('<', 2), 'work_order': wo_doc.name, 'operation_id': row.name}):
used_serial_nos.extend(get_serial_nos(d.serial_no))
serial_nos = sorted(list(set(serial_nos) - set(used_serial_nos)))
row.serial_no = '\n'.join(serial_nos[0:row.job_card_qty])
def validate_operation_data(row): def validate_operation_data(row):
if row.get("qty") <= 0: if row.get("qty") <= 0:
@ -892,20 +1017,22 @@ def validate_operation_data(row):
) )
) )
def create_job_card(work_order, row, qty=0, enable_capacity_planning=False, auto_create=False): def create_job_card(work_order, row, 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.get("operation"), 'operation': row.get("operation"),
'workstation': row.get("workstation"), 'workstation': row.get("workstation"),
'posting_date': nowdate(), 'posting_date': nowdate(),
'for_quantity': qty or work_order.get('qty', 0), 'for_quantity': row.job_card_qty or work_order.get('qty', 0),
'operation_id': row.get("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,
'sequence_id': row.get("sequence_id"), 'sequence_id': row.get("sequence_id"),
'wip_warehouse': work_order.wip_warehouse 'wip_warehouse': work_order.wip_warehouse,
'hour_rate': row.get("hour_rate"),
'serial_no': row.get("serial_no")
}) })
if work_order.transfer_material_against == 'Job Card' and not work_order.skip_transfer: if work_order.transfer_material_against == 'Job Card' and not work_order.skip_transfer:

View File

@ -4,10 +4,17 @@ from frappe import _
def get_data(): def get_data():
return { return {
'fieldname': 'work_order', 'fieldname': 'work_order',
'non_standard_fieldnames': {
'Batch': 'reference_name'
},
'transactions': [ 'transactions': [
{ {
'label': _('Transactions'), 'label': _('Transactions'),
'items': ['Stock Entry', 'Job Card', 'Pick List'] 'items': ['Stock Entry', 'Job Card', 'Pick List']
},
{
'label': _('Reference'),
'items': ['Serial No', 'Batch']
} }
] ]
} }

View File

@ -8,8 +8,9 @@
"details", "details",
"operation", "operation",
"bom", "bom",
"sequence_id", "column_break_4",
"description", "description",
"sequence_id",
"col_break1", "col_break1",
"completed_qty", "completed_qty",
"status", "status",
@ -195,12 +196,16 @@
"label": "Sequence ID", "label": "Sequence ID",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-10-14 12:58:49.241252", "modified": "2021-01-12 14:48:31.061286",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Work Order Operation", "name": "Work Order Operation",

View File

@ -0,0 +1,105 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Cost of Poor Quality Report"] = {
"filters": [
{
label: __("Company"),
fieldname: "company",
fieldtype: "Link",
options: "Company",
default: frappe.defaults.get_user_default("Company"),
reqd: 1
},
{
label: __("From Date"),
fieldname:"from_date",
fieldtype: "Datetime",
default: frappe.datetime.convert_to_system_tz(frappe.datetime.add_months(frappe.datetime.now_datetime(), -1)),
reqd: 1
},
{
label: __("To Date"),
fieldname:"to_date",
fieldtype: "Datetime",
default: frappe.datetime.now_datetime(),
reqd: 1,
},
{
label: __("Job Card"),
fieldname: "name",
fieldtype: "Link",
options: "Job Card",
get_query: function() {
return {
filters: {
is_corrective_job_card: 1,
docstatus: 1
}
}
}
},
{
label: __("Work Order"),
fieldname: "work_order",
fieldtype: "Link",
options: "Work Order"
},
{
label: __("Operation"),
fieldname: "operation",
fieldtype: "Link",
options: "Operation",
get_query: function() {
return {
filters: {
is_corrective_operation: 1
}
}
}
},
{
label: __("Workstation"),
fieldname: "workstation",
fieldtype: "Link",
options: "Workstation"
},
{
label: __("Item"),
fieldname: "production_item",
fieldtype: "Link",
options: "Item"
},
{
label: __("Serial No"),
fieldname: "serial_no",
fieldtype: "Link",
options: "Serial No",
depends_on: "eval: doc.production_item",
get_query: function() {
var item_code = frappe.query_report.get_filter_value('production_item');
return {
filters: {
item_code: item_code
}
}
}
},
{
label: __("Batch No"),
fieldname: "batch_no",
fieldtype: "Link",
options: "Batch No",
depends_on: "eval: doc.production_item",
get_query: function() {
var item_code = frappe.query_report.get_filter_value('production_item');
return {
filters: {
item: item_code
}
}
}
},
]
};

View File

@ -0,0 +1,33 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2021-01-11 11:10:58.292896",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"json": "{}",
"modified": "2021-01-11 11:11:03.594242",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Cost of Poor Quality Report",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Job Card",
"report_name": "Cost of Poor Quality Report",
"report_type": "Script Report",
"roles": [
{
"role": "System Manager"
},
{
"role": "Manufacturing User"
},
{
"role": "Manufacturing Manager"
}
]
}

View File

@ -0,0 +1,127 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import flt
def execute(filters=None):
columns, data = [], []
columns = get_columns(filters)
data = get_data(filters)
return columns, data
def get_data(report_filters):
data = []
operations = frappe.get_all("Operation", filters = {"is_corrective_operation": 1})
if operations:
operations = [d.name for d in operations]
fields = ["production_item as item_code", "item_name", "work_order", "operation",
"workstation", "total_time_in_mins", "name", "hour_rate", "serial_no", "batch_no"]
filters = get_filters(report_filters, operations)
job_cards = frappe.get_all("Job Card", fields = fields,
filters = filters)
for row in job_cards:
row.operating_cost = flt(row.hour_rate) * (flt(row.total_time_in_mins) / 60.0)
update_raw_material_cost(row, report_filters)
data.append(row)
return data
def get_filters(report_filters, operations):
filters = {"docstatus": 1, "operation": ("in", operations), "is_corrective_job_card": 1}
for field in ["name", "work_order", "operation", "workstation", "company", "serial_no", "batch_no", "production_item"]:
if report_filters.get(field):
if field != 'serial_no':
filters[field] = report_filters.get(field)
else:
filters[field] = ('like', '% {} %'.format(report_filters.get(field)))
return filters
def update_raw_material_cost(row, filters):
row.rm_cost = 0.0
for data in frappe.get_all("Job Card Item", fields = ["amount"],
filters={"parent": row.name, "docstatus": 1}):
row.rm_cost += data.amount
def get_columns(filters):
return [
{
"label": _("Job Card"),
"fieldtype": "Link",
"fieldname": "name",
"options": "Job Card",
"width": "100"
},
{
"label": _("Work Order"),
"fieldtype": "Link",
"fieldname": "work_order",
"options": "Work Order",
"width": "100"
},
{
"label": _("Item Code"),
"fieldtype": "Link",
"fieldname": "item_code",
"options": "Item",
"width": "100"
},
{
"label": _("Item Name"),
"fieldtype": "Data",
"fieldname": "item_name",
"width": "100"
},
{
"label": _("Operation"),
"fieldtype": "Link",
"fieldname": "operation",
"options": "Operation",
"width": "100"
},
{
"label": _("Serial No"),
"fieldtype": "Data",
"fieldname": "serial_no",
"width": "100"
},
{
"label": _("Batch No"),
"fieldtype": "Data",
"fieldname": "batch_no",
"width": "100"
},
{
"label": _("Workstation"),
"fieldtype": "Link",
"fieldname": "workstation",
"options": "Workstation",
"width": "100"
},
{
"label": _("Operating Cost"),
"fieldtype": "Currency",
"fieldname": "operating_cost",
"width": "100"
},
{
"label": _("Raw Material Cost"),
"fieldtype": "Currency",
"fieldname": "rm_cost",
"width": "100"
},
{
"label": _("Total Time (in Mins)"),
"fieldtype": "Float",
"fieldname": "total_time_in_mins",
"width": "100"
}
]

View File

@ -289,3 +289,4 @@ erpnext.patches.v13_0.update_timesheet_changes
erpnext.patches.v13_0.set_training_event_attendance erpnext.patches.v13_0.set_training_event_attendance
erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold
erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice
erpnext.patches.v13_0.update_job_card_details

View File

@ -0,0 +1,16 @@
# Copyright (c) 2019, Frappe and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
def execute():
frappe.reload_doc("manufacturing", "doctype", "job_card")
frappe.reload_doc("manufacturing", "doctype", "job_card_item")
frappe.reload_doc("manufacturing", "doctype", "work_order_operation")
frappe.db.sql(""" update `tabJob Card` jc, `tabWork Order Operation` wo
SET jc.hour_rate = wo.hour_rate
WHERE
jc.operation_id = wo.name and jc.docstatus < 2 and wo.hour_rate > 0
""")

View File

@ -1,4 +1,5 @@
{ {
"actions": [],
"allow_import": 1, "allow_import": 1,
"autoname": "field:batch_id", "autoname": "field:batch_id",
"creation": "2013-03-05 14:50:38", "creation": "2013-03-05 14:50:38",
@ -25,7 +26,11 @@
"reference_doctype", "reference_doctype",
"reference_name", "reference_name",
"section_break_7", "section_break_7",
"description" "description",
"manufacturing_section",
"qty_to_produce",
"column_break_23",
"produced_qty"
], ],
"fields": [ "fields": [
{ {
@ -160,13 +165,35 @@
"label": "Batch UOM", "label": "Batch UOM",
"options": "UOM", "options": "UOM",
"read_only": 1 "read_only": 1
},
{
"fieldname": "manufacturing_section",
"fieldtype": "Section Break",
"label": "Manufacturing"
},
{
"fieldname": "qty_to_produce",
"fieldtype": "Float",
"label": "Qty To Produce",
"read_only": 1
},
{
"fieldname": "column_break_23",
"fieldtype": "Column Break"
},
{
"fieldname": "produced_qty",
"fieldtype": "Float",
"label": "Produced Qty",
"read_only": 1
} }
], ],
"icon": "fa fa-archive", "icon": "fa fa-archive",
"idx": 1, "idx": 1,
"image_field": "image", "image_field": "image",
"links": [],
"max_attachments": 5, "max_attachments": 5,
"modified": "2020-09-18 17:26:09.703215", "modified": "2021-01-07 11:10:09.149170",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Batch", "name": "Batch",

View File

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

View File

@ -57,7 +57,8 @@
"more_info", "more_info",
"serial_no_details", "serial_no_details",
"company", "company",
"status" "status",
"work_order"
], ],
"fields": [ "fields": [
{ {
@ -422,12 +423,18 @@
"label": "Status", "label": "Status",
"options": "\nActive\nInactive\nDelivered\nExpired", "options": "\nActive\nInactive\nDelivered\nExpired",
"read_only": 1 "read_only": 1
},
{
"fieldname": "work_order",
"fieldtype": "Link",
"label": "Work Order",
"options": "Work Order"
} }
], ],
"icon": "fa fa-barcode", "icon": "fa fa-barcode",
"idx": 1, "idx": 1,
"links": [], "links": [],
"modified": "2020-07-20 20:50:16.660433", "modified": "2021-01-08 14:31:15.375996",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Serial No", "name": "Serial No",

View File

@ -473,16 +473,13 @@ def get_serial_nos(serial_no):
if s.strip()] if s.strip()]
def update_args_for_serial_no(serial_no_doc, serial_no, args, is_new=False): def update_args_for_serial_no(serial_no_doc, serial_no, args, is_new=False):
serial_no_doc.update({ for field in ["item_code", "work_order", "company", "batch_no", "supplier", "location"]:
"item_code": args.get("item_code"), if args.get(field):
"company": args.get("company"), serial_no_doc.set(field, args.get(field))
"batch_no": args.get("batch_no"),
"via_stock_ledger": args.get("via_stock_ledger") or True, serial_no_doc.via_stock_ledger = args.get("via_stock_ledger") or True
"supplier": args.get("supplier"), serial_no_doc.warehouse = (args.get("warehouse")
"location": args.get("location"), if args.get("actual_qty", 0) > 0 else None)
"warehouse": (args.get("warehouse")
if args.get("actual_qty", 0) > 0 else None)
})
if is_new: if is_new:
serial_no_doc.serial_no = serial_no serial_no_doc.serial_no = serial_no

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")) d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount"))
if not d.t_warehouse: if not d.t_warehouse:
outgoing_items_cost += flt(d.basic_amount) outgoing_items_cost += flt(d.basic_amount)
return outgoing_items_cost return outgoing_items_cost
def get_args_for_incoming_rate(self, item): def get_args_for_incoming_rate(self, item):
@ -854,6 +855,7 @@ class StockEntry(StockController):
pro_doc.run_method("update_work_order_qty") pro_doc.run_method("update_work_order_qty")
if self.purpose == "Manufacture": if self.purpose == "Manufacture":
pro_doc.run_method("update_planned_qty") pro_doc.run_method("update_planned_qty")
pro_doc.update_batch_produced_qty(self)
if not pro_doc.operations: if not pro_doc.operations:
pro_doc.set_actual_dates() pro_doc.set_actual_dates()
@ -1076,18 +1078,54 @@ class StockEntry(StockController):
# in case of BOM # in case of BOM
to_warehouse = item.get("default_warehouse") to_warehouse = item.get("default_warehouse")
args = {
"to_warehouse": to_warehouse,
"from_warehouse": "",
"qty": self.fg_completed_qty,
"item_name": item.item_name,
"description": item.description,
"stock_uom": item.stock_uom,
"expense_account": item.get("expense_account"),
"cost_center": item.get("buying_cost_center"),
"is_finished_item": 1
}
if self.work_order and self.pro_doc.has_batch_no:
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)
filters = {
"reference_name": self.pro_doc.name,
"reference_doctype": self.pro_doc.doctype,
"qty_to_produce": (">", 0)
}
fields = ["qty_to_produce as qty", "produced_qty", "name"]
for row in frappe.get_all("Batch", filters = filters, fields = fields, order_by="creation asc"):
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.name
self.add_finisged_goods(args, item)
def add_finisged_goods(self, args, item):
self.add_to_stock_entry_detail({ self.add_to_stock_entry_detail({
item.name: { item.name: args
"to_warehouse": to_warehouse,
"from_warehouse": "",
"qty": self.fg_completed_qty,
"item_name": item.item_name,
"description": item.description,
"stock_uom": item.stock_uom,
"expense_account": item.get("expense_account"),
"cost_center": item.get("buying_cost_center"),
"is_finished_item": 1
}
}, bom_no = self.bom_no) }, bom_no = self.bom_no)
def get_bom_raw_materials(self, qty): def get_bom_raw_materials(self, qty):
@ -1524,6 +1562,36 @@ class StockEntry(StockController):
material_requests.append(material_request) material_requests.append(material_request)
frappe.db.set_value('Material Request', material_request, 'transfer_status', status) 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:
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() @frappe.whitelist()
def move_sample_to_retention_warehouse(company, items): def move_sample_to_retention_warehouse(company, items):
if isinstance(items, string_types): if isinstance(items, string_types):
@ -1635,6 +1703,10 @@ def get_operating_cost_per_unit(work_order=None, bom_no=None):
if bom.quantity: if bom.quantity:
operating_cost_per_unit = flt(bom.operating_cost) / flt(bom.quantity) operating_cost_per_unit = flt(bom.operating_cost) / flt(bom.quantity)
if work_order and work_order.produced_qty and cint(frappe.db.get_single_value('Manufacturing Settings',
'add_corrective_operation_cost_in_finished_good_valuation')):
operating_cost_per_unit += flt(work_order.corrective_operation_cost) / flt(work_order.produced_qty)
return operating_cost_per_unit return operating_cost_per_unit
def get_used_alternative_items(purchase_order=None, work_order=None): def get_used_alternative_items(purchase_order=None, work_order=None):

View File

@ -18,6 +18,7 @@
"col_break2", "col_break2",
"is_finished_item", "is_finished_item",
"is_scrap_item", "is_scrap_item",
"quality_inspection",
"subcontracted_item", "subcontracted_item",
"section_break_8", "section_break_8",
"description", "description",
@ -69,7 +70,6 @@
"putaway_rule", "putaway_rule",
"column_break_51", "column_break_51",
"reference_purchase_receipt", "reference_purchase_receipt",
"quality_inspection",
"job_card_item" "job_card_item"
], ],
"fields": [ "fields": [
@ -548,7 +548,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-02-11 13:47:50.158754", "modified": "2021-04-22 20:08:23.799715",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Entry Detail", "name": "Stock Entry Detail",