Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: added process loss in job card (backport #35629) #35658

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion erpnext/manufacturing/doctype/job_card/job_card.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
});
Expand Down Expand Up @@ -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");
}
});
Expand Down
11 changes: 9 additions & 2 deletions erpnext/manufacturing/doctype/job_card/job_card.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"time_logs",
"section_break_13",
"total_completed_qty",
"process_loss_qty",
"column_break_15",
"total_time_in_mins",
"section_break_8",
Expand Down Expand Up @@ -435,11 +436,17 @@
"fieldname": "expected_end_date",
"fieldtype": "Datetime",
"label": "Expected End Date"
},
{
"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",
Expand Down Expand Up @@ -497,4 +504,4 @@
"states": [],
"title_field": "operation",
"track_changes": 1
}
}
57 changes: 46 additions & 11 deletions erpnext/manufacturing/doctype/job_card/job_card.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ def validate_time_logs(self):
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
Expand Down Expand Up @@ -451,6 +451,9 @@ def get_required_items(self):
},
)

def before_save(self):
self.set_process_loss()

def on_submit(self):
self.validate_transfer_qty()
self.validate_job_card()
Expand Down Expand Up @@ -487,19 +490,35 @@ def validate_job_card(self):
)
)

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
Expand All @@ -511,23 +530,24 @@ def update_work_order(self):
):
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"
data = self.get_current_operation_data()
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)

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.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
Expand All @@ -542,11 +562,11 @@ def update_corrective_in_work_order(self, wo):
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(
Expand All @@ -561,7 +581,7 @@ def validate_produced_quantity(self, for_quantity, wo):
_("{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")
Expand All @@ -582,6 +602,7 @@ def update_work_order_data(self, for_quantity, time_in_mins, wo):
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
Expand All @@ -599,7 +620,11 @@ def update_work_order_data(self, for_quantity, time_in_mins, wo):
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,
Expand Down Expand Up @@ -777,7 +802,7 @@ def validate_sequence_id(self):

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",
)
Expand All @@ -795,6 +820,16 @@ def validate_sequence_id(self):
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."))
Expand Down
114 changes: 114 additions & 0 deletions erpnext/manufacturing/doctype/job_card/test_job_card.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -469,6 +470,119 @@ def test_job_card_material_request_and_bom_details(self):
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"
Expand Down
1 change: 1 addition & 0 deletions erpnext/manufacturing/doctype/routing/test_routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -891,7 +891,7 @@ def test_wo_completion_with_pl_bom(self):
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):
Expand Down
Loading