fix: added process loss in job card

This commit is contained in:
Rohit Waghchaure 2023-06-09 20:33:46 +05:30
parent 9e650a004a
commit e9a6191af9
10 changed files with 155 additions and 38 deletions

View File

@ -83,7 +83,7 @@ frappe.ui.form.on('Job Card', {
// and if stock mvt for WIP is required // and if stock mvt for WIP is required
if (frm.doc.work_order) { if (frm.doc.work_order) {
frappe.db.get_value('Work Order', frm.doc.work_order, ['skip_transfer', 'status'], (result) => { frappe.db.get_value('Work Order', frm.doc.work_order, ['skip_transfer', 'status'], (result) => {
if (result.skip_transfer === 1 || result.status == 'In Process' || frm.doc.transferred_qty > 0) { if (result.skip_transfer === 1 || result.status == 'In Process' || frm.doc.transferred_qty > 0 || !frm.doc.items.length) {
frm.trigger("prepare_timer_buttons"); frm.trigger("prepare_timer_buttons");
} }
}); });
@ -411,6 +411,16 @@ frappe.ui.form.on('Job Card', {
} }
}); });
if (frm.doc.total_completed_qty && frm.doc.for_quantity > frm.doc.total_completed_qty) {
let flt_precision = precision('for_quantity', frm.doc);
let process_loss_qty = (
flt(frm.doc.for_quantity, flt_precision)
- flt(frm.doc.total_completed_qty, flt_precision)
);
frm.set_value('process_loss_qty', process_loss_qty);
}
refresh_field("total_completed_qty"); refresh_field("total_completed_qty");
} }
}); });

View File

@ -39,6 +39,7 @@
"time_logs", "time_logs",
"section_break_13", "section_break_13",
"total_completed_qty", "total_completed_qty",
"process_loss_qty",
"column_break_15", "column_break_15",
"total_time_in_mins", "total_time_in_mins",
"section_break_8", "section_break_8",
@ -448,11 +449,17 @@
"no_copy": 1, "no_copy": 1,
"options": "Serial and Batch Bundle", "options": "Serial and Batch Bundle",
"print_hide": 1 "print_hide": 1
},
{
"fieldname": "process_loss_qty",
"fieldtype": "Float",
"label": "Process Loss Qty",
"read_only": 1
} }
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-05-23 09:56:43.826602", "modified": "2023-06-09 12:04:55.534264",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Job Card", "name": "Job Card",

View File

@ -161,7 +161,7 @@ class JobCard(Document):
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: for row in self.sub_operations:
self.total_completed_qty += row.completed_qty self.c += 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
@ -451,6 +451,9 @@ class JobCard(Document):
}, },
) )
def before_save(self):
self.set_process_loss()
def on_submit(self): def on_submit(self):
self.validate_transfer_qty() self.validate_transfer_qty()
self.validate_job_card() self.validate_job_card()
@ -487,19 +490,35 @@ class JobCard(Document):
) )
) )
if self.for_quantity and self.total_completed_qty != self.for_quantity: precision = self.precision("total_completed_qty")
total_completed_qty = flt(
flt(self.total_completed_qty, precision) + flt(self.process_loss_qty, precision)
)
if self.for_quantity and flt(total_completed_qty, precision) != flt(
self.for_quantity, precision
):
total_completed_qty = bold(_("Total Completed Qty")) total_completed_qty = bold(_("Total Completed Qty"))
qty_to_manufacture = bold(_("Qty to Manufacture")) qty_to_manufacture = bold(_("Qty to Manufacture"))
frappe.throw( frappe.throw(
_("The {0} ({1}) must be equal to {2} ({3})").format( _("The {0} ({1}) must be equal to {2} ({3})").format(
total_completed_qty, total_completed_qty,
bold(self.total_completed_qty), bold(flt(total_completed_qty, precision)),
qty_to_manufacture, qty_to_manufacture,
bold(self.for_quantity), bold(self.for_quantity),
) )
) )
def set_process_loss(self):
precision = self.precision("total_completed_qty")
self.process_loss_qty = 0.0
if self.total_completed_qty and self.for_quantity > self.total_completed_qty:
self.process_loss_qty = flt(self.for_quantity, precision) - flt(
self.total_completed_qty, precision
)
def update_work_order(self): def update_work_order(self):
if not self.work_order: if not self.work_order:
return return
@ -511,7 +530,7 @@ class JobCard(Document):
): ):
return return
for_quantity, time_in_mins = 0, 0 for_quantity, time_in_mins, process_loss_qty = 0, 0, 0
from_time_list, to_time_list = [], [] from_time_list, to_time_list = [], []
field = "operation_id" field = "operation_id"
@ -519,6 +538,7 @@ class JobCard(Document):
if data and len(data) > 0: if data and len(data) > 0:
for_quantity = flt(data[0].completed_qty) for_quantity = flt(data[0].completed_qty)
time_in_mins = flt(data[0].time_in_mins) time_in_mins = flt(data[0].time_in_mins)
process_loss_qty = flt(data[0].process_loss_qty)
wo = frappe.get_doc("Work Order", self.work_order) wo = frappe.get_doc("Work Order", self.work_order)
@ -526,8 +546,8 @@ class JobCard(Document):
self.update_corrective_in_work_order(wo) self.update_corrective_in_work_order(wo)
elif self.operation_id: elif self.operation_id:
self.validate_produced_quantity(for_quantity, wo) self.validate_produced_quantity(for_quantity, process_loss_qty, wo)
self.update_work_order_data(for_quantity, time_in_mins, wo) self.update_work_order_data(for_quantity, process_loss_qty, time_in_mins, wo)
def update_corrective_in_work_order(self, wo): def update_corrective_in_work_order(self, wo):
wo.corrective_operation_cost = 0.0 wo.corrective_operation_cost = 0.0
@ -542,11 +562,11 @@ class JobCard(Document):
wo.flags.ignore_validate_update_after_submit = True wo.flags.ignore_validate_update_after_submit = True
wo.save() wo.save()
def validate_produced_quantity(self, for_quantity, wo): def validate_produced_quantity(self, for_quantity, process_loss_qty, wo):
if self.docstatus < 2: if self.docstatus < 2:
return return
if wo.produced_qty > for_quantity: if wo.produced_qty > for_quantity + process_loss_qty:
first_part_msg = _( first_part_msg = _(
"The {0} {1} is used to calculate the valuation cost for the finished good {2}." "The {0} {1} is used to calculate the valuation cost for the finished good {2}."
).format( ).format(
@ -561,7 +581,7 @@ class JobCard(Document):
_("{0} {1}").format(first_part_msg, second_part_msg), JobCardCancelError, title=_("Error") _("{0} {1}").format(first_part_msg, second_part_msg), JobCardCancelError, title=_("Error")
) )
def update_work_order_data(self, for_quantity, time_in_mins, wo): def update_work_order_data(self, for_quantity, process_loss_qty, time_in_mins, wo):
workstation_hour_rate = frappe.get_value("Workstation", self.workstation, "hour_rate") workstation_hour_rate = frappe.get_value("Workstation", self.workstation, "hour_rate")
jc = frappe.qb.DocType("Job Card") jc = frappe.qb.DocType("Job Card")
jctl = frappe.qb.DocType("Job Card Time Log") jctl = frappe.qb.DocType("Job Card Time Log")
@ -582,6 +602,7 @@ class JobCard(Document):
for data in wo.operations: for data in wo.operations:
if data.get("name") == self.operation_id: if data.get("name") == self.operation_id:
data.completed_qty = for_quantity data.completed_qty = for_quantity
data.process_loss_qty = process_loss_qty
data.actual_operation_time = time_in_mins data.actual_operation_time = time_in_mins
data.actual_start_time = time_data[0].start_time if time_data else None data.actual_start_time = time_data[0].start_time if time_data else None
data.actual_end_time = time_data[0].end_time if time_data else None data.actual_end_time = time_data[0].end_time if time_data else None
@ -599,7 +620,11 @@ class JobCard(Document):
def get_current_operation_data(self): def get_current_operation_data(self):
return frappe.get_all( return frappe.get_all(
"Job Card", "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",
"sum(process_loss_qty) as process_loss_qty",
],
filters={ filters={
"docstatus": 1, "docstatus": 1,
"work_order": self.work_order, "work_order": self.work_order,
@ -777,7 +802,7 @@ class JobCard(Document):
data = frappe.get_all( data = frappe.get_all(
"Work Order Operation", "Work Order Operation",
fields=["operation", "status", "completed_qty"], fields=["operation", "status", "completed_qty", "sequence_id"],
filters={"docstatus": 1, "parent": self.work_order, "sequence_id": ("<", self.sequence_id)}, filters={"docstatus": 1, "parent": self.work_order, "sequence_id": ("<", self.sequence_id)},
order_by="sequence_id, idx", order_by="sequence_id, idx",
) )
@ -795,6 +820,16 @@ class JobCard(Document):
OperationSequenceError, OperationSequenceError,
) )
if row.completed_qty < current_operation_qty:
msg = f"""The completed quantity {bold(current_operation_qty)}
of an operation {bold(self.operation)} cannot be greater
than the completed quantity {bold(row.completed_qty)}
of a previous operation
{bold(row.operation)}.
"""
frappe.throw(_(msg))
def validate_work_order(self): def validate_work_order(self):
if self.is_work_order_closed(): if self.is_work_order_closed():
frappe.throw(_("You can't make any changes to Job Card since Work Order is closed.")) frappe.throw(_("You can't make any changes to Job Card since Work Order is closed."))

View File

@ -139,7 +139,7 @@ frappe.ui.form.on("Work Order", {
} }
if (frm.doc.status != "Closed") { if (frm.doc.status != "Closed") {
if (frm.doc.docstatus === 1 if (frm.doc.docstatus === 1 && frm.doc.status !== "Completed"
&& frm.doc.operations && frm.doc.operations.length) { && frm.doc.operations && frm.doc.operations.length) {
const not_completed = frm.doc.operations.filter(d => { const not_completed = frm.doc.operations.filter(d => {
@ -256,6 +256,12 @@ frappe.ui.form.on("Work Order", {
label: __('Batch Size'), label: __('Batch Size'),
read_only: 1 read_only: 1
}, },
{
fieldtype: 'Int',
fieldname: 'sequence_id',
label: __('Sequence Id'),
read_only: 1
},
], ],
data: operations_data, data: operations_data,
in_place_edit: true, in_place_edit: true,
@ -280,8 +286,8 @@ frappe.ui.form.on("Work Order", {
var pending_qty = 0; var pending_qty = 0;
frm.doc.operations.forEach(data => { frm.doc.operations.forEach(data => {
if(data.completed_qty != frm.doc.qty) { if(data.completed_qty + data.process_loss_qty != frm.doc.qty) {
pending_qty = frm.doc.qty - flt(data.completed_qty); pending_qty = frm.doc.qty - flt(data.completed_qty) - flt(data.process_loss_qty);
if (pending_qty) { if (pending_qty) {
dialog.fields_dict.operations.df.data.push({ dialog.fields_dict.operations.df.data.push({
@ -290,7 +296,8 @@ frappe.ui.form.on("Work Order", {
'workstation': data.workstation, 'workstation': data.workstation,
'batch_size': data.batch_size, 'batch_size': data.batch_size,
'qty': pending_qty, 'qty': pending_qty,
'pending_qty': pending_qty 'pending_qty': pending_qty,
'sequence_id': data.sequence_id
}); });
} }
} }

View File

@ -46,8 +46,8 @@
"required_items_section", "required_items_section",
"materials_and_operations_tab", "materials_and_operations_tab",
"operations_section", "operations_section",
"operations",
"transfer_material_against", "transfer_material_against",
"operations",
"time", "time",
"planned_start_date", "planned_start_date",
"planned_end_date", "planned_end_date",
@ -330,7 +330,6 @@
"label": "Expected Delivery Date" "label": "Expected Delivery Date"
}, },
{ {
"collapsible": 1,
"fieldname": "operations_section", "fieldname": "operations_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Operations", "label": "Operations",
@ -591,7 +590,7 @@
"image_field": "image", "image_field": "image",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-04-06 12:35:12.149827", "modified": "2023-06-09 13:20:09.154362",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Work Order", "name": "Work Order",

View File

@ -245,7 +245,9 @@ class WorkOrder(Document):
status = "Not Started" status = "Not Started"
if flt(self.material_transferred_for_manufacturing) > 0: if flt(self.material_transferred_for_manufacturing) > 0:
status = "In Process" status = "In Process"
if flt(self.produced_qty) >= flt(self.qty):
total_qty = flt(self.produced_qty) + flt(self.process_loss_qty)
if flt(total_qty) >= flt(self.qty):
status = "Completed" status = "Completed"
else: else:
status = "Cancelled" status = "Cancelled"
@ -761,13 +763,15 @@ class WorkOrder(Document):
max_allowed_qty_for_wo = flt(self.qty) + (allowance_percentage / 100 * flt(self.qty)) max_allowed_qty_for_wo = flt(self.qty) + (allowance_percentage / 100 * flt(self.qty))
for d in self.get("operations"): for d in self.get("operations"):
if not d.completed_qty: precision = d.precision("completed_qty")
qty = flt(d.completed_qty, precision) + flt(d.process_loss_qty, precision)
if not qty:
d.status = "Pending" d.status = "Pending"
elif flt(d.completed_qty) < flt(self.qty): elif flt(qty) < flt(self.qty):
d.status = "Work in Progress" d.status = "Work in Progress"
elif flt(d.completed_qty) == flt(self.qty): elif flt(qty) == flt(self.qty):
d.status = "Completed" d.status = "Completed"
elif flt(d.completed_qty) <= max_allowed_qty_for_wo: elif flt(qty) <= max_allowed_qty_for_wo:
d.status = "Completed" d.status = "Completed"
else: else:
frappe.throw(_("Completed Qty cannot be greater than 'Qty to Manufacture'")) frappe.throw(_("Completed Qty cannot be greater than 'Qty to Manufacture'"))

View File

@ -2,12 +2,14 @@
"actions": [], "actions": [],
"creation": "2014-10-16 14:35:41.950175", "creation": "2014-10-16 14:35:41.950175",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"details", "details",
"operation", "operation",
"status", "status",
"completed_qty", "completed_qty",
"process_loss_qty",
"column_break_4", "column_break_4",
"bom", "bom",
"workstation_type", "workstation_type",
@ -36,6 +38,7 @@
"fieldtype": "Section Break" "fieldtype": "Section Break"
}, },
{ {
"columns": 2,
"fieldname": "operation", "fieldname": "operation",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
@ -46,6 +49,7 @@
"reqd": 1 "reqd": 1
}, },
{ {
"columns": 2,
"fieldname": "bom", "fieldname": "bom",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
@ -62,7 +66,7 @@
"oldfieldtype": "Text" "oldfieldtype": "Text"
}, },
{ {
"columns": 1, "columns": 2,
"description": "Operation completed for how many finished goods?", "description": "Operation completed for how many finished goods?",
"fieldname": "completed_qty", "fieldname": "completed_qty",
"fieldtype": "Float", "fieldtype": "Float",
@ -80,6 +84,7 @@
"options": "Pending\nWork in Progress\nCompleted" "options": "Pending\nWork in Progress\nCompleted"
}, },
{ {
"columns": 1,
"fieldname": "workstation", "fieldname": "workstation",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
@ -115,7 +120,7 @@
"fieldname": "time_in_mins", "fieldname": "time_in_mins",
"fieldtype": "Float", "fieldtype": "Float",
"in_list_view": 1, "in_list_view": 1,
"label": "Operation Time", "label": "Time",
"oldfieldname": "time_in_mins", "oldfieldname": "time_in_mins",
"oldfieldtype": "Currency", "oldfieldtype": "Currency",
"reqd": 1 "reqd": 1
@ -203,12 +208,21 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Workstation Type", "label": "Workstation Type",
"options": "Workstation Type" "options": "Workstation Type"
},
{
"columns": 2,
"fieldname": "process_loss_qty",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Process Loss Qty",
"no_copy": 1,
"read_only": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2022-11-09 01:37:56.563068", "modified": "2023-06-09 14:03:01.612909",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Work Order Operation", "name": "Work Order Operation",

View File

@ -656,6 +656,21 @@ frappe.ui.form.on('Stock Entry', {
}); });
} }
}, },
process_loss_qty(frm) {
if (frm.doc.process_loss_qty) {
frm.doc.process_loss_percentage = flt(frm.doc.process_loss_qty / frm.doc.fg_completed_qty * 100, precision("process_loss_qty", frm.doc));
refresh_field("process_loss_percentage");
}
},
process_loss_percentage(frm) {
debugger
if (frm.doc.process_loss_percentage) {
frm.doc.process_loss_qty = flt((frm.doc.fg_completed_qty * frm.doc.process_loss_percentage) / 100 , precision("process_loss_qty", frm.doc));
refresh_field("process_loss_qty");
}
}
}); });
frappe.ui.form.on('Stock Entry Detail', { frappe.ui.form.on('Stock Entry Detail', {

View File

@ -24,6 +24,7 @@
"company", "company",
"posting_date", "posting_date",
"posting_time", "posting_time",
"column_break_eaoa",
"set_posting_time", "set_posting_time",
"inspection_required", "inspection_required",
"apply_putaway_rule", "apply_putaway_rule",
@ -640,16 +641,16 @@
}, },
{ {
"collapsible": 1, "collapsible": 1,
"depends_on": "eval: doc.fg_completed_qty > 0 && in_list([\"Manufacture\", \"Repack\"], doc.purpose)",
"fieldname": "section_break_7qsm", "fieldname": "section_break_7qsm",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Process Loss" "label": "Process Loss"
}, },
{ {
"depends_on": "process_loss_percentage", "depends_on": "eval: doc.fg_completed_qty > 0 && in_list([\"Manufacture\", \"Repack\"], doc.purpose)",
"fieldname": "process_loss_qty", "fieldname": "process_loss_qty",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Process Loss Qty", "label": "Process Loss Qty"
"read_only": 1
}, },
{ {
"fieldname": "column_break_e92r", "fieldname": "column_break_e92r",
@ -657,8 +658,6 @@
}, },
{ {
"depends_on": "eval:doc.from_bom && doc.fg_completed_qty", "depends_on": "eval:doc.from_bom && doc.fg_completed_qty",
"fetch_from": "bom_no.process_loss_percentage",
"fetch_if_empty": 1,
"fieldname": "process_loss_percentage", "fieldname": "process_loss_percentage",
"fieldtype": "Percent", "fieldtype": "Percent",
"label": "% Process Loss" "label": "% Process Loss"
@ -667,6 +666,10 @@
"fieldname": "items_section", "fieldname": "items_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Items" "label": "Items"
},
{
"fieldname": "column_break_eaoa",
"fieldtype": "Column Break"
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
@ -674,7 +677,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-04-06 12:42:56.673180", "modified": "2023-06-09 15:46:28.418339",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Entry", "name": "Stock Entry",

View File

@ -442,13 +442,16 @@ class StockEntry(StockController):
if self.purpose == "Manufacture" and self.work_order: if self.purpose == "Manufacture" and self.work_order:
for d in self.items: for d in self.items:
if d.is_finished_item: if d.is_finished_item:
if self.process_loss_qty:
d.qty = self.fg_completed_qty - self.process_loss_qty
item_wise_qty.setdefault(d.item_code, []).append(d.qty) item_wise_qty.setdefault(d.item_code, []).append(d.qty)
precision = frappe.get_precision("Stock Entry Detail", "qty") precision = frappe.get_precision("Stock Entry Detail", "qty")
for item_code, qty_list in item_wise_qty.items(): for item_code, qty_list in item_wise_qty.items():
total = flt(sum(qty_list), precision) total = flt(sum(qty_list), precision)
if (self.fg_completed_qty - total) > 0: if (self.fg_completed_qty - total) > 0 and not self.process_loss_qty:
self.process_loss_qty = flt(self.fg_completed_qty - total, precision) self.process_loss_qty = flt(self.fg_completed_qty - total, precision)
self.process_loss_percentage = flt(self.process_loss_qty * 100 / self.fg_completed_qty) self.process_loss_percentage = flt(self.process_loss_qty * 100 / self.fg_completed_qty)
@ -1640,16 +1643,36 @@ class StockEntry(StockController):
if self.purpose not in ("Manufacture", "Repack"): if self.purpose not in ("Manufacture", "Repack"):
return return
self.process_loss_qty = 0.0 precision = self.precision("process_loss_qty")
if not self.process_loss_percentage: if self.work_order:
data = frappe.get_all(
"Work Order Operation",
filters={"parent": self.work_order},
fields=["max(process_loss_qty) as process_loss_qty"],
)
if data and data[0].process_loss_qty is not None:
process_loss_qty = data[0].process_loss_qty
if flt(self.process_loss_qty, precision) != flt(process_loss_qty, precision):
self.process_loss_qty = flt(process_loss_qty, precision)
frappe.msgprint(
_("The Process Loss Qty has reset as per job cards Process Loss Qty"), alert=True
)
if not self.process_loss_percentage and not self.process_loss_qty:
self.process_loss_percentage = frappe.get_cached_value( self.process_loss_percentage = frappe.get_cached_value(
"BOM", self.bom_no, "process_loss_percentage" "BOM", self.bom_no, "process_loss_percentage"
) )
if self.process_loss_percentage: if self.process_loss_percentage and not self.process_loss_qty:
self.process_loss_qty = flt( self.process_loss_qty = flt(
(flt(self.fg_completed_qty) * flt(self.process_loss_percentage)) / 100 (flt(self.fg_completed_qty) * flt(self.process_loss_percentage)) / 100
) )
else:
self.process_loss_percentage = flt(
(flt(self.process_loss_qty) / flt(self.fg_completed_qty)) * 100
)
def set_work_order_details(self): def set_work_order_details(self):
if not getattr(self, "pro_doc", None): if not getattr(self, "pro_doc", None):