fix: total time calculation

This commit is contained in:
Rohit Waghchaure 2021-03-17 14:03:12 +05:30
parent 57307443f0
commit 2330c41cca
18 changed files with 324 additions and 171 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

@ -42,7 +42,7 @@ frappe.ui.form.on('Job Card', {
} }
if (frm.doc.docstatus == 1 && !frm.doc.is_corrective_job_card) { if (frm.doc.docstatus == 1 && !frm.doc.is_corrective_job_card) {
frm.trigger('setup_corrective_job_card') frm.trigger('setup_corrective_job_card');
} }
frm.set_query("quality_inspection", function() { frm.set_query("quality_inspection", function() {
@ -71,15 +71,27 @@ frappe.ui.form.on('Job Card', {
let fields = [ let fields = [
{ {
fieldtype: 'Link', label: __('Corrective Operation'), options: 'Operation', fieldtype: 'Link', label: __('Corrective Operation'), options: 'Operation',
fieldname: 'operation', get_query() { return { filters: { "is_corrective_operation": 1 }}} fieldname: 'operation', get_query() {
return {
filters: {
"is_corrective_operation": 1
}
};
}
}, { }, {
fieldtype: 'Link', label: __('For Operation'), options: 'Operation', fieldtype: 'Link', label: __('For Operation'), options: 'Operation',
fieldname: 'for_operation', get_query() { return { filters: { "name": ["in", operations] }}} fieldname: 'for_operation', get_query() {
return {
filters: {
"name": ["in", operations]
}
};
}
} }
]; ];
frappe.prompt(fields, d => { frappe.prompt(fields, d => {
frm.events.make_corrective_job_card(frm, d.operation, d.for_operation); frm.events.make_corrective_job_card(frm, d.operation, d.for_operation);
}, __("Select Corrective Operation")); }, __("Select Corrective Operation"));
}, __('Make')); }, __('Make'));
}, },
@ -152,14 +164,18 @@ frappe.ui.form.on('Job Card', {
if (!frm.doc.started_time && !frm.doc.current_time) { if (!frm.doc.started_time && !frm.doc.current_time) {
frm.add_custom_button(__("Start Job"), () => { frm.add_custom_button(__("Start Job"), () => {
frappe.prompt({fieldtype: 'Table MultiSelect', label: __('Select Employees'), if ((frm.doc.employee && !frm.doc.employee.length) || !frm.doc.employee) {
options: "Job Card Time Log", fieldname: 'employees'}, d => { frappe.prompt({fieldtype: 'Table MultiSelect', label: __('Select Employees'),
options: "Job Card Time Log", fieldname: 'employees'}, d => {
frm.events.start_job(frm, "Work In Progress", d.employees); frm.events.start_job(frm, "Work In Progress", d.employees);
}, __("Assign Job to Employee")); }, __("Assign Job to Employee"));
} else {
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 Job"), () => { frm.add_custom_button(__("Resume Job"), () => {
frm.events.start_job(frm, "Resume Job"); frm.events.start_job(frm, "Resume Job", frm.doc.employee);
}).addClass("btn-primary"); }).addClass("btn-primary");
} else { } else {
frm.add_custom_button(__("Pause Job"), () => { frm.add_custom_button(__("Pause Job"), () => {
@ -167,10 +183,26 @@ frappe.ui.form.on('Job Card', {
}); });
frm.add_custom_button(__("Complete Job"), () => { frm.add_custom_button(__("Complete Job"), () => {
frappe.prompt({fieldtype: 'Float', label: __('Completed Quantity'), var sub_operations = frm.doc.sub_operations;
fieldname: 'qty', default: frm.doc.for_quantity}, data => {
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'),
fieldname: 'qty', default: frm.doc.for_quantity}, data => {
frm.events.complete_job(frm, "Complete", data.qty); frm.events.complete_job(frm, "Complete", data.qty);
}, __("Enter Value")); }, __("Enter Value"));
} else {
frm.events.complete_job(frm, "Complete", 0.0);
}
}).addClass("btn-primary"); }).addClass("btn-primary");
} }
}, },
@ -204,11 +236,11 @@ frappe.ui.form.on('Job Card', {
args: args args: args
}, },
freeze: true, freeze: true,
callback: function (r) { callback: function () {
frm.reload_doc(); frm.reload_doc();
frm.trigger("make_dashboard"); frm.trigger("make_dashboard");
} }
}) });
}, },
update_sub_operation: function(frm, args) { update_sub_operation: function(frm, args) {

View File

@ -16,15 +16,18 @@
"production_item", "production_item",
"item_name", "item_name",
"for_quantity", "for_quantity",
"serial_no",
"column_break_12", "column_break_12",
"wip_warehouse", "wip_warehouse",
"quality_inspection", "quality_inspection",
"project", "project",
"batch_no",
"operation_section_section", "operation_section_section",
"operation", "operation",
"operation_row_number", "operation_row_number",
"column_break_18", "column_break_18",
"workstation", "workstation",
"employee",
"section_break_21", "section_break_21",
"sub_operations", "sub_operations",
"timing_detail", "timing_detail",
@ -163,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,
@ -373,11 +375,28 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "For Operation", "label": "For Operation",
"options": "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": "2021-02-03 20:36:51.826944", "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

@ -4,9 +4,9 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
import datetime, json import datetime
import json
from frappe import _, bold from frappe import _, bold
from six import string_types
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,
@ -33,11 +33,10 @@ class JobCard(Document):
if self.operation: if self.operation:
self.sub_operations = [] self.sub_operations = []
for row in frappe.get_all("Sub Operation", for row in frappe.get_all("Sub Operation",
filters = {"parent": self.operation}, fields=["operation"]): filters = {"parent": self.operation}, fields=["operation", "idx"]):
self.append("sub_operations", { row.status = "Pending"
"sub_operation": row.operation, row.sub_operation = row.operation
"status": "Pending" self.append("sub_operations", row)
})
def validate_time_logs(self): def validate_time_logs(self):
self.total_time_in_mins = 0.0 self.total_time_in_mins = 0.0
@ -57,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
@ -173,6 +175,10 @@ class JobCard(Document):
def add_time_log(self, args): def add_time_log(self, args):
last_row = [] last_row = []
employees = args.employees
if isinstance(employees, str):
employees = json.loads(employees)
if self.time_logs and len(self.time_logs) > 0: if self.time_logs and len(self.time_logs) > 0:
last_row = self.time_logs[-1] last_row = self.time_logs[-1]
@ -186,13 +192,7 @@ class JobCard(Document):
"completed_qty": args.get("completed_qty") or 0.0 "completed_qty": args.get("completed_qty") or 0.0
}) })
elif args.get("start_time"): elif args.get("start_time"):
employees = args.employees
print(args)
if isinstance(employees, string_types):
employees = json.loads(employees)
for name in employees: for name in employees:
print(name.get('employee'))
self.append("time_logs", { self.append("time_logs", {
"from_time": get_datetime(args.get("start_time")), "from_time": get_datetime(args.get("start_time")),
"employee": name.get('employee'), "employee": name.get('employee'),
@ -200,11 +200,21 @@ class JobCard(Document):
"completed_qty": 0.0 "completed_qty": 0.0
}) })
if not self.employee:
self.set_employees(employees)
if self.status == "On Hold": if self.status == "On Hold":
self.current_time = time_diff_in_seconds(last_row.to_time, last_row.from_time) self.current_time = time_diff_in_seconds(last_row.to_time, last_row.from_time)
self.save() 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): def reset_timer_value(self, args):
self.started_time = None self.started_time = None
@ -221,24 +231,41 @@ class JobCard(Document):
self.status = args.get("status") self.status = args.get("status")
def update_sub_operation_status(self): def update_sub_operation_status(self):
if not (self.sub_operations and self.time_logs): return if not (self.sub_operations and self.time_logs):
return
operation_wise_completed_time = {} operation_wise_completed_time = {}
for time_log in self.time_logs: for time_log in self.time_logs:
if time_log.operation not in operation_wise_completed_time: if time_log.operation not in operation_wise_completed_time:
operation_wise_completed_time.setdefault(time_log.operation, operation_wise_completed_time.setdefault(time_log.operation,
frappe._dict({"status": "Pending", "completed_time": 0.0})) frappe._dict({"status": "Pending", "completed_qty":0.0, "completed_time": 0.0, "employee": []}))
op_row = operation_wise_completed_time[time_log.operation] op_row = operation_wise_completed_time[time_log.operation]
op_row.status = "Work In Progress" if not time_log.time_in_mins else "Complete" 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: if time_log.time_in_mins:
op_row.completed_time += 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: for row in self.sub_operations:
operation_deatils = operation_wise_completed_time.get(row.sub_operation) operation_deatils = operation_wise_completed_time.get(row.sub_operation)
if operation_deatils: if operation_deatils:
row.status = operation_deatils.status if row.status != 'Complete':
row.status = operation_deatils.status
row.completed_time = operation_deatils.completed_time 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", {
@ -275,6 +302,7 @@ class JobCard(Document):
}) })
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()
@ -283,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)))
@ -299,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 = [], []
@ -346,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:
@ -453,9 +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 self.is_corrective_job_card: return if self.is_corrective_job_card:
return
if not (self.work_order and self.sequence_id): 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()
@ -480,7 +523,7 @@ class JobCard(Document):
@frappe.whitelist() @frappe.whitelist()
def make_time_log(args): def make_time_log(args):
if isinstance(args, string_types): if isinstance(args, str):
args = json.loads(args) args = json.loads(args)
args = frappe._dict(args) args = frappe._dict(args)
@ -632,6 +675,8 @@ def make_corrective_job_card(source_name, operation=None, for_operation=None, ta
target.for_operation = for_operation target.for_operation = for_operation
target.set('time_logs', []) target.set('time_logs', [])
target.set('employee', [])
target.set('items', [])
target.get_sub_operations() target.get_sub_operations()
target.get_required_items() target.get_required_items()
target.validate_time_logs() target.validate_time_logs()

View File

@ -17,8 +17,6 @@
"required_qty", "required_qty",
"column_break_9", "column_break_9",
"transferred_qty", "transferred_qty",
"rate",
"amount",
"allow_alternative_item" "allow_alternative_item"
], ],
"fields": [ "fields": [
@ -27,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",
@ -69,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",
@ -102,25 +98,14 @@
"fieldtype": "Float", "fieldtype": "Float",
"label": "Transferred Qty", "label": "Transferred Qty",
"no_copy": 1, "no_copy": 1,
"print_hide": 1 "print_hide": 1,
},
{
"fieldname": "rate",
"fieldtype": "Currency",
"label": "Rate",
"read_only": 1
},
{
"fieldname": "amount",
"fieldtype": "Currency",
"label": "Amount",
"read_only": 1 "read_only": 1
} }
], ],
"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

@ -7,7 +7,8 @@
"field_order": [ "field_order": [
"sub_operation", "sub_operation",
"completed_time", "completed_time",
"status" "status",
"completed_qty"
], ],
"fields": [ "fields": [
{ {
@ -34,12 +35,18 @@
"label": "Operation", "label": "Operation",
"options": "Operation", "options": "Operation",
"read_only": 1 "read_only": 1
},
{
"fieldname": "completed_qty",
"fieldtype": "Float",
"label": "Completed Qty",
"read_only": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-12-14 17:08:25.992957", "modified": "2021-03-16 18:24:35.399593",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Job Card Operation", "name": "Job Card Operation",

View File

@ -27,6 +27,7 @@
"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", "column_break_23",
"make_serial_no_batch_from_work_order" "make_serial_no_batch_from_work_order"
], ],
@ -168,13 +169,19 @@
"fieldname": "make_serial_no_batch_from_work_order", "fieldname": "make_serial_no_batch_from_work_order",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Make Serial No / Batch from Work Order" "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-12-08 13:37:40.325838", "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,5 +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', {
setup: function(frm) {
frm.set_query('operation', 'sub_operations', function() {
return {
filters: {
'name': ['not in', [frm.doc.name]]
}
};
});
}
}); });

View File

@ -5,7 +5,6 @@ from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import flt
from frappe.model.document import Document from frappe.model.document import Document
class Operation(Document): class Operation(Document):

View File

@ -189,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,
@ -228,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"));
@ -247,6 +257,7 @@ frappe.ui.form.on("Work Order", {
'name': data.name, 'name': data.name,
'operation': data.operation, 'operation': data.operation,
'workstation': data.workstation, 'workstation': data.workstation,
'batch_size': data.batch_size,
'qty': pending_qty, 'qty': pending_qty,
'pending_qty': pending_qty 'pending_qty': pending_qty
}); });

View File

@ -527,7 +527,8 @@
"depends_on": "has_serial_no", "depends_on": "has_serial_no",
"fieldname": "serial_no", "fieldname": "serial_no",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Serial Nos" "label": "Serial Nos",
"no_copy": 1
}, },
{ {
"default": "0", "default": "0",
@ -552,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

@ -27,9 +27,8 @@ 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): pass class SerialNoQtyError(frappe.ValidationError):
pass
from six import string_types
form_grid_templates = { form_grid_templates = {
"operations": "templates/form_grid/work_order_grid.html" "operations": "templates/form_grid/work_order_grid.html"
@ -277,10 +276,11 @@ class WorkOrder(Document):
self.delete_auto_created_batch_and_serial_no() self.delete_auto_created_batch_and_serial_no()
def create_serial_no_batch_no(self): def create_serial_no_batch_no(self):
if not (self.has_serial_no or self.has_batch_no): return if not (self.has_serial_no or self.has_batch_no):
return
if not cint(frappe.db.get_single_value("Manufacturing Settings", if not cint(frappe.db.get_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order")):
"make_serial_no_batch_from_work_order")): return return
if self.has_batch_no: if self.has_batch_no:
self.create_batch_for_finished_good() self.create_batch_for_finished_good()
@ -346,29 +346,17 @@ class WorkOrder(Document):
for index, row in enumerate(self.operations): for index, row in enumerate(self.operations):
qty = self.qty qty = self.qty
i=0
while qty > 0: while qty > 0:
i += 1 qty = split_qty_based_on_batch_size(self, row, qty)
if not cint(frappe.db.get_value("Operation", if row.job_card_qty > 0:
row.operation, "create_job_card_based_on_batch_size")): self.prepare_data_for_job_card(row, index,
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
qty = 0
if job_card_qty > 0:
self.prepare_data_for_job_card(row, job_card_qty, index,
plan_days, enable_capacity_planning) plan_days, enable_capacity_planning)
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, job_card_qty, index, plan_days, enable_capacity_planning): def prepare_data_for_job_card(self, row, index, plan_days, enable_capacity_planning):
self.set_operation_start_end_time(index, row) self.set_operation_start_end_time(index, row)
if not row.workstation: if not row.workstation:
@ -376,8 +364,8 @@ class WorkOrder(Document):
.format(row.idx, row.operation)) .format(row.idx, row.operation))
original_start_time = row.planned_start_time original_start_time = row.planned_start_time
job_card_doc = create_job_card(self, row, qty=job_card_qty, job_card_doc = create_job_card(self, row, auto_create=True,
enable_capacity_planning=enable_capacity_planning, auto_create=True) enable_capacity_planning=enable_capacity_planning)
if enable_capacity_planning and job_card_doc: if enable_capacity_planning and job_card_doc:
row.planned_start_time = job_card_doc.time_logs[-1].from_time row.planned_start_time = job_card_doc.time_logs[-1].from_time
@ -761,8 +749,8 @@ class WorkOrder(Document):
return bom return bom
def update_batch_produced_qty(self, stock_entry_doc): def update_batch_produced_qty(self, stock_entry_doc):
if not cint(frappe.db.get_single_value("Manufacturing Settings", if not cint(frappe.db.get_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order")):
"make_serial_no_batch_from_work_order")): return return
for row in stock_entry_doc.items: for row in stock_entry_doc.items:
if row.batch_no and (row.is_finished_item or row.is_scrap_item): if row.batch_no and (row.is_finished_item or row.is_scrap_item):
@ -848,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:
@ -970,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:
@ -995,21 +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") '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

@ -65,5 +65,41 @@ frappe.query_reports["Cost of Poor Quality Report"] = {
fieldtype: "Link", fieldtype: "Link",
options: "Workstation" 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

@ -20,7 +20,7 @@ def get_data(report_filters):
if operations: if operations:
operations = [d.name for d in operations] operations = [d.name for d in operations]
fields = ["production_item as item_code", "item_name", "work_order", "operation", fields = ["production_item as item_code", "item_name", "work_order", "operation",
"workstation", "total_time_in_mins", "name", "hour_rate"] "workstation", "total_time_in_mins", "name", "hour_rate", "serial_no", "batch_no"]
filters = get_filters(report_filters, operations) filters = get_filters(report_filters, operations)
@ -30,15 +30,18 @@ def get_data(report_filters):
for row in job_cards: for row in job_cards:
row.operating_cost = flt(row.hour_rate) * (flt(row.total_time_in_mins) / 60.0) row.operating_cost = flt(row.hour_rate) * (flt(row.total_time_in_mins) / 60.0)
update_raw_material_cost(row, report_filters) update_raw_material_cost(row, report_filters)
update_time_details(row, report_filters, data) data.append(row)
return data return data
def get_filters(report_filters, operations): def get_filters(report_filters, operations):
filters = {"docstatus": 1, "operation": ("in", operations), "is_corrective_job_card": 1} filters = {"docstatus": 1, "operation": ("in", operations), "is_corrective_job_card": 1}
for field in ["name", "work_order", "operation", "workstation", "company"]: for field in ["name", "work_order", "operation", "workstation", "company", "serial_no", "batch_no", "production_item"]:
if report_filters.get(field): if report_filters.get(field):
filters[field] = 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 return filters
@ -48,24 +51,6 @@ def update_raw_material_cost(row, filters):
filters={"parent": row.name, "docstatus": 1}): filters={"parent": row.name, "docstatus": 1}):
row.rm_cost += data.amount row.rm_cost += data.amount
def update_time_details(row, filters, data):
args = frappe._dict({"item_code": "", "item_name": "", "name": "", "work_order":"",
"operation": "", "workstation":"", "operating_cost": "", "rm_cost": "", "total_time_in_mins": ""})
i=0
for time_log in frappe.get_all("Job Card Time Log",
fields = ["from_time", "to_time", "time_in_mins"],
filters={"parent": row.name, "docstatus": 1,
"from_time": (">=", filters.from_date), "to_time": ("<=", filters.to_date)}):
if i==0:
i += 1
row.update(time_log)
data.append(row)
else:
args.update(time_log)
data.append(args)
def get_columns(filters): def get_columns(filters):
return [ return [
{ {
@ -102,6 +87,18 @@ def get_columns(filters):
"options": "Operation", "options": "Operation",
"width": "100" "width": "100"
}, },
{
"label": _("Serial No"),
"fieldtype": "Data",
"fieldname": "serial_no",
"width": "100"
},
{
"label": _("Batch No"),
"fieldtype": "Data",
"fieldname": "batch_no",
"width": "100"
},
{ {
"label": _("Workstation"), "label": _("Workstation"),
"fieldtype": "Link", "fieldtype": "Link",
@ -126,23 +123,5 @@ def get_columns(filters):
"fieldtype": "Float", "fieldtype": "Float",
"fieldname": "total_time_in_mins", "fieldname": "total_time_in_mins",
"width": "100" "width": "100"
}, }
{
"label": _("From Time"),
"fieldtype": "Datetime",
"fieldname": "from_time",
"width": "100"
},
{
"label": _("To Time"),
"fieldtype": "Datetime",
"fieldname": "to_time",
"width": "100"
},
{
"label": _("Time in Mins"),
"fieldtype": "Float",
"fieldname": "time_in_mins",
"width": "100"
},
] ]

View File

@ -1097,7 +1097,8 @@ class StockEntry(StockController):
def set_batchwise_finished_goods(self, args, item): def set_batchwise_finished_goods(self, args, item):
qty = flt(self.fg_completed_qty) qty = flt(self.fg_completed_qty)
filters = {"reference_name": self.pro_doc.name, filters = {
"reference_name": self.pro_doc.name,
"reference_doctype": self.pro_doc.doctype, "reference_doctype": self.pro_doc.doctype,
"qty_to_produce": (">", 0) "qty_to_produce": (">", 0)
} }
@ -1106,7 +1107,8 @@ class StockEntry(StockController):
for row in frappe.get_all("Batch", filters = filters, fields = fields, order_by="creation asc"): for row in frappe.get_all("Batch", filters = filters, fields = fields, order_by="creation asc"):
batch_qty = flt(row.qty) - flt(row.produced_qty) batch_qty = flt(row.qty) - flt(row.produced_qty)
if not batch_qty: continue if not batch_qty:
continue
if qty <=0: if qty <=0:
break break
@ -1701,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",