mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-13 22:51:49 +00:00
fix(manufacturing): refine corrective job cards (#58079)
This commit is contained in:
@@ -96,6 +96,41 @@ frappe.ui.form.on("Job Card", {
|
||||
}
|
||||
},
|
||||
|
||||
set_corrective_job_card_labels(frm) {
|
||||
const is_corrective_job_card = Boolean(frm.doc.is_corrective_job_card);
|
||||
|
||||
frm.set_df_property(
|
||||
"for_quantity",
|
||||
"label",
|
||||
is_corrective_job_card ? __("Qty To Correct") : __("Qty To Manufacture")
|
||||
);
|
||||
frm.set_df_property(
|
||||
"total_completed_qty",
|
||||
"label",
|
||||
is_corrective_job_card ? __("Total Corrected Qty") : __("Total Completed Qty")
|
||||
);
|
||||
},
|
||||
|
||||
toggle_material_tables(frm) {
|
||||
const backflush_based_on_material_transfer =
|
||||
frm.doc.__onload?.backflush_raw_materials_based_on === "Material Transferred for Manufacture";
|
||||
const show_material_tables =
|
||||
backflush_based_on_material_transfer ||
|
||||
frm.doc.__onload?.transfer_material_against === "Job Card" ||
|
||||
Boolean(frm.doc.track_semi_finished_goods) ||
|
||||
Boolean(frm.doc.items?.length) ||
|
||||
Boolean(frm.doc.secondary_items?.length);
|
||||
|
||||
frm.toggle_display(
|
||||
["section_break_8", "items", "secondary_items_section", "secondary_items"],
|
||||
show_material_tables
|
||||
);
|
||||
},
|
||||
|
||||
track_semi_finished_goods(frm) {
|
||||
frm.trigger("toggle_material_tables");
|
||||
},
|
||||
|
||||
setup_stock_entry(frm) {
|
||||
const { doc } = frm;
|
||||
const can_make_stock_entry =
|
||||
@@ -140,6 +175,8 @@ frappe.ui.form.on("Job Card", {
|
||||
}
|
||||
|
||||
frm.trigger("make_fields_read_only");
|
||||
frm.trigger("set_corrective_job_card_labels");
|
||||
frm.trigger("toggle_material_tables");
|
||||
|
||||
if (!frm.is_new() && doc.__onload?.work_order_closed) {
|
||||
frm.disable_save();
|
||||
@@ -464,6 +501,7 @@ frappe.ui.form.on("Job Card", {
|
||||
label: __("Corrective Operation"),
|
||||
options: "Operation",
|
||||
fieldname: "operation",
|
||||
reqd: 1,
|
||||
get_query() {
|
||||
return { filters: { is_corrective_operation: 1 } };
|
||||
},
|
||||
@@ -473,6 +511,7 @@ frappe.ui.form.on("Job Card", {
|
||||
label: __("For Operation"),
|
||||
options: "Operation",
|
||||
fieldname: "for_operation",
|
||||
reqd: 1,
|
||||
get_query() {
|
||||
return { filters: { name: ["in", operations] } };
|
||||
},
|
||||
@@ -482,10 +521,11 @@ frappe.ui.form.on("Job Card", {
|
||||
frappe.prompt(
|
||||
fields,
|
||||
(d) => frm.events.make_corrective_job_card(frm, d.operation, d.for_operation),
|
||||
__("Select Corrective Operation")
|
||||
__("Select Corrective Operation"),
|
||||
__("Create")
|
||||
);
|
||||
},
|
||||
__("Make")
|
||||
__("Create")
|
||||
);
|
||||
},
|
||||
|
||||
@@ -816,6 +856,10 @@ frappe.ui.form.on("Job Card", {
|
||||
},
|
||||
|
||||
for_quantity(frm) {
|
||||
if (frm.doc.is_corrective_job_card) {
|
||||
return;
|
||||
}
|
||||
|
||||
frm.doc.items = [];
|
||||
frm.call({
|
||||
method: "get_required_items",
|
||||
|
||||
@@ -252,6 +252,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:!doc.is_corrective_job_card",
|
||||
"fieldname": "requested_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Requested Qty",
|
||||
@@ -552,6 +553,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:!doc.is_corrective_job_card",
|
||||
"fieldname": "is_subcontracted",
|
||||
"fieldtype": "Check",
|
||||
"label": " Is Subcontracted",
|
||||
@@ -604,6 +606,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:!doc.is_corrective_job_card",
|
||||
"fetch_from": "work_order.track_semi_finished_goods",
|
||||
"fieldname": "track_semi_finished_goods",
|
||||
"fieldtype": "Check",
|
||||
@@ -697,7 +700,7 @@
|
||||
"grid_page_length": 50,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-08-01 14:22:19.926911",
|
||||
"modified": "2026-08-12 15:28:19.126628",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Job Card",
|
||||
|
||||
@@ -27,7 +27,11 @@ from erpnext.controllers.stock_controller import (
|
||||
QualityInspectionNotSubmittedError,
|
||||
QualityInspectionRejectedError,
|
||||
)
|
||||
from erpnext.manufacturing.doctype.bom.bom import add_additional_cost, get_bom_items_as_dict
|
||||
from erpnext.manufacturing.doctype.bom.bom import (
|
||||
add_additional_cost,
|
||||
get_backflush_based_on,
|
||||
get_bom_items_as_dict,
|
||||
)
|
||||
from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import (
|
||||
get_mins_between_operations,
|
||||
)
|
||||
@@ -148,6 +152,11 @@ class JobCard(Document):
|
||||
def onload(self):
|
||||
excess_transfer = frappe.db.get_single_value("Manufacturing Settings", "job_card_excess_transfer")
|
||||
self.set_onload("job_card_excess_transfer", excess_transfer)
|
||||
self.set_onload("backflush_raw_materials_based_on", get_backflush_based_on(self.bom_no))
|
||||
self.set_onload(
|
||||
"transfer_material_against",
|
||||
frappe.get_cached_value("Work Order", self.work_order, "transfer_material_against"),
|
||||
)
|
||||
self.set_onload("work_order_closed", self.is_work_order_closed())
|
||||
self.set_onload("has_stock_entry", self.has_stock_entry())
|
||||
|
||||
@@ -786,7 +795,7 @@ class JobCard(Document):
|
||||
def get_required_items(self):
|
||||
frappe.has_permission("Job Card", "write", doc=self, throw=True)
|
||||
|
||||
if not self.get("work_order"):
|
||||
if self.is_corrective_job_card or not self.get("work_order"):
|
||||
return
|
||||
|
||||
doc = frappe.get_doc("Work Order", self.get("work_order"))
|
||||
@@ -806,11 +815,7 @@ class JobCard(Document):
|
||||
)
|
||||
)
|
||||
|
||||
if not (
|
||||
self.get("operation") == d.operation
|
||||
or self.operation_row_id == d.operation_row_id
|
||||
or self.is_corrective_job_card
|
||||
):
|
||||
if not (self.get("operation") == d.operation or self.operation_row_id == d.operation_row_id):
|
||||
return
|
||||
|
||||
self.append(
|
||||
|
||||
@@ -7,6 +7,7 @@ from frappe.model.document import Document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.utils import flt
|
||||
|
||||
from erpnext.manufacturing.doctype.bom.bom import get_backflush_based_on
|
||||
from erpnext.subcontracting.doctype.subcontracting_bom.subcontracting_bom import (
|
||||
get_subcontracting_boms_for_finished_goods,
|
||||
)
|
||||
@@ -164,17 +165,33 @@ def make_corrective_job_card(
|
||||
for_operation: str | None = None,
|
||||
target_doc: str | dict | Document | None = None,
|
||||
):
|
||||
if not operation:
|
||||
frappe.throw(_("Corrective Operation is required"))
|
||||
|
||||
if not for_operation:
|
||||
frappe.throw(_("For Operation is required"))
|
||||
|
||||
def set_missing_values(source, target):
|
||||
if source.track_semi_finished_goods:
|
||||
frappe.throw(
|
||||
_("Corrective Job Cards cannot be created for Work Orders that track semi-finished goods")
|
||||
)
|
||||
|
||||
target.is_corrective_job_card = 1
|
||||
target.operation = operation
|
||||
target.for_operation = for_operation
|
||||
target.total_completed_qty = 0
|
||||
|
||||
target.set("time_logs", [])
|
||||
target.set("employee", [])
|
||||
target.set("items", [])
|
||||
target.set("sub_operations", [])
|
||||
target.set_sub_operations()
|
||||
target.get_required_items()
|
||||
target.set_onload("backflush_raw_materials_based_on", get_backflush_based_on(target.bom_no))
|
||||
target.set_onload(
|
||||
"transfer_material_against",
|
||||
frappe.get_cached_value("Work Order", target.work_order, "transfer_material_against"),
|
||||
)
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
"Job Card",
|
||||
|
||||
@@ -582,6 +582,102 @@ class TestJobCard(ERPNextTestSuite):
|
||||
work_order.reload()
|
||||
self.assertEqual(work_order.material_transferred_for_manufacturing, min(completed_qty))
|
||||
|
||||
def test_corrective_job_card_requires_operation_details(self):
|
||||
job_card = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name})
|
||||
|
||||
with self.assertRaisesRegex(frappe.ValidationError, "Corrective Operation is required"):
|
||||
make_corrective_job_card(job_card.name, for_operation=job_card.operation)
|
||||
|
||||
with self.assertRaisesRegex(frappe.ValidationError, "For Operation is required"):
|
||||
make_corrective_job_card(job_card.name, operation=job_card.operation)
|
||||
|
||||
def test_corrective_job_card_not_allowed_for_tracked_semi_finished_goods(self):
|
||||
job_card = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name})
|
||||
job_card.db_set("track_semi_finished_goods", 1)
|
||||
|
||||
with self.assertRaisesRegex(frappe.ValidationError, "track semi-finished goods"):
|
||||
make_corrective_job_card(
|
||||
job_card.name,
|
||||
operation=job_card.operation,
|
||||
for_operation=job_card.operation,
|
||||
)
|
||||
|
||||
def test_corrective_job_card_does_not_copy_total_completed_qty(self):
|
||||
job_card = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name})
|
||||
job_card.append(
|
||||
"time_logs",
|
||||
{"from_time": now(), "to_time": add_to_date(now(), hours=1), "completed_qty": 1},
|
||||
)
|
||||
job_card.save()
|
||||
self.assertEqual(job_card.total_completed_qty, 1)
|
||||
|
||||
corrective_job_card = make_corrective_job_card(
|
||||
job_card.name,
|
||||
operation=job_card.operation,
|
||||
for_operation=job_card.operation,
|
||||
)
|
||||
|
||||
self.assertEqual(corrective_job_card.total_completed_qty, 0)
|
||||
|
||||
@ERPNextTestSuite.change_settings(
|
||||
"Manufacturing Settings", {"backflush_raw_materials_based_on": "Material Transferred for Manufacture"}
|
||||
)
|
||||
def test_corrective_job_card_does_not_autofill_items(self):
|
||||
self.transfer_material_against = "Job Card"
|
||||
job_card = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name})
|
||||
frappe.db.set_value("BOM", job_card.bom_no, "backflush_based_on", "")
|
||||
self.assertTrue(job_card.items)
|
||||
|
||||
corrective_job_card = make_corrective_job_card(
|
||||
job_card.name,
|
||||
operation=job_card.operation,
|
||||
for_operation=job_card.operation,
|
||||
)
|
||||
|
||||
self.assertFalse(corrective_job_card.items)
|
||||
corrective_job_card.get_required_items()
|
||||
self.assertFalse(corrective_job_card.items)
|
||||
self.assertEqual(
|
||||
corrective_job_card.get_onload("backflush_raw_materials_based_on"),
|
||||
"Material Transferred for Manufacture",
|
||||
)
|
||||
|
||||
@ERPNextTestSuite.change_settings("Manufacturing Settings", {"backflush_raw_materials_based_on": "BOM"})
|
||||
def test_corrective_job_card_uses_bom_backflush_setting(self):
|
||||
job_card = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name})
|
||||
frappe.db.set_value(
|
||||
"BOM",
|
||||
job_card.bom_no,
|
||||
"backflush_based_on",
|
||||
"Material Transferred for Manufacture",
|
||||
)
|
||||
|
||||
corrective_job_card = make_corrective_job_card(
|
||||
job_card.name,
|
||||
operation=job_card.operation,
|
||||
for_operation=job_card.operation,
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
corrective_job_card.get_onload("backflush_raw_materials_based_on"),
|
||||
"Material Transferred for Manufacture",
|
||||
)
|
||||
|
||||
@ERPNextTestSuite.change_settings("Manufacturing Settings", {"backflush_raw_materials_based_on": "BOM"})
|
||||
def test_corrective_job_card_uses_work_order_transfer_setting(self):
|
||||
self.transfer_material_against = "Job Card"
|
||||
job_card = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name})
|
||||
frappe.db.set_value("BOM", job_card.bom_no, "backflush_based_on", "BOM")
|
||||
|
||||
corrective_job_card = make_corrective_job_card(
|
||||
job_card.name,
|
||||
operation=job_card.operation,
|
||||
for_operation=job_card.operation,
|
||||
)
|
||||
|
||||
self.assertEqual(corrective_job_card.get_onload("backflush_raw_materials_based_on"), "BOM")
|
||||
self.assertEqual(corrective_job_card.get_onload("transfer_material_against"), "Job Card")
|
||||
|
||||
@ERPNextTestSuite.change_settings(
|
||||
"Manufacturing Settings",
|
||||
{
|
||||
|
||||
@@ -196,9 +196,13 @@ frappe.ui.form.on("Work Order", {
|
||||
frm.doc.operations.length
|
||||
) {
|
||||
if (frm.doc.__onload?.show_create_job_card_button) {
|
||||
frm.add_custom_button(__("Create Job Card"), () => {
|
||||
frm.trigger("make_job_card");
|
||||
});
|
||||
frm.add_custom_button(
|
||||
__("Create Job Card"),
|
||||
() => {
|
||||
frm.trigger("make_job_card");
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -211,6 +215,7 @@ frappe.ui.form.on("Work Order", {
|
||||
frappe.set_route("shop-floor");
|
||||
});
|
||||
}
|
||||
erpnext.work_order.add_start_button(frm);
|
||||
|
||||
if (frm.doc.status == "Completed") {
|
||||
if (frm.doc.__onload.backflush_raw_materials_based_on == "Material Transferred for Manufacture") {
|
||||
@@ -764,6 +769,7 @@ frappe.ui.form.on("Work Order Operation", {
|
||||
erpnext.work_order = {
|
||||
set_custom_buttons: function (frm) {
|
||||
var doc = frm.doc;
|
||||
frm.has_start_btn = false;
|
||||
|
||||
if (doc.docstatus === 1 && !["Closed", "Completed"].includes(doc.status)) {
|
||||
frm.add_custom_button(
|
||||
@@ -833,11 +839,6 @@ erpnext.work_order = {
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
|
||||
var start_btn = frm.add_custom_button(__("Start"), function () {
|
||||
erpnext.work_order.make_se(frm, "Material Transfer for Manufacture");
|
||||
});
|
||||
start_btn.addClass("btn-primary");
|
||||
} else if (transfer_extra_materials && allowed_qty) {
|
||||
let qty =
|
||||
allowed_qty -
|
||||
@@ -951,6 +952,17 @@ erpnext.work_order = {
|
||||
}
|
||||
},
|
||||
|
||||
add_start_button(frm) {
|
||||
if (!frm.has_start_btn) {
|
||||
return;
|
||||
}
|
||||
|
||||
const start_btn = frm.add_custom_button(__("Start"), () => {
|
||||
erpnext.work_order.make_se(frm, "Material Transfer for Manufacture");
|
||||
});
|
||||
start_btn.addClass("btn-primary");
|
||||
},
|
||||
|
||||
setup_stock_reservation(frm) {
|
||||
if (frm.doc.docstatus === 1 && frm.doc.reserve_stock) {
|
||||
if (
|
||||
|
||||
Reference in New Issue
Block a user