feat: pending qty in job card

This commit is contained in:
Rohit Waghchaure
2026-05-20 14:12:44 +05:30
parent 83f100bae1
commit db64f451c1
7 changed files with 146 additions and 39 deletions

View File

@@ -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) { set_company_filters(frm, fieldname) {
frm.set_query(fieldname, () => { frm.set_query(fieldname, () => {
return { return {
@@ -148,6 +172,10 @@ frappe.ui.form.on("Job Card", {
return; 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; let has_stock_entry = frm.doc.__onload && frm.doc.__onload.has_stock_entry ? true : false;
frm.toggle_enable("for_quantity", !has_stock_entry); frm.toggle_enable("for_quantity", !has_stock_entry);
@@ -212,12 +240,14 @@ frappe.ui.form.on("Job Card", {
!has_items?.length) !has_items?.length)
) { ) {
let last_row = {}; 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); last_row = get_last_row(frm.doc.time_logs);
} }
if ( 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.doc.is_paused
) { ) {
frm.add_custom_button(__("Start Job"), () => { frm.add_custom_button(__("Start Job"), () => {
@@ -312,13 +342,18 @@ frappe.ui.form.on("Job Card", {
}, },
complete_job_card(frm) { 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 = [ let fields = [
{ {
fieldtype: "Float", fieldtype: "Float",
label: __("Qty to Manufacture"), label: __("Qty to Manufacture"),
fieldname: "for_quantity", fieldname: "for_quantity",
reqd: 1, reqd: 1,
default: frm.doc.for_quantity, default: pending_qty,
change() { change() {
let doc = frm.job_completion_dialog; let doc = frm.job_completion_dialog;
@@ -331,12 +366,29 @@ frappe.ui.form.on("Job Card", {
label: __("Completed Quantity"), label: __("Completed Quantity"),
fieldname: "completed_qty", fieldname: "completed_qty",
reqd: 1, reqd: 1,
default: frm.doc.for_quantity - frm.doc.total_completed_qty, default: pending_qty,
change() { change() {
let doc = frm.job_completion_dialog; let doc = frm.job_completion_dialog;
let process_loss_qty = doc.get_value("for_quantity") - doc.get_value("completed_qty"); let pending_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")) { 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); doc.set_value("process_loss_qty", process_loss_qty);
} }
}, },
@@ -348,8 +400,13 @@ frappe.ui.form.on("Job Card", {
onchange() { onchange() {
let doc = frm.job_completion_dialog; let doc = frm.job_completion_dialog;
let completed_qty = doc.get_value("for_quantity") - doc.get_value("process_loss_qty"); let pending_qty =
doc.set_value("completed_qty", completed_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);
}
}, },
}, },
{ {
@@ -405,6 +462,8 @@ frappe.ui.form.on("Job Card", {
args: { args: {
qty: data.completed_qty, qty: data.completed_qty,
for_quantity: data.for_quantity, for_quantity: data.for_quantity,
pending_qty: data.pending_qty,
process_loss_qty: data.process_loss_qty,
end_time: data.end_time, end_time: data.end_time,
sub_operation: data.sub_operation, sub_operation: data.sub_operation,
}, },

View File

@@ -9,23 +9,26 @@
"field_order": [ "field_order": [
"company", "company",
"naming_series", "naming_series",
"work_order", "production_item",
"employee", "employee",
"column_break_4", "column_break_4",
"posting_date", "posting_date",
"project", "work_order",
"bom_no", "bom_no",
"is_subcontracted",
"semi_finished_good__finished_good_section", "semi_finished_good__finished_good_section",
"finished_good", "finished_good",
"production_item",
"semi_fg_bom",
"total_completed_qty",
"column_break_mcnb", "column_break_mcnb",
"semi_fg_bom",
"section_break_folk",
"for_quantity", "for_quantity",
"transferred_qty", "pending_qty",
"manufactured_qty", "column_break_cyjw",
"process_loss_qty", "process_loss_qty",
"total_completed_qty",
"section_break_wpjf",
"transferred_qty",
"column_break_lgte",
"manufactured_qty",
"production_section", "production_section",
"operation", "operation",
"source_warehouse", "source_warehouse",
@@ -72,8 +75,10 @@
"item_name", "item_name",
"requested_qty", "requested_qty",
"is_paused", "is_paused",
"is_subcontracted",
"track_semi_finished_goods", "track_semi_finished_goods",
"column_break_20", "column_break_20",
"project",
"remarks", "remarks",
"section_break_dfoc", "section_break_dfoc",
"status", "status",
@@ -626,12 +631,35 @@
"fieldname": "secondary_items_section", "fieldname": "secondary_items_section",
"fieldtype": "Tab Break", "fieldtype": "Tab Break",
"label": "Secondary Items" "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, "grid_page_length": 50,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2026-05-12 12:17:17.750857", "modified": "2026-05-20 14:05:46.205365",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Job Card", "name": "Job Card",

View File

@@ -104,6 +104,7 @@ class JobCard(Document):
operation_id: DF.Data | None operation_id: DF.Data | None
operation_row_id: DF.Int operation_row_id: DF.Int
operation_row_number: DF.Literal[None] operation_row_number: DF.Literal[None]
pending_qty: DF.Float
posting_date: DF.Date | None posting_date: DF.Date | None
process_loss_qty: DF.Float process_loss_qty: DF.Float
production_item: DF.Link | None production_item: DF.Link | None
@@ -882,7 +883,9 @@ class JobCard(Document):
precision = self.precision("total_completed_qty") precision = self.precision("total_completed_qty")
total_completed_qty = flt( 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): if self.for_quantity and flt(total_completed_qty, precision) != flt(self.for_quantity, precision):
@@ -929,8 +932,10 @@ class JobCard(Document):
self.process_loss_qty = 0.0 self.process_loss_qty = 0.0
if self.total_completed_qty and self.for_quantity > self.total_completed_qty: if self.total_completed_qty and self.for_quantity > self.total_completed_qty:
self.process_loss_qty = flt(self.for_quantity, precision) - flt( self.process_loss_qty = (
self.total_completed_qty, precision flt(self.for_quantity, precision)
- flt(self.total_completed_qty, precision)
- flt(self.pending_qty, precision)
) )
def update_work_order(self): def update_work_order(self):
@@ -944,13 +949,14 @@ class JobCard(Document):
): ):
return 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() data = self.get_current_operation_data()
if data and len(data) > 0: if data and len(data) > 0:
for_quantity = flt(data[0].completed_qty) for_quantity = flt(data[0].completed_qty)
time_in_mins = flt(data[0].time_in_mins) time_in_mins = flt(data[0].time_in_mins)
process_loss_qty = flt(data[0].process_loss_qty) 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) wo = frappe.get_doc("Work Order", self.work_order)
@@ -958,8 +964,8 @@ class JobCard(Document):
self.update_corrective_in_work_order(wo) self.update_corrective_in_work_order(wo)
elif self.operation_id: elif self.operation_id:
self.validate_produced_quantity(for_quantity, process_loss_qty, wo) self.validate_produced_quantity(for_quantity, process_loss_qty, pending_qty, wo)
self.update_work_order_data(for_quantity, process_loss_qty, time_in_mins, wo) self.update_work_order_data(for_quantity, process_loss_qty, pending_qty, time_in_mins, wo)
def update_semi_finished_good_details(self): def update_semi_finished_good_details(self):
if self.operation_id: if self.operation_id:
@@ -988,11 +994,11 @@ class JobCard(Document):
wo.flags.ignore_validate_update_after_submit = True wo.flags.ignore_validate_update_after_submit = True
wo.save() 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: if self.docstatus < 2:
return return
if wo.produced_qty > for_quantity + process_loss_qty: if wo.produced_qty > for_quantity + process_loss_qty + pending_qty:
first_part_msg = _( first_part_msg = _(
"The {0} {1} is used to calculate the valuation cost for the finished good {2}." "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)) ).format(frappe.bold(_("Job Card")), frappe.bold(self.name), frappe.bold(self.production_item))
@@ -1005,7 +1011,7 @@ class JobCard(Document):
_("{0} {1}").format(first_part_msg, second_part_msg), JobCardCancelError, title=_("Error") _("{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") workstation_hour_rate = frappe.get_value("Workstation", self.workstation, "hour_rate")
jc = frappe.qb.DocType("Job Card") jc = frappe.qb.DocType("Job Card")
jctl = frappe.qb.DocType("Job Card Time Log") jctl = frappe.qb.DocType("Job Card Time Log")
@@ -1027,6 +1033,7 @@ class JobCard(Document):
if data.get("name") == self.operation_id: if data.get("name") == self.operation_id:
data.completed_qty = for_quantity data.completed_qty = for_quantity
data.process_loss_qty = process_loss_qty data.process_loss_qty = process_loss_qty
data.pending_qty = pending_qty
data.actual_operation_time = time_in_mins data.actual_operation_time = time_in_mins
data.actual_start_time = time_data[0].start_time if time_data else None 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 data.actual_end_time = time_data[0].end_time if time_data else None
@@ -1052,6 +1059,7 @@ class JobCard(Document):
{"SUM": "total_time_in_mins", "as": "time_in_mins"}, {"SUM": "total_time_in_mins", "as": "time_in_mins"},
{"SUM": "total_completed_qty", "as": "completed_qty"}, {"SUM": "total_completed_qty", "as": "completed_qty"},
{"SUM": "process_loss_qty", "as": "process_loss_qty"}, {"SUM": "process_loss_qty", "as": "process_loss_qty"},
{"SUM": "pending_qty", "as": "pending_qty"},
], ],
filters={ filters={
"docstatus": 1, "docstatus": 1,
@@ -1446,10 +1454,10 @@ class JobCard(Document):
if isinstance(kwargs, dict): if isinstance(kwargs, dict):
kwargs = frappe._dict(kwargs) kwargs = frappe._dict(kwargs)
if kwargs.end_time: self.pending_qty = flt(kwargs.pending_qty)
if kwargs.for_quantity: self.process_loss_qty = flt(kwargs.process_loss_qty)
self.for_quantity = kwargs.for_quantity
if kwargs.end_time:
self.add_time_logs( self.add_time_logs(
to_time=kwargs.end_time, to_time=kwargs.end_time,
completed_qty=kwargs.qty, completed_qty=kwargs.qty,

View File

@@ -721,6 +721,7 @@ class TestJobCard(ERPNextTestSuite):
) )
jc.time_logs[0].completed_qty = 8 jc.time_logs[0].completed_qty = 8
jc.pending_qty = 0.0
jc.save() jc.save()
jc.submit() jc.submit()

View File

@@ -167,18 +167,19 @@ class WorkOrder(Document):
self.set_onload("backflush_raw_materials_based_on", based_on) self.set_onload("backflush_raw_materials_based_on", based_on)
def show_create_job_card_button(self): def show_create_job_card_button(self):
operation_details = frappe._dict( jc_doctype = frappe.qb.DocType("Job Card")
frappe.get_all( query = (
"Job Card", frappe.qb.from_(jc_doctype)
fields=["operation", {"SUM": "for_quantity"}], .select(jc_doctype.operation_id, Sum(jc_doctype.for_quantity - IfNull(jc_doctype.pending_qty, 0)))
filters={"docstatus": ("<", 2), "work_order": self.name}, .where((jc_doctype.docstatus < 2) & (jc_doctype.work_order == self.name))
as_list=1, .groupby(jc_doctype.operation_id)
group_by="operation_id",
)
) )
operation_details = query.run(as_list=1)
operation_details = frappe._dict(operation_details)
for d in self.operations: 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: if job_card_qty > 0:
return True return True

View File

@@ -1,5 +1,6 @@
{ {
"actions": [], "actions": [],
"allow_bulk_edit": 1,
"creation": "2025-04-09 12:12:19.824560", "creation": "2025-04-09 12:12:19.824560",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
@@ -10,6 +11,7 @@
"status", "status",
"completed_qty", "completed_qty",
"process_loss_qty", "process_loss_qty",
"pending_qty",
"column_break_4", "column_break_4",
"bom", "bom",
"workstation_type", "workstation_type",
@@ -301,13 +303,20 @@
"fieldname": "quality_inspection_required", "fieldname": "quality_inspection_required",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Quality Inspection Required" "label": "Quality Inspection Required"
},
{
"fieldname": "pending_qty",
"fieldtype": "Float",
"label": "Pending Qty",
"no_copy": 1,
"read_only": 1
} }
], ],
"grid_page_length": 50, "grid_page_length": 50,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2026-03-30 17:20:08.874381", "modified": "2026-05-20 13:01:21.827200",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Work Order Operation", "name": "Work Order Operation",

View File

@@ -32,6 +32,7 @@ class WorkOrderOperation(Document):
parent: DF.Data parent: DF.Data
parentfield: DF.Data parentfield: DF.Data
parenttype: DF.Data parenttype: DF.Data
pending_qty: DF.Float
planned_end_time: DF.Datetime | None planned_end_time: DF.Datetime | None
planned_operating_cost: DF.Currency planned_operating_cost: DF.Currency
planned_start_time: DF.Datetime | None planned_start_time: DF.Datetime | None