From b372e6f1183cad21389f58bd3ff877b28ef32302 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 20 May 2026 14:12:44 +0530 Subject: [PATCH] feat: pending qty in job card (cherry picked from commit db64f451c17e3afa050b5eb9c25e11985af4ac82) --- .../doctype/job_card/job_card.js | 75 +++++++++++++++++-- .../doctype/job_card/job_card.json | 46 +++++++++--- .../doctype/job_card/job_card.py | 32 +++++--- .../doctype/job_card/test_job_card.py | 1 + .../doctype/work_order/work_order.py | 19 ++--- .../work_order_operation.json | 11 ++- .../work_order_operation.py | 1 + 7 files changed, 146 insertions(+), 39 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index 19132ecf9fd..ef2682dc634 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -77,6 +77,30 @@ frappe.ui.form.on("Job Card", { }); }, + pending_qty(frm) { + if (frm.doc.total_completed_qty <= 0.0) { + frm.doc.pending_qty = 0.0; + refresh_field("pending_qty"); + frappe.throw(__("Please complete the job first before entering Pending Quantity")); + } + + if (frm.doc.pending_qty < 0) { + frappe.throw(__("Pending Quantity cannot be less than 0")); + } + + let remaining_qty = flt(frm.doc.for_quantity) - flt(frm.doc.total_completed_qty); + + if (remaining_qty < frm.doc.pending_qty) { + frm.doc.pending_qty = 0.0; + refresh_field("pending_qty"); + frappe.throw(__("Pending Quantity cannot be greater than {0}", [remaining_qty])); + } + + let process_loss_qty = flt(remaining_qty) - flt(frm.doc.pending_qty); + frm.doc.process_loss_qty = process_loss_qty >= 0 ? process_loss_qty : 0; + refresh_field("process_loss_qty"); + }, + set_company_filters(frm, fieldname) { frm.set_query(fieldname, () => { return { @@ -150,6 +174,10 @@ frappe.ui.form.on("Job Card", { return; } + if (frm.doc.docstatus > 0) { + frm.set_df_property("pending_qty", "read_only", 1); + } + let has_stock_entry = frm.doc.__onload && frm.doc.__onload.has_stock_entry ? true : false; frm.toggle_enable("for_quantity", !has_stock_entry); @@ -212,12 +240,14 @@ frappe.ui.form.on("Job Card", { !has_items?.length) ) { let last_row = {}; - if (frm.doc.sub_operations?.length && frm.doc.time_logs?.length) { + if ((frm.doc.sub_operations?.length || frm.doc.pending_qty > 0) && frm.doc.time_logs?.length) { last_row = get_last_row(frm.doc.time_logs); } if ( - (!frm.doc.time_logs?.length || (frm.doc.sub_operations?.length && last_row?.to_time)) && + (!frm.doc.time_logs?.length || + (flt(frm.doc.pending_qty) > 0.0 && last_row?.to_time) || + (frm.doc.sub_operations?.length && last_row?.to_time)) && !frm.doc.is_paused ) { frm.add_custom_button(__("Start Job"), () => { @@ -306,13 +336,18 @@ frappe.ui.form.on("Job Card", { }, complete_job_card(frm) { + let pending_qty = frm.doc.for_quantity - frm.doc.total_completed_qty; + if (frm.doc.pending_qty > 0) { + pending_qty = frm.doc.pending_qty; + } + let fields = [ { fieldtype: "Float", label: __("Qty to Manufacture"), fieldname: "for_quantity", reqd: 1, - default: frm.doc.for_quantity, + default: pending_qty, change() { let doc = frm.job_completion_dialog; @@ -325,12 +360,29 @@ frappe.ui.form.on("Job Card", { label: __("Completed Quantity"), fieldname: "completed_qty", reqd: 1, - default: frm.doc.for_quantity - frm.doc.total_completed_qty, + default: pending_qty, change() { let doc = frm.job_completion_dialog; - let process_loss_qty = doc.get_value("for_quantity") - doc.get_value("completed_qty"); - if (process_loss_qty > 0 && process_loss_qty != doc.get_value("process_loss_qty")) { + let pending_qty = doc.get_value("for_quantity") - doc.get_value("completed_qty"); + if (pending_qty > 0 && pending_qty != doc.get_value("pending_qty")) { + doc.set_value("pending_qty", pending_qty); + } + }, + }, + { + fieldtype: "Float", + label: __("Pending Quantity"), + fieldname: "pending_qty", + default: 0.0, + change() { + let doc = frm.job_completion_dialog; + + let process_loss_qty = + doc.get_value("for_quantity") - + doc.get_value("completed_qty") - + doc.get_value("pending_qty"); + if (process_loss_qty >= 0 && process_loss_qty != doc.get_value("process_loss_qty")) { doc.set_value("process_loss_qty", process_loss_qty); } }, @@ -342,8 +394,13 @@ frappe.ui.form.on("Job Card", { onchange() { let doc = frm.job_completion_dialog; - let completed_qty = doc.get_value("for_quantity") - doc.get_value("process_loss_qty"); - doc.set_value("completed_qty", completed_qty); + let pending_qty = + doc.get_value("for_quantity") - + doc.get_value("completed_qty") - + doc.get_value("process_loss_qty"); + if (pending_qty >= 0 && pending_qty != doc.get_value("pending_qty")) { + doc.set_value("pending_qty", pending_qty); + } }, }, { @@ -399,6 +456,8 @@ frappe.ui.form.on("Job Card", { args: { qty: data.completed_qty, for_quantity: data.for_quantity, + pending_qty: data.pending_qty, + process_loss_qty: data.process_loss_qty, end_time: data.end_time, sub_operation: data.sub_operation, }, diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json index 1e4af027c30..b9236d2ba00 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.json +++ b/erpnext/manufacturing/doctype/job_card/job_card.json @@ -9,23 +9,26 @@ "field_order": [ "company", "naming_series", - "work_order", + "production_item", "employee", "column_break_4", "posting_date", - "project", + "work_order", "bom_no", - "is_subcontracted", "semi_finished_good__finished_good_section", "finished_good", - "production_item", - "semi_fg_bom", - "total_completed_qty", "column_break_mcnb", + "semi_fg_bom", + "section_break_folk", "for_quantity", - "transferred_qty", - "manufactured_qty", + "pending_qty", + "column_break_cyjw", "process_loss_qty", + "total_completed_qty", + "section_break_wpjf", + "transferred_qty", + "column_break_lgte", + "manufactured_qty", "production_section", "operation", "source_warehouse", @@ -72,8 +75,10 @@ "item_name", "requested_qty", "is_paused", + "is_subcontracted", "track_semi_finished_goods", "column_break_20", + "project", "remarks", "section_break_dfoc", "status", @@ -626,12 +631,35 @@ "fieldname": "secondary_items_section", "fieldtype": "Tab Break", "label": "Secondary Items" + }, + { + "fieldname": "section_break_folk", + "fieldtype": "Section Break", + "hide_border": 1 + }, + { + "fieldname": "column_break_cyjw", + "fieldtype": "Column Break" + }, + { + "allow_on_submit": 1, + "fieldname": "pending_qty", + "fieldtype": "Float", + "label": "Pending Qty" + }, + { + "fieldname": "section_break_wpjf", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_lgte", + "fieldtype": "Column Break" } ], "grid_page_length": 50, "is_submittable": 1, "links": [], - "modified": "2026-05-12 12:17:17.750857", + "modified": "2026-05-20 14:05:46.205365", "modified_by": "Administrator", "module": "Manufacturing", "name": "Job Card", diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 9716ae49dc0..0c625c78a17 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -103,6 +103,7 @@ class JobCard(Document): operation_id: DF.Data | None operation_row_id: DF.Int operation_row_number: DF.Literal[None] + pending_qty: DF.Float posting_date: DF.Date | None process_loss_qty: DF.Float production_item: DF.Link | None @@ -881,7 +882,9 @@ class JobCard(Document): precision = self.precision("total_completed_qty") total_completed_qty = flt( - flt(self.total_completed_qty, precision) + flt(self.process_loss_qty, precision) + flt(self.total_completed_qty, precision) + + flt(self.process_loss_qty, precision) + + flt(self.pending_qty, precision) ) if self.for_quantity and flt(total_completed_qty, precision) != flt(self.for_quantity, precision): @@ -928,8 +931,10 @@ class JobCard(Document): 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 + self.process_loss_qty = ( + flt(self.for_quantity, precision) + - flt(self.total_completed_qty, precision) + - flt(self.pending_qty, precision) ) def update_work_order(self): @@ -943,13 +948,14 @@ class JobCard(Document): ): return - for_quantity, time_in_mins, process_loss_qty = 0, 0, 0 + for_quantity, time_in_mins, process_loss_qty, pending_qty = 0, 0, 0, 0 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) + pending_qty = flt(data[0].pending_qty) wo = frappe.get_doc("Work Order", self.work_order) @@ -957,8 +963,8 @@ class JobCard(Document): self.update_corrective_in_work_order(wo) elif self.operation_id: - self.validate_produced_quantity(for_quantity, process_loss_qty, wo) - self.update_work_order_data(for_quantity, process_loss_qty, time_in_mins, wo) + self.validate_produced_quantity(for_quantity, process_loss_qty, pending_qty, wo) + self.update_work_order_data(for_quantity, process_loss_qty, pending_qty, time_in_mins, wo) def update_semi_finished_good_details(self): if self.operation_id: @@ -987,11 +993,11 @@ class JobCard(Document): wo.flags.ignore_validate_update_after_submit = True wo.save() - def validate_produced_quantity(self, for_quantity, process_loss_qty, wo): + def validate_produced_quantity(self, for_quantity, process_loss_qty, pending_qty, wo): if self.docstatus < 2: return - if wo.produced_qty > for_quantity + process_loss_qty: + if wo.produced_qty > for_quantity + process_loss_qty + pending_qty: first_part_msg = _( "The {0} {1} is used to calculate the valuation cost for the finished good {2}." ).format(frappe.bold(_("Job Card")), frappe.bold(self.name), frappe.bold(self.production_item)) @@ -1004,7 +1010,7 @@ class JobCard(Document): _("{0} {1}").format(first_part_msg, second_part_msg), JobCardCancelError, title=_("Error") ) - def update_work_order_data(self, for_quantity, process_loss_qty, time_in_mins, wo): + def update_work_order_data(self, for_quantity, process_loss_qty, pending_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") @@ -1026,6 +1032,7 @@ class JobCard(Document): if data.get("name") == self.operation_id: data.completed_qty = for_quantity data.process_loss_qty = process_loss_qty + data.pending_qty = pending_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 @@ -1051,6 +1058,7 @@ class JobCard(Document): {"SUM": "total_time_in_mins", "as": "time_in_mins"}, {"SUM": "total_completed_qty", "as": "completed_qty"}, {"SUM": "process_loss_qty", "as": "process_loss_qty"}, + {"SUM": "pending_qty", "as": "pending_qty"}, ], filters={ "docstatus": 1, @@ -1445,10 +1453,10 @@ class JobCard(Document): if isinstance(kwargs, dict): kwargs = frappe._dict(kwargs) - if kwargs.end_time: - if kwargs.for_quantity: - self.for_quantity = kwargs.for_quantity + self.pending_qty = flt(kwargs.pending_qty) + self.process_loss_qty = flt(kwargs.process_loss_qty) + if kwargs.end_time: self.add_time_logs( to_time=kwargs.end_time, completed_qty=kwargs.qty, diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 4965f6eebec..7174347d5bb 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -720,6 +720,7 @@ class TestJobCard(ERPNextTestSuite): ) jc.time_logs[0].completed_qty = 8 + jc.pending_qty = 0.0 jc.save() jc.submit() diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 8bc4485785c..81047a8c391 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -167,18 +167,19 @@ class WorkOrder(Document): self.set_onload("backflush_raw_materials_based_on", based_on) def show_create_job_card_button(self): - operation_details = frappe._dict( - frappe.get_all( - "Job Card", - fields=["operation", {"SUM": "for_quantity"}], - filters={"docstatus": ("<", 2), "work_order": self.name}, - as_list=1, - group_by="operation_id", - ) + jc_doctype = frappe.qb.DocType("Job Card") + query = ( + frappe.qb.from_(jc_doctype) + .select(jc_doctype.operation_id, Sum(jc_doctype.for_quantity - IfNull(jc_doctype.pending_qty, 0))) + .where((jc_doctype.docstatus < 2) & (jc_doctype.work_order == self.name)) + .groupby(jc_doctype.operation_id) ) + operation_details = query.run(as_list=1) + operation_details = frappe._dict(operation_details) + for d in self.operations: - job_card_qty = self.qty - flt(operation_details.get(d.operation)) + job_card_qty = self.qty - flt(operation_details.get(d.name)) if job_card_qty > 0: return True diff --git a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json index 89ed830116b..918f2b21847 100644 --- a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json +++ b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_bulk_edit": 1, "creation": "2025-04-09 12:12:19.824560", "doctype": "DocType", "editable_grid": 1, @@ -10,6 +11,7 @@ "status", "completed_qty", "process_loss_qty", + "pending_qty", "column_break_4", "bom", "workstation_type", @@ -301,13 +303,20 @@ "fieldname": "quality_inspection_required", "fieldtype": "Check", "label": "Quality Inspection Required" + }, + { + "fieldname": "pending_qty", + "fieldtype": "Float", + "label": "Pending Qty", + "no_copy": 1, + "read_only": 1 } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2026-03-30 17:20:08.874381", + "modified": "2026-05-20 13:01:21.827200", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order Operation", diff --git a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.py b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.py index 2e45434f94b..8950fd6b320 100644 --- a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.py +++ b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.py @@ -32,6 +32,7 @@ class WorkOrderOperation(Document): parent: DF.Data parentfield: DF.Data parenttype: DF.Data + pending_qty: DF.Float planned_end_time: DF.Datetime | None planned_operating_cost: DF.Currency planned_start_time: DF.Datetime | None