diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js
index 19132ecf9fd..140733353f4 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.js
+++ b/erpnext/manufacturing/doctype/job_card/job_card.js
@@ -2,89 +2,83 @@
// For license information, please see license.txt
frappe.ui.form.on("Job Card", {
- setup: function (frm) {
- frm.set_query("operation", function () {
- return {
- query: "erpnext.manufacturing.doctype.job_card.job_card.get_operations",
- filters: {
- work_order: frm.doc.work_order,
- },
- };
- });
+ setup(frm) {
+ frm.set_query("operation", () => ({
+ query: "erpnext.manufacturing.doctype.job_card.job_card.get_operations",
+ filters: { work_order: frm.doc.work_order },
+ }));
- frm.set_query("serial_and_batch_bundle", () => {
- return {
- filters: {
- item_code: frm.doc.production_item,
- voucher_type: frm.doc.doctype,
- voucher_no: ["in", [frm.doc.name, ""]],
- is_cancelled: 0,
- },
- };
- });
+ frm.set_query("serial_and_batch_bundle", () => ({
+ filters: {
+ item_code: frm.doc.production_item,
+ voucher_type: frm.doc.doctype,
+ voucher_no: ["in", [frm.doc.name, ""]],
+ is_cancelled: 0,
+ },
+ }));
- frm.set_query("item_code", "secondary_items", () => {
- return {
- filters: {
- disabled: 0,
- },
- };
- });
+ frm.set_query("item_code", "secondary_items", () => ({
+ filters: { disabled: 0 },
+ }));
frm.set_query("operation", "time_logs", () => {
- let operations = (frm.doc.sub_operations || []).map((d) => d.sub_operation);
- return {
- filters: {
- name: ["in", operations],
- },
- };
+ const operations = (frm.doc.sub_operations || []).map((d) => d.sub_operation);
+ return { filters: { name: ["in", operations] } };
});
- frm.set_query("work_order", function () {
- return {
- filters: {
- status: ["not in", ["Cancelled", "Closed", "Stopped"]],
- },
- };
- });
+ frm.set_query("work_order", () => ({
+ filters: { status: ["not in", ["Cancelled", "Closed", "Stopped"]] },
+ }));
frm.events.set_company_filters(frm, "target_warehouse");
frm.events.set_company_filters(frm, "source_warehouse");
frm.events.set_company_filters(frm, "wip_warehouse");
- frm.set_query("source_warehouse", "items", () => {
- return {
- filters: {
- company: frm.doc.company,
- },
- };
+
+ frm.set_query("source_warehouse", "items", () => ({
+ filters: { company: frm.doc.company },
+ }));
+
+ frm.set_indicator_formatter("sub_operation", (doc) => {
+ if (doc.status === "Pending") return "red";
+ return doc.status === "Complete" ? "green" : "orange";
});
- frm.set_indicator_formatter("sub_operation", function (doc) {
- if (doc.status == "Pending") {
- return "red";
- } else {
- return doc.status === "Complete" ? "green" : "orange";
- }
- });
+ frm.set_query("employee", () => ({
+ filters: {
+ company: frm.doc.company,
+ status: "Active",
+ },
+ }));
+ },
- frm.set_query("employee", () => {
- return {
- filters: {
- company: frm.doc.company,
- status: "Active",
- },
- };
- });
+ 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"));
+ }
+
+ const 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]));
+ }
+
+ const 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 {
- filters: {
- company: frm.doc.company,
- },
- };
- });
+ frm.set_query(fieldname, () => ({
+ filters: { company: frm.doc.company },
+ }));
},
make_fields_read_only(frm) {
@@ -99,33 +93,29 @@ frappe.ui.form.on("Job Card", {
},
setup_stock_entry(frm) {
- if (
- frm.doc.track_semi_finished_goods &&
- frm.doc.docstatus === 1 &&
- !frm.doc.is_subcontracted &&
- (frm.doc.skip_material_transfer || frm.doc.transferred_qty > 0) &&
- flt(frm.doc.manufactured_qty) + flt(frm.doc.process_loss_qty) < flt(frm.doc.for_quantity)
- ) {
- frm.add_custom_button(__("Make Stock Entry"), () => {
- frappe.confirm(
- __("Do you want to submit the stock entry?"),
- () => {
- frm.events.make_manufacture_stock_entry(frm, 1);
- },
- () => {
- frm.events.make_manufacture_stock_entry(frm, 0);
- }
- );
- }).addClass("btn-primary");
- }
+ const { doc } = frm;
+ const can_make_stock_entry =
+ doc.track_semi_finished_goods &&
+ doc.docstatus === 1 &&
+ !doc.is_subcontracted &&
+ (doc.skip_material_transfer || doc.transferred_qty > 0) &&
+ flt(doc.manufactured_qty) + flt(doc.process_loss_qty) < flt(doc.for_quantity);
+
+ if (!can_make_stock_entry) return;
+
+ frm.add_custom_button(__("Make Stock Entry"), () => {
+ frappe.confirm(
+ __("Do you want to submit the stock entry?"),
+ () => frm.events.make_manufacture_stock_entry(frm, 1),
+ () => frm.events.make_manufacture_stock_entry(frm, 0)
+ );
+ }).addClass("btn-primary");
},
make_manufacture_stock_entry(frm, submit_entry) {
frm.call({
method: "make_stock_entry_for_semi_fg_item",
- args: {
- auto_submit: submit_entry,
- },
+ args: { auto_submit: submit_entry },
doc: frm.doc,
freeze: true,
callback() {
@@ -134,190 +124,134 @@ frappe.ui.form.on("Job Card", {
});
},
- refresh: function (frm) {
- frm.trigger("setup_stock_entry");
+ refresh(frm) {
+ const { doc } = frm;
+ const has_items = doc.items && doc.items.length;
+
+ // Clear any running timer tick from a previous render.
+ if (frm._jcd_timer_interval) {
+ clearInterval(frm._jcd_timer_interval);
+ frm._jcd_timer_interval = null;
+ }
- let has_items = frm.doc.items && frm.doc.items.length;
frm.trigger("make_fields_read_only");
- if (!frm.is_new() && frm.doc.__onload?.work_order_closed) {
+ if (!frm.is_new() && doc.__onload?.work_order_closed) {
frm.disable_save();
return;
}
- if (frm.doc.is_subcontracted) {
+ if (doc.is_subcontracted) {
frm.trigger("make_subcontracting_po");
return;
}
- let has_stock_entry = frm.doc.__onload && frm.doc.__onload.has_stock_entry ? true : false;
+ if (doc.docstatus > 0) {
+ frm.set_df_property("pending_qty", "read_only", 1);
+ }
+ const has_stock_entry = !!doc.__onload?.has_stock_entry;
frm.toggle_enable("for_quantity", !has_stock_entry);
- if (frm.doc.docstatus != 0) {
+ if (doc.docstatus != 0) {
frm.fields_dict["time_logs"].grid.update_docfield_property("completed_qty", "read_only", 1);
frm.fields_dict["time_logs"].grid.update_docfield_property("time_in_mins", "read_only", 1);
}
- if (!frm.is_new() && !frm.doc.skip_material_transfer && frm.doc.docstatus < 2) {
- let to_request = frm.doc.for_quantity > frm.doc.transferred_qty;
- let excess_transfer_allowed = frm.doc.__onload.job_card_excess_transfer;
+ frm.events.setup_material_transfer_buttons(frm, has_items);
- if (has_items && (to_request || excess_transfer_allowed)) {
- frm.add_custom_button(
- __("Material Request"),
- () => {
- frm.trigger("make_material_request");
- },
- __("Create")
- );
- }
-
- // check if any row has untransferred materials
- // in case of multiple items in JC
- let to_transfer = frm.doc.items.some((row) => row.transferred_qty < row.required_qty);
-
- if (has_items && (to_transfer || excess_transfer_allowed)) {
- frm.add_custom_button(
- __("Material Transfer"),
- () => {
- frm.trigger("make_stock_entry");
- },
- __("Create")
- );
- }
- }
-
- if (frm.doc.docstatus == 1 && !frm.doc.is_corrective_job_card && !frm.doc.finished_good) {
+ if (doc.docstatus == 1 && !doc.is_corrective_job_card && !doc.finished_good) {
frm.trigger("setup_corrective_job_card");
}
- frm.set_query("quality_inspection", function () {
- return {
- query: "erpnext.stock.doctype.quality_inspection.quality_inspection.quality_inspection_query",
- filters: {
- item_code: frm.doc.production_item,
- reference_name: frm.doc.name,
- },
- };
- });
+ frm.set_query("quality_inspection", () => ({
+ query: "erpnext.stock.doctype.quality_inspection.quality_inspection.quality_inspection_query",
+ filters: {
+ item_code: doc.production_item,
+ reference_name: doc.name,
+ },
+ }));
frm.trigger("toggle_operation_number");
- if (
- frm.doc.for_quantity + frm.doc.process_loss_qty > frm.doc.total_completed_qty &&
- (frm.doc.skip_material_transfer ||
- frm.doc.transferred_qty >= frm.doc.for_quantity + frm.doc.process_loss_qty ||
- !frm.doc.finished_good ||
- !has_items?.length)
- ) {
- let last_row = {};
- if (frm.doc.sub_operations?.length && frm.doc.time_logs?.length) {
- last_row = get_last_row(frm.doc.time_logs);
- }
+ const is_timer_running = frm.events.setup_job_action_buttons(frm, has_items);
- if (
- (!frm.doc.time_logs?.length || (frm.doc.sub_operations?.length && last_row?.to_time)) &&
- !frm.doc.is_paused
- ) {
- frm.add_custom_button(__("Start Job"), () => {
- let from_time = frappe.datetime.now_datetime();
- if ((frm.doc.employee && !frm.doc.employee.length) || !frm.doc.employee) {
- frappe.prompt(
- {
- fieldtype: "Table MultiSelect",
- label: __("Select Employees"),
- options: "Job Card Time Log",
- fieldname: "employees",
- reqd: 1,
- filters: {
- status: "Active",
- },
- },
- (d) => {
- frm.events.start_timer(frm, from_time, d.employees);
- },
- __("Assign Job to Employee")
- );
- } else {
- frm.events.start_timer(frm, from_time, frm.doc.employee);
- }
- });
- } else if (frm.doc.is_paused) {
- frm.add_custom_button(__("Resume Job"), () => {
- frm.call({
- method: "resume_job",
- doc: frm.doc,
- args: {
- start_time: frappe.datetime.now_datetime(),
- },
- callback() {
- frm.reload_doc();
- },
- });
- });
- } else {
- let manufactured_qty = frm.doc.manufactured_qty || frm.doc.total_completed_qty;
- if (frm.doc.for_quantity - (manufactured_qty + frm.doc.process_loss_qty) > 0) {
- if (!frm.doc.is_paused) {
- frm.add_custom_button(__("Pause Job"), () => {
- frm.call({
- method: "pause_job",
- doc: frm.doc,
- args: {
- end_time: frappe.datetime.now_datetime(),
- },
- callback() {
- frm.reload_doc();
- },
- });
- });
- }
-
- frm.add_custom_button(__("Complete Job"), () => {
- frm.trigger("complete_job_card");
- });
- }
-
- frm.trigger("make_dashboard");
- }
+ if (!is_timer_running) {
+ frm.trigger("setup_stock_entry");
}
frm.trigger("setup_quality_inspection");
- if (frm.doc.work_order) {
- frappe.db.get_value("Work Order", frm.doc.work_order, "transfer_material_against").then((r) => {
- if (r.message.transfer_material_against == "Work Order" && !frm.doc.operation_row_id) {
+ if (doc.work_order) {
+ frappe.db.get_value("Work Order", doc.work_order, "transfer_material_against").then((r) => {
+ if (r.message.transfer_material_against == "Work Order" && !doc.operation_row_id) {
frm.set_df_property("items", "hidden", 1);
}
});
}
- let sbb_field = frm.get_docfield("serial_and_batch_bundle");
+ const sbb_field = frm.get_docfield("serial_and_batch_bundle");
if (sbb_field) {
- sbb_field.get_route_options_for_new_doc = () => {
- return {
- item_code: frm.doc.production_item,
- warehouse: frm.doc.wip_warehouse,
- voucher_type: frm.doc.doctype,
- };
- };
+ sbb_field.get_route_options_for_new_doc = () => ({
+ item_code: doc.production_item,
+ warehouse: doc.wip_warehouse,
+ voucher_type: doc.doctype,
+ });
}
},
+ // Adds Material Request and Material Transfer buttons when items need to be transferred.
+ setup_material_transfer_buttons(frm, has_items) {
+ const { doc } = frm;
+
+ if (frm.is_new() || doc.skip_material_transfer || doc.docstatus >= 2) return;
+
+ const excess_transfer_allowed = doc.__onload.job_card_excess_transfer;
+ const to_request = doc.for_quantity > doc.transferred_qty;
+
+ if (has_items && (to_request || excess_transfer_allowed)) {
+ frm.add_custom_button(
+ __("Material Request"),
+ () => frm.trigger("make_material_request"),
+ __("Create")
+ );
+ }
+
+ // check if any row has untransferred materials in case of multiple items in JC
+ const to_transfer = doc.items.some((row) => row.transferred_qty < row.required_qty);
+
+ if (has_items && (to_transfer || excess_transfer_allowed)) {
+ frm.add_custom_button(
+ __("Material Transfer"),
+ () => frm.trigger("make_stock_entry"),
+ __("Create")
+ );
+ }
+ },
+
+ // Renders the dashboard widget (info + timer + action buttons) into job_card_dashboard wrapper.
+ // Returns true if the job timer is actively running, so the caller can skip the stock entry button.
+ setup_job_action_buttons(frm, has_items) {
+ return frm.events.make_dashboard(frm, has_items);
+ },
+
complete_job_card(frm) {
- let fields = [
+ let pending_qty = frm.doc.for_quantity - frm.doc.total_completed_qty;
+ if (frm.doc.pending_qty > 0) {
+ pending_qty = frm.doc.pending_qty;
+ }
+
+ const 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;
-
- doc.set_value("completed_qty", doc.get_value("for_quantity"));
- doc.set_value("process_loss_qty", 0);
+ const dialog = frm.job_completion_dialog;
+ dialog.set_value("completed_qty", dialog.get_value("for_quantity"));
+ dialog.set_value("process_loss_qty", 0);
},
},
{
@@ -325,13 +259,28 @@ 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")) {
- doc.set_value("process_loss_qty", process_loss_qty);
+ const dialog = frm.job_completion_dialog;
+ const remaining = dialog.get_value("for_quantity") - dialog.get_value("completed_qty");
+ if (remaining > 0 && remaining != dialog.get_value("pending_qty")) {
+ dialog.set_value("pending_qty", remaining);
+ }
+ },
+ },
+ {
+ fieldtype: "Float",
+ label: __("Pending Quantity"),
+ fieldname: "pending_qty",
+ default: 0.0,
+ change() {
+ const dialog = frm.job_completion_dialog;
+ const process_loss_qty =
+ dialog.get_value("for_quantity") -
+ dialog.get_value("completed_qty") -
+ dialog.get_value("pending_qty");
+ if (process_loss_qty >= 0 && process_loss_qty != dialog.get_value("process_loss_qty")) {
+ dialog.set_value("process_loss_qty", process_loss_qty);
}
},
},
@@ -340,10 +289,14 @@ frappe.ui.form.on("Job Card", {
label: __("Process Loss Quantity"),
fieldname: "process_loss_qty",
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);
+ const dialog = frm.job_completion_dialog;
+ const remaining =
+ dialog.get_value("for_quantity") -
+ dialog.get_value("completed_qty") -
+ dialog.get_value("process_loss_qty");
+ if (remaining >= 0 && remaining != dialog.get_value("pending_qty")) {
+ dialog.set_value("pending_qty", remaining);
+ }
},
},
{
@@ -358,20 +311,16 @@ frappe.ui.form.on("Job Card", {
fieldname: "sub_operation",
options: "Operation",
get_query() {
- let non_completed_operations = frm.doc.sub_operations.filter(
- (d) => d.status === "Pending"
- );
+ const non_completed = frm.doc.sub_operations.filter((d) => d.status === "Pending");
return {
- filters: {
- name: ["in", non_completed_operations.map((d) => d.sub_operation)],
- },
+ filters: { name: ["in", non_completed.map((d) => d.sub_operation)] },
};
},
reqd: 1,
});
}
- let last_completed_row = get_last_completed_row(frm.doc.time_logs);
+ const last_completed_row = get_last_completed_row(frm.doc.time_logs);
let last_row = {};
if (frm.doc.sub_operations?.length && frm.doc.time_logs?.length) {
last_row = get_last_row(frm.doc.time_logs);
@@ -399,10 +348,12 @@ 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,
},
- callback: function (r) {
+ callback() {
frm.reload_doc();
},
});
@@ -428,19 +379,15 @@ frappe.ui.form.on("Job Card", {
frm.call({
method: "start_timer",
doc: frm.doc,
- args: {
- start_time: start_time,
- employees: employees,
- },
- callback: function (r) {
+ args: { start_time, employees },
+ callback() {
frm.reload_doc();
- frm.trigger("make_dashboard");
},
});
},
make_finished_good(frm) {
- let fields = [
+ const fields = [
{
fieldtype: "Float",
label: __("Completed Quantity"),
@@ -466,12 +413,9 @@ frappe.ui.form.on("Job Card", {
frm.call({
method: "make_finished_good",
doc: frm.doc,
- args: {
- qty: data.qty,
- end_time: data.end_time,
- },
- callback: function (r) {
- var doc = frappe.model.sync(r.message);
+ args: { qty: data.qty, end_time: data.end_time },
+ callback(r) {
+ const doc = frappe.model.sync(r.message);
frappe.set_route("Form", doc[0].doctype, doc[0].name);
},
});
@@ -482,8 +426,8 @@ frappe.ui.form.on("Job Card", {
);
},
- setup_quality_inspection: function (frm) {
- let quality_inspection_field = frm.get_docfield("quality_inspection");
+ setup_quality_inspection(frm) {
+ const quality_inspection_field = frm.get_docfield("quality_inspection");
quality_inspection_field.get_route_options_for_new_doc = function (frm) {
return {
inspection_type: "In Process",
@@ -498,24 +442,22 @@ frappe.ui.form.on("Job Card", {
};
},
- setup_corrective_job_card: function (frm) {
+ setup_corrective_job_card(frm) {
frm.add_custom_button(
__("Corrective Job Card"),
() => {
- let operations = frm.doc.sub_operations.map((d) => d.sub_operation).concat(frm.doc.operation);
+ const operations = frm.doc.sub_operations
+ .map((d) => d.sub_operation)
+ .concat(frm.doc.operation);
- let fields = [
+ const fields = [
{
fieldtype: "Link",
label: __("Corrective Operation"),
options: "Operation",
fieldname: "operation",
get_query() {
- return {
- filters: {
- is_corrective_operation: 1,
- },
- };
+ return { filters: { is_corrective_operation: 1 } };
},
},
{
@@ -524,20 +466,14 @@ frappe.ui.form.on("Job Card", {
options: "Operation",
fieldname: "for_operation",
get_query() {
- return {
- filters: {
- name: ["in", operations],
- },
- };
+ return { filters: { name: ["in", operations] } };
},
},
];
frappe.prompt(
fields,
- (d) => {
- frm.events.make_corrective_job_card(frm, d.operation, d.for_operation);
- },
+ (d) => frm.events.make_corrective_job_card(frm, d.operation, d.for_operation),
__("Select Corrective Operation")
);
},
@@ -545,7 +481,7 @@ frappe.ui.form.on("Job Card", {
);
},
- make_corrective_job_card: function (frm, operation, for_operation) {
+ make_corrective_job_card(frm, operation, for_operation) {
frappe.call({
method: "erpnext.manufacturing.doctype.job_card.job_card.make_corrective_job_card",
args: {
@@ -553,7 +489,7 @@ frappe.ui.form.on("Job Card", {
operation: operation,
for_operation: for_operation,
},
- callback: function (r) {
+ callback(r) {
if (r.message) {
frappe.model.sync(r.message);
frappe.set_route("Form", r.message.doctype, r.message.name);
@@ -562,7 +498,7 @@ frappe.ui.form.on("Job Card", {
});
},
- operation: function (frm) {
+ operation(frm) {
frm.trigger("toggle_operation_number");
if (frm.doc.operation && frm.doc.work_order) {
@@ -572,28 +508,22 @@ frappe.ui.form.on("Job Card", {
work_order: frm.doc.work_order,
operation: frm.doc.operation,
},
- callback: function (r) {
- if (r.message) {
- if (r.message.length == 1) {
- frm.set_value("operation_id", r.message[0].name);
- } else {
- let args = [];
+ callback(r) {
+ if (!r.message) return;
- r.message.forEach((row) => {
- args.push({ label: row.idx, value: row.name });
- });
-
- let description = __("Operation {0} added multiple times in the work order {1}", [
- frm.doc.operation,
- frm.doc.work_order,
- ]);
-
- frm.set_df_property("operation_row_number", "options", args);
- frm.set_df_property("operation_row_number", "description", description);
- }
-
- frm.trigger("toggle_operation_number");
+ if (r.message.length == 1) {
+ frm.set_value("operation_id", r.message[0].name);
+ } else {
+ const args = r.message.map((row) => ({ label: row.idx, value: row.name }));
+ const description = __("Operation {0} added multiple times in the work order {1}", [
+ frm.doc.operation,
+ frm.doc.work_order,
+ ]);
+ frm.set_df_property("operation_row_number", "options", args);
+ frm.set_df_property("operation_row_number", "description", description);
}
+
+ frm.trigger("toggle_operation_number");
},
});
}
@@ -610,89 +540,233 @@ frappe.ui.form.on("Job Card", {
frm.toggle_reqd("operation_row_number", !frm.doc.operation_id && frm.doc.operation);
},
- make_time_log: function (frm, args) {
+ make_time_log(frm, args) {
frm.events.update_sub_operation(frm, args);
frappe.call({
method: "erpnext.manufacturing.doctype.job_card.job_card.make_time_log",
- args: {
- args: args,
- },
+ args: { args },
freeze: true,
- callback: function () {
+ callback() {
frm.reload_doc();
frm.trigger("make_dashboard");
},
});
},
- update_sub_operation: function (frm, args) {
- if (frm.doc.sub_operations && frm.doc.sub_operations.length) {
- let sub_operations = frm.doc.sub_operations.filter((d) => d.status != "Complete");
- if (sub_operations && sub_operations.length) {
- args["sub_operation"] = sub_operations[0].sub_operation;
+ update_sub_operation(frm, args) {
+ if (frm.doc.sub_operations?.length) {
+ const pending_sub_ops = frm.doc.sub_operations.filter((d) => d.status != "Complete");
+ if (pending_sub_ops.length) {
+ args["sub_operation"] = pending_sub_ops[0].sub_operation;
}
}
},
- make_dashboard: function (frm) {
- if (frm.doc.__islocal) return;
- var section = "";
+ make_dashboard(frm, has_items) {
+ if (frm.doc.__islocal) return false;
- function setCurrentIncrement() {
- currentIncrement += 1;
- return currentIncrement;
+ frm.dashboard.refresh();
+
+ // Clear any previously running timer tick before re-rendering.
+ if (frm._jcd_timer_interval) {
+ clearInterval(frm._jcd_timer_interval);
+ frm._jcd_timer_interval = null;
}
- function updateStopwatch(increment) {
- var hours = Math.floor(increment / 3600);
- var minutes = Math.floor((increment - hours * 3600) / 60);
- var seconds = Math.floor(flt(increment - hours * 3600 - minutes * 60, 2));
+ const wrapper = $(frm.fields_dict["job_card_dashboard"].wrapper);
+ wrapper.empty();
- $(section)
- .find(".hours")
- .text(hours < 10 ? "0" + hours.toString() : hours.toString());
- $(section)
- .find(".minutes")
- .text(minutes < 10 ? "0" + minutes.toString() : minutes.toString());
- $(section)
- .find(".seconds")
- .text(seconds < 10 ? "0" + seconds.toString() : seconds.toString());
+ const { doc } = frm;
+ const { time_logs, status } = doc;
+
+ // ── Determine which action buttons to show ────────────────────────
+ const has_remaining_qty = doc.for_quantity + doc.process_loss_qty > doc.total_completed_qty;
+ const materials_ready =
+ doc.skip_material_transfer ||
+ doc.transferred_qty >= doc.for_quantity + doc.process_loss_qty ||
+ !doc.finished_good ||
+ !has_items?.length;
+
+ let last_row = {};
+ const has_sub_ops_or_pending_qty = doc.sub_operations?.length || doc.pending_qty > 0;
+ if (has_sub_ops_or_pending_qty && time_logs?.length) {
+ last_row = get_last_row(time_logs);
}
- function initialiseTimer() {
- const interval = setInterval(function () {
- var current = setCurrentIncrement();
- updateStopwatch(current);
+ const no_time_logs_yet = !time_logs?.length;
+ const pending_qty_cycle_done = flt(doc.pending_qty) > 0.0 && last_row?.to_time;
+ const sub_operation_cycle_done = doc.sub_operations?.length && last_row?.to_time;
+ const should_show_start =
+ (no_time_logs_yet || pending_qty_cycle_done || sub_operation_cycle_done) && !doc.is_paused;
+
+ const last_log_complete = time_logs?.length && time_logs[time_logs.length - 1].to_time;
+ const is_on_hold = status === "On Hold";
+ const is_actively_running = !!(
+ time_logs?.length &&
+ !last_log_complete &&
+ !is_on_hold &&
+ !doc.is_paused
+ );
+
+ let show_start = false,
+ show_pause = false,
+ show_resume = false,
+ show_complete = false,
+ is_timer_running = false;
+
+ if (has_remaining_qty && materials_ready) {
+ const manufactured_qty = doc.manufactured_qty || doc.total_completed_qty;
+ const qty_yet_to_manufacture = doc.for_quantity - (manufactured_qty + doc.process_loss_qty);
+
+ if (should_show_start) {
+ show_start = true;
+ } else if (doc.is_paused) {
+ show_resume = true;
+ } else if (qty_yet_to_manufacture > 0) {
+ show_pause = true;
+ show_complete = true;
+ is_timer_running = true;
+ }
+ }
+
+ // ── Timer color reflects job state ────────────────────────────────
+ const [timer_color, timer_bg, timer_border] = [
+ "var(--gray-600,#6b7280)",
+ "var(--gray-100,#f3f4f6)",
+ "var(--gray-300,#d1d5db)",
+ ];
+
+ // ── Action button HTML ────────────────────────────────────────────
+ const btn = (cls, icon_path, label, icon_color) => `
+ `;
+
+ const icons = {
+ play: { d: '