Skip to content

Commit

Permalink
Merge pull request #35658 from frappe/mergify/bp/version-14-hotfix/pr…
Browse files Browse the repository at this point in the history
…-35629

fix: added process loss in job card (backport #35629)
  • Loading branch information
rohitwaghchaure committed Jun 13, 2023
2 parents 9d18b40 + 2060a00 commit 74ffb1d
Show file tree
Hide file tree
Showing 13 changed files with 275 additions and 41 deletions.
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

0 comments on commit 74ffb1d

Please sign in to comment.