Merge pull request #35629 from rohitwaghchaure/fixed-process-loss-in-job-card

fix: added process loss in job card
This commit is contained in:
rohitwaghchaure 2023-06-12 23:29:15 +05:30 committed by GitHub
commit 4ee08b92ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 274 additions and 40 deletions

View File

@ -83,7 +83,7 @@ frappe.ui.form.on('Job Card', {
// and if stock mvt for WIP is required
if (frm.doc.work_order) {
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");
}
});
@ -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");
}
});

View File

@ -39,6 +39,7 @@
"time_logs",
"section_break_13",
"total_completed_qty",
"process_loss_qty",
"column_break_15",
"total_time_in_mins",
"section_break_8",
@ -448,11 +449,17 @@
"no_copy": 1,
"options": "Serial and Batch Bundle",
"print_hide": 1
},
{
"fieldname": "process_loss_qty",
"fieldtype": "Float",
"label": "Process Loss Qty",
"read_only": 1
}
],
"is_submittable": 1,
"links": [],
"modified": "2023-05-23 09:56:43.826602",
"modified": "2023-06-09 12:04:55.534264",
"modified_by": "Administrator",
"module": "Manufacturing",
"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"))
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):
production_capacity = 1
@ -451,6 +451,9 @@ class JobCard(Document):
},
)
def before_save(self):
self.set_process_loss()
def on_submit(self):
self.validate_transfer_qty()
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"))
qty_to_manufacture = bold(_("Qty to Manufacture"))
frappe.throw(
_("The {0} ({1}) must be equal to {2} ({3})").format(
total_completed_qty,
bold(self.total_completed_qty),
bold(flt(total_completed_qty, precision)),
qty_to_manufacture,
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):
if not self.work_order:
return
@ -511,7 +530,7 @@ class JobCard(Document):
):
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 = [], []
field = "operation_id"
@ -519,6 +538,7 @@ class JobCard(Document):
if data and len(data) > 0:
for_quantity = flt(data[0].completed_qty)
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)
@ -526,8 +546,8 @@ class JobCard(Document):
self.update_corrective_in_work_order(wo)
elif self.operation_id:
self.validate_produced_quantity(for_quantity, wo)
self.update_work_order_data(for_quantity, time_in_mins, wo)
self.validate_produced_quantity(for_quantity, process_loss_qty, wo)
self.update_work_order_data(for_quantity, process_loss_qty, time_in_mins, wo)
def update_corrective_in_work_order(self, wo):
wo.corrective_operation_cost = 0.0
@ -542,11 +562,11 @@ class JobCard(Document):
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, process_loss_qty, wo):
if self.docstatus < 2:
return
if wo.produced_qty > for_quantity:
if wo.produced_qty > for_quantity + process_loss_qty:
first_part_msg = _(
"The {0} {1} is used to calculate the valuation cost for the finished good {2}."
).format(
@ -561,7 +581,7 @@ class JobCard(Document):
_("{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")
jc = frappe.qb.DocType("Job Card")
jctl = frappe.qb.DocType("Job Card Time Log")
@ -582,6 +602,7 @@ class JobCard(Document):
for data in wo.operations:
if data.get("name") == self.operation_id:
data.completed_qty = for_quantity
data.process_loss_qty = process_loss_qty
data.actual_operation_time = time_in_mins
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
@ -599,7 +620,11 @@ class JobCard(Document):
def get_current_operation_data(self):
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",
"sum(process_loss_qty) as process_loss_qty",
],
filters={
"docstatus": 1,
"work_order": self.work_order,
@ -777,7 +802,7 @@ class JobCard(Document):
data = frappe.get_all(
"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)},
order_by="sequence_id, idx",
)
@ -795,6 +820,16 @@ class JobCard(Document):
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):
if self.is_work_order_closed():
frappe.throw(_("You can't make any changes to Job Card since Work Order is closed."))

View File

@ -5,6 +5,7 @@
from typing import Literal
import frappe
from frappe.test_runner import make_test_records
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import random_string
from frappe.utils.data import add_to_date, now, today
@ -469,6 +470,119 @@ class TestJobCard(FrappeTestCase):
self.assertEqual(ste.from_bom, 1.0)
self.assertEqual(ste.bom_no, work_order.bom_no)
def test_job_card_proccess_qty_and_completed_qty(self):
from erpnext.manufacturing.doctype.routing.test_routing import (
create_routing,
setup_bom,
setup_operations,
)
from erpnext.manufacturing.doctype.work_order.work_order import (
make_stock_entry as make_stock_entry_for_wo,
)
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
operations = [
{"operation": "Test Operation A1", "workstation": "Test Workstation A", "time_in_mins": 30},
{"operation": "Test Operation B1", "workstation": "Test Workstation A", "time_in_mins": 20},
]
make_test_records("UOM")
warehouse = create_warehouse("Test Warehouse 123 for Job Card")
setup_operations(operations)
item_code = "Test Job Card Process Qty Item"
for item in [item_code, item_code + "RM 1", item_code + "RM 2"]:
if not frappe.db.exists("Item", item):
make_item(
item,
{
"item_name": item,
"stock_uom": "Nos",
"is_stock_item": 1,
},
)
routing_doc = create_routing(routing_name="Testing Route", operations=operations)
bom_doc = setup_bom(
item_code=item_code,
routing=routing_doc.name,
raw_materials=[item_code + "RM 1", item_code + "RM 2"],
source_warehouse=warehouse,
)
for row in bom_doc.items:
make_stock_entry(
item_code=row.item_code,
target=row.source_warehouse,
qty=10,
basic_rate=100,
)
wo_doc = make_wo_order_test_record(
production_item=item_code,
bom_no=bom_doc.name,
skip_transfer=1,
wip_warehouse=warehouse,
source_warehouse=warehouse,
)
for row in routing_doc.operations:
self.assertEqual(row.sequence_id, row.idx)
first_job_card = frappe.get_all(
"Job Card",
filters={"work_order": wo_doc.name, "sequence_id": 1},
fields=["name"],
order_by="sequence_id",
limit=1,
)[0].name
jc = frappe.get_doc("Job Card", first_job_card)
jc.time_logs[0].completed_qty = 8
jc.save()
jc.submit()
self.assertEqual(jc.process_loss_qty, 2)
self.assertEqual(jc.for_quantity, 10)
second_job_card = frappe.get_all(
"Job Card",
filters={"work_order": wo_doc.name, "sequence_id": 2},
fields=["name"],
order_by="sequence_id",
limit=1,
)[0].name
jc2 = frappe.get_doc("Job Card", second_job_card)
jc2.time_logs[0].completed_qty = 10
self.assertRaises(frappe.ValidationError, jc2.save)
jc2.load_from_db()
jc2.time_logs[0].completed_qty = 8
jc2.save()
jc2.submit()
self.assertEqual(jc2.for_quantity, 10)
self.assertEqual(jc2.process_loss_qty, 2)
s = frappe.get_doc(make_stock_entry_for_wo(wo_doc.name, "Manufacture", 10))
s.submit()
self.assertEqual(s.process_loss_qty, 2)
wo_doc.reload()
for row in wo_doc.operations:
self.assertEqual(row.completed_qty, 8)
self.assertEqual(row.process_loss_qty, 2)
self.assertEqual(wo_doc.produced_qty, 8)
self.assertEqual(wo_doc.process_loss_qty, 2)
self.assertEqual(wo_doc.status, "Completed")
def create_bom_with_multiple_operations():
"Create a BOM with multiple operations and Material Transfer against Job Card"

View File

@ -141,6 +141,7 @@ def setup_bom(**args):
routing=args.routing,
with_operations=1,
currency=args.currency,
source_warehouse=args.source_warehouse,
)
else:
bom_doc = frappe.get_doc("BOM", name)

View File

@ -903,7 +903,7 @@ class TestWorkOrder(FrappeTestCase):
self.assertEqual(se.process_loss_qty, 1)
wo.load_from_db()
self.assertEqual(wo.status, "In Process")
self.assertEqual(wo.status, "Completed")
@timeout(seconds=60)
def test_job_card_scrap_item(self):

View File

@ -139,7 +139,7 @@ frappe.ui.form.on("Work Order", {
}
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) {
const not_completed = frm.doc.operations.filter(d => {
@ -256,6 +256,12 @@ frappe.ui.form.on("Work Order", {
label: __('Batch Size'),
read_only: 1
},
{
fieldtype: 'Int',
fieldname: 'sequence_id',
label: __('Sequence Id'),
read_only: 1
},
],
data: operations_data,
in_place_edit: true,
@ -280,8 +286,8 @@ frappe.ui.form.on("Work Order", {
var pending_qty = 0;
frm.doc.operations.forEach(data => {
if(data.completed_qty != frm.doc.qty) {
pending_qty = frm.doc.qty - flt(data.completed_qty);
if(data.completed_qty + data.process_loss_qty != frm.doc.qty) {
pending_qty = frm.doc.qty - flt(data.completed_qty) - flt(data.process_loss_qty);
if (pending_qty) {
dialog.fields_dict.operations.df.data.push({
@ -290,7 +296,8 @@ frappe.ui.form.on("Work Order", {
'workstation': data.workstation,
'batch_size': data.batch_size,
'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",
"materials_and_operations_tab",
"operations_section",
"operations",
"transfer_material_against",
"operations",
"time",
"planned_start_date",
"planned_end_date",
@ -330,7 +330,6 @@
"label": "Expected Delivery Date"
},
{
"collapsible": 1,
"fieldname": "operations_section",
"fieldtype": "Section Break",
"label": "Operations",
@ -591,7 +590,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
"modified": "2023-04-06 12:35:12.149827",
"modified": "2023-06-09 13:20:09.154362",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Work Order",

View File

@ -245,7 +245,9 @@ class WorkOrder(Document):
status = "Not Started"
if flt(self.material_transferred_for_manufacturing) > 0:
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"
else:
status = "Cancelled"
@ -761,13 +763,15 @@ class WorkOrder(Document):
max_allowed_qty_for_wo = flt(self.qty) + (allowance_percentage / 100 * flt(self.qty))
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"
elif flt(d.completed_qty) < flt(self.qty):
elif flt(qty) < flt(self.qty):
d.status = "Work in Progress"
elif flt(d.completed_qty) == flt(self.qty):
elif flt(qty) == flt(self.qty):
d.status = "Completed"
elif flt(d.completed_qty) <= max_allowed_qty_for_wo:
elif flt(qty) <= max_allowed_qty_for_wo:
d.status = "Completed"
else:
frappe.throw(_("Completed Qty cannot be greater than 'Qty to Manufacture'"))

View File

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

View File

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

View File

@ -442,13 +442,16 @@ class StockEntry(StockController):
if self.purpose == "Manufacture" and self.work_order:
for d in self.items:
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)
precision = frappe.get_precision("Stock Entry Detail", "qty")
for item_code, qty_list in item_wise_qty.items():
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_percentage = flt(self.process_loss_qty * 100 / self.fg_completed_qty)
@ -578,7 +581,9 @@ class StockEntry(StockController):
for d in prod_order.get("operations"):
total_completed_qty = flt(self.fg_completed_qty) + flt(prod_order.produced_qty)
completed_qty = d.completed_qty + (allowance_percentage / 100 * d.completed_qty)
completed_qty = (
d.completed_qty + d.process_loss_qty + (allowance_percentage / 100 * d.completed_qty)
)
if total_completed_qty > flt(completed_qty):
job_card = frappe.db.get_value("Job Card", {"operation_id": d.name}, "name")
if not job_card:
@ -1640,16 +1645,36 @@ class StockEntry(StockController):
if self.purpose not in ("Manufacture", "Repack"):
return
self.process_loss_qty = 0.0
if not self.process_loss_percentage:
precision = self.precision("process_loss_qty")
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(
"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(
(flt(self.fg_completed_qty) * flt(self.process_loss_percentage)) / 100
)
elif self.process_loss_qty and not self.process_loss_percentage:
self.process_loss_percentage = flt(
(flt(self.process_loss_qty) / flt(self.fg_completed_qty)) * 100
)
def set_work_order_details(self):
if not getattr(self, "pro_doc", None):