mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-26 08:24:47 +00:00
feat: track Semi-finished goods (including subcontracted items) against Job Cards (#38341)
* feat: Track Semi-finished goods (including subcontracted items) against Job Cards * feat: option to add raw materials manually against operation
This commit is contained in:
@@ -33,10 +33,11 @@ class BOMConfigurator {
|
||||
frm: this.frm,
|
||||
add_item: this.add_item,
|
||||
add_sub_assembly: this.add_sub_assembly,
|
||||
set_query_for_workstation: this.set_query_for_workstation,
|
||||
get_sub_assembly_modal_fields: this.get_sub_assembly_modal_fields,
|
||||
convert_to_sub_assembly: this.convert_to_sub_assembly,
|
||||
delete_node: this.delete_node,
|
||||
edit_qty: this.edit_qty,
|
||||
edit_bom: this.edit_bom,
|
||||
load_tree: this.load_tree,
|
||||
set_default_qty: this.set_default_qty,
|
||||
};
|
||||
@@ -107,15 +108,15 @@ class BOMConfigurator {
|
||||
this.frm?.doc.docstatus === 0
|
||||
? [
|
||||
{
|
||||
label: `${frappe.utils.icon("edit", "sm")} ${__("Qty")}`,
|
||||
label: __(frappe.utils.icon("edit", "sm") + " BOM"),
|
||||
click: function (node) {
|
||||
let view = frappe.views.trees["BOM Configurator"];
|
||||
view.events.edit_qty(node, view);
|
||||
view.events.edit_bom(node, view);
|
||||
},
|
||||
btnClass: "hidden-xs",
|
||||
},
|
||||
{
|
||||
label: `${frappe.utils.icon("add", "sm")} ${__("Raw Material")}`,
|
||||
label: __(frappe.utils.icon("add", "sm") + " Raw Material"),
|
||||
click: function (node) {
|
||||
let view = frappe.views.trees["BOM Configurator"];
|
||||
view.events.add_item(node, view);
|
||||
@@ -126,7 +127,7 @@ class BOMConfigurator {
|
||||
btnClass: "hidden-xs",
|
||||
},
|
||||
{
|
||||
label: `${frappe.utils.icon("add", "sm")} ${__("Sub Assembly")}`,
|
||||
label: __(frappe.utils.icon("add", "sm") + " Sub Assembly"),
|
||||
click: function (node) {
|
||||
let view = frappe.views.trees["BOM Configurator"];
|
||||
view.events.add_sub_assembly(node, view);
|
||||
@@ -156,7 +157,7 @@ class BOMConfigurator {
|
||||
btnClass: "hidden-xs expand-all-btn",
|
||||
},
|
||||
{
|
||||
label: `${frappe.utils.icon("move", "sm")} ${__("Sub Assembly")}`,
|
||||
label: __(frappe.utils.icon("move", "sm") + " Sub Assembly"),
|
||||
click: function (node) {
|
||||
let view = frappe.views.trees["BOM Configurator"];
|
||||
view.events.convert_to_sub_assembly(node, view);
|
||||
@@ -167,7 +168,7 @@ class BOMConfigurator {
|
||||
btnClass: "hidden-xs",
|
||||
},
|
||||
{
|
||||
label: `${frappe.utils.icon("delete", "sm")} ${__("Item")}`,
|
||||
label: __(frappe.utils.icon("delete", "sm") + " Item"),
|
||||
click: function (node) {
|
||||
let view = frappe.views.trees["BOM Configurator"];
|
||||
view.events.delete_node(node, view);
|
||||
@@ -232,18 +233,38 @@ class BOMConfigurator {
|
||||
);
|
||||
}
|
||||
|
||||
set_query_for_workstation(dialog) {
|
||||
let workstation = dialog.fields.filter((field) => field.fieldname === "workstation");
|
||||
if (workstation.length) {
|
||||
workstation[0].get_query = function () {
|
||||
let workstation_type = dialog.get_value("workstation_type");
|
||||
|
||||
if (workstation_type) {
|
||||
return {
|
||||
filters: {
|
||||
workstation_type: dialog.get_value("workstation_type"),
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
add_sub_assembly(node, view) {
|
||||
let dialog = new frappe.ui.Dialog({
|
||||
fields: view.events.get_sub_assembly_modal_fields(),
|
||||
fields: view.events.get_sub_assembly_modal_fields(view, node.is_root),
|
||||
title: __("Add Sub Assembly"),
|
||||
});
|
||||
view.events.set_query_for_workstation(dialog);
|
||||
|
||||
dialog.show();
|
||||
view.events.set_default_qty(dialog);
|
||||
|
||||
dialog.set_primary_action(__("Add"), () => {
|
||||
let bom_item = dialog.get_values();
|
||||
|
||||
if (dialog.operation && !dialog.workstation_type && !dialog.workstation) {
|
||||
frappe.throw(__("Either Workstation or Workstation Type is mandatory"));
|
||||
}
|
||||
|
||||
if (!node.data?.parent_id) {
|
||||
node.data.parent_id = this.frm.doc.name;
|
||||
}
|
||||
@@ -255,6 +276,9 @@ class BOMConfigurator {
|
||||
fg_item: node.data.value,
|
||||
fg_reference_id: node.data.name || this.frm.doc.name,
|
||||
bom_item: bom_item,
|
||||
operation: node.data.operation,
|
||||
workstation_type: node.data.workstation_type,
|
||||
operation_time: node.data.operation_time,
|
||||
},
|
||||
callback: (r) => {
|
||||
view.events.load_tree(r, node);
|
||||
@@ -265,8 +289,8 @@ class BOMConfigurator {
|
||||
});
|
||||
}
|
||||
|
||||
get_sub_assembly_modal_fields(read_only = false) {
|
||||
return [
|
||||
get_sub_assembly_modal_fields(view, is_root = false, read_only = false, show_operations_fields = false) {
|
||||
let fields = [
|
||||
{
|
||||
label: __("Sub Assembly Item"),
|
||||
fieldname: "item_code",
|
||||
@@ -284,37 +308,150 @@ class BOMConfigurator {
|
||||
reqd: 1,
|
||||
read_only: read_only,
|
||||
},
|
||||
{ fieldtype: "Section Break" },
|
||||
{
|
||||
label: __("Raw Materials"),
|
||||
fieldname: "items",
|
||||
fieldtype: "Table",
|
||||
reqd: 1,
|
||||
fields: [
|
||||
{
|
||||
label: __("Item"),
|
||||
fieldname: "item_code",
|
||||
fieldtype: "Link",
|
||||
options: "Item",
|
||||
reqd: 1,
|
||||
in_list_view: 1,
|
||||
},
|
||||
{
|
||||
label: __("Qty"),
|
||||
fieldname: "qty",
|
||||
default: 1.0,
|
||||
fieldtype: "Float",
|
||||
reqd: 1,
|
||||
in_list_view: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
if (this.frm.doc.track_operations && (is_root || show_operations_fields)) {
|
||||
fields.push(
|
||||
...[
|
||||
{ fieldtype: "Section Break" },
|
||||
{
|
||||
label: __("Operation"),
|
||||
fieldname: "operation",
|
||||
fieldtype: "Link",
|
||||
options: "Operation",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
label: __("Operation Time"),
|
||||
fieldname: "operation_time",
|
||||
fieldtype: "Int",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
label: __("Is Subcontracted"),
|
||||
fieldname: "is_subcontracted",
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{ fieldtype: "Column Break" },
|
||||
{
|
||||
label: __("Workstation Type"),
|
||||
fieldname: "workstation_type",
|
||||
fieldtype: "Link",
|
||||
options: "Workstation Type",
|
||||
},
|
||||
{
|
||||
label: __("Workstation"),
|
||||
fieldname: "workstation",
|
||||
fieldtype: "Link",
|
||||
options: "Workstation",
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
if (this.frm.doc.track_semi_finished_goods) {
|
||||
fields.push(
|
||||
...[
|
||||
{ label: __("Default Warehouse"), fieldtype: "Section Break", collapsible: 1 },
|
||||
{
|
||||
label: __("Skip Material Transfer"),
|
||||
fieldname: "skip_material_transfer",
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
label: __("Backflush Materials From WIP"),
|
||||
fieldname: "backflush_from_wip_warehouse",
|
||||
fieldtype: "Check",
|
||||
depends_on: "eval:doc.skip_material_transfer",
|
||||
},
|
||||
{
|
||||
label: __("Source Warehouse"),
|
||||
fieldname: "source_warehouse",
|
||||
fieldtype: "Link",
|
||||
options: "Warehouse",
|
||||
depends_on: "eval:!doc.backflush_from_wip_warehouse",
|
||||
get_query() {
|
||||
return {
|
||||
filters: {
|
||||
company: view.events.frm.doc.company,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{ fieldtype: "Column Break" },
|
||||
{
|
||||
label: __("Work In Progress Warehouse"),
|
||||
fieldname: "wip_warehouse",
|
||||
fieldtype: "Link",
|
||||
options: "Warehouse",
|
||||
depends_on:
|
||||
"eval:!doc.skip_material_transfer || doc.backflush_from_wip_warehouse",
|
||||
get_query() {
|
||||
return {
|
||||
filters: {
|
||||
company: view.events.frm.doc.company,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
label: __("Finished Good Warehouse"),
|
||||
fieldname: "fg_warehouse",
|
||||
fieldtype: "Link",
|
||||
options: "Warehouse",
|
||||
get_query() {
|
||||
return {
|
||||
filters: {
|
||||
company: view.events.frm.doc.company,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fields.push(
|
||||
...[
|
||||
{ fieldtype: "Section Break" },
|
||||
{
|
||||
label: __("Raw Materials"),
|
||||
fieldname: "items",
|
||||
fieldtype: "Table",
|
||||
reqd: 1,
|
||||
fields: [
|
||||
{
|
||||
label: __("Item"),
|
||||
fieldname: "item_code",
|
||||
fieldtype: "Link",
|
||||
options: "Item",
|
||||
reqd: 1,
|
||||
in_list_view: 1,
|
||||
change() {
|
||||
let doc = this.doc;
|
||||
doc.qty = 1.0;
|
||||
this.grid.set_value("qty", 1.0, doc);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: __("Qty"),
|
||||
fieldname: "qty",
|
||||
default: 1.0,
|
||||
fieldtype: "Float",
|
||||
reqd: 1,
|
||||
in_list_view: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
convert_to_sub_assembly(node, view) {
|
||||
let dialog = new frappe.ui.Dialog({
|
||||
fields: view.events.get_sub_assembly_modal_fields(true),
|
||||
fields: view.events.get_sub_assembly_modal_fields(view, node.is_root, true, true),
|
||||
title: __("Add Sub Assembly"),
|
||||
});
|
||||
|
||||
@@ -324,11 +461,13 @@ class BOMConfigurator {
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
view.events.set_default_qty(dialog);
|
||||
|
||||
dialog.set_primary_action(__("Add"), () => {
|
||||
let bom_item = dialog.get_values();
|
||||
|
||||
if (dialog.operation && !dialog.workstation_type && !dialog.workstation) {
|
||||
frappe.throw(__("Either Workstation or Workstation Type is mandatory"));
|
||||
}
|
||||
|
||||
frappe.call({
|
||||
method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.add_sub_assembly",
|
||||
args: {
|
||||
@@ -337,10 +476,14 @@ class BOMConfigurator {
|
||||
bom_item: bom_item,
|
||||
fg_reference_id: node.data.name || this.frm.doc.name,
|
||||
convert_to_sub_assembly: true,
|
||||
operation: node.data.operation,
|
||||
workstation_type: node.data.workstation_type,
|
||||
operation_time: node.data.operation_time,
|
||||
workstation: node.data.workstation,
|
||||
},
|
||||
callback: (r) => {
|
||||
node.expandable = true;
|
||||
view.events.load_tree(r, node);
|
||||
view.events.load_tree(r, node.parent_node);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -376,24 +519,146 @@ class BOMConfigurator {
|
||||
});
|
||||
}
|
||||
|
||||
edit_qty(node, view) {
|
||||
edit_bom(node, view) {
|
||||
let me = this;
|
||||
let qty = node.data.qty || this.frm.doc.qty;
|
||||
frappe.prompt(
|
||||
[{ label: __("Qty"), fieldname: "qty", default: qty, fieldtype: "Float", reqd: 1 }],
|
||||
let fields = [{ label: __("Qty"), fieldname: "qty", default: qty, fieldtype: "Float", reqd: 1 }];
|
||||
|
||||
if (node.expandable && this.frm.doc.track_operations) {
|
||||
let data = node.data.operation ? node.data : this.frm.doc;
|
||||
|
||||
fields = [
|
||||
...fields,
|
||||
...[
|
||||
{ fieldtype: "Section Break" },
|
||||
{
|
||||
label: __("Operation"),
|
||||
fieldname: "operation",
|
||||
fieldtype: "Link",
|
||||
options: "Operation",
|
||||
default: data.operation,
|
||||
},
|
||||
{
|
||||
label: __("Operation Time"),
|
||||
fieldname: "operation_time",
|
||||
fieldtype: "Float",
|
||||
default: data.operation_time,
|
||||
},
|
||||
{
|
||||
label: __("Is Subcontracted"),
|
||||
fieldname: "is_subcontracted",
|
||||
fieldtype: "Check",
|
||||
default: data.is_subcontracted,
|
||||
},
|
||||
{ fieldtype: "Column Break" },
|
||||
{
|
||||
label: __("Workstation Type"),
|
||||
fieldname: "workstation_type",
|
||||
fieldtype: "Link",
|
||||
options: "Workstation Type",
|
||||
default: data.workstation_type,
|
||||
},
|
||||
{
|
||||
label: __("Workstation"),
|
||||
fieldname: "workstation",
|
||||
fieldtype: "Link",
|
||||
options: "Workstation",
|
||||
default: data.workstation,
|
||||
get_query() {
|
||||
let dialog = me.frm.edit_bom_dialog;
|
||||
let workstation_type = dialog.get_value("workstation_type");
|
||||
|
||||
if (workstation_type) {
|
||||
return {
|
||||
filters: {
|
||||
workstation_type: dialog.get_value("workstation_type"),
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
{ fieldtype: "Section Break" },
|
||||
{
|
||||
label: __("Skip Material Transfer"),
|
||||
fieldname: "skip_material_transfer",
|
||||
fieldtype: "Check",
|
||||
default: data.skip_material_transfer,
|
||||
},
|
||||
{
|
||||
label: __("Backflush Materials From WIP"),
|
||||
fieldname: "backflush_from_wip_warehouse",
|
||||
fieldtype: "Check",
|
||||
depends_on: "eval:doc.skip_material_transfer",
|
||||
default: data.backflush_from_wip_warehouse,
|
||||
},
|
||||
{
|
||||
label: __("Source Warehouse"),
|
||||
fieldname: "source_warehouse",
|
||||
fieldtype: "Link",
|
||||
options: "Warehouse",
|
||||
default: data.source_warehouse,
|
||||
depends_on: "eval:!doc.backflush_from_wip_warehouse",
|
||||
get_query() {
|
||||
return {
|
||||
filters: {
|
||||
company: me.frm.doc.company,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{ fieldtype: "Column Break" },
|
||||
{
|
||||
label: __("Work In Progress Warehouse"),
|
||||
fieldname: "wip_warehouse",
|
||||
fieldtype: "Link",
|
||||
options: "Warehouse",
|
||||
default: data.wip_warehouse,
|
||||
depends_on: "eval:!doc.skip_material_transfer || doc.backflush_from_wip_warehouse",
|
||||
get_query() {
|
||||
return {
|
||||
filters: {
|
||||
company: me.frm.doc.company,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
label: __("Finished Good Warehouse"),
|
||||
fieldname: "fg_warehouse",
|
||||
fieldtype: "Link",
|
||||
options: "Warehouse",
|
||||
default: data.fg_warehouse,
|
||||
get_query() {
|
||||
return {
|
||||
filters: {
|
||||
company: me.frm.doc.company,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
this.frm.edit_bom_dialog = frappe.prompt(
|
||||
fields,
|
||||
(data) => {
|
||||
let doctype = node.data.doctype || this.frm.doc.doctype;
|
||||
let docname = node.data.name || this.frm.doc.name;
|
||||
|
||||
frappe.call({
|
||||
method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.edit_qty",
|
||||
method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.edit_bom_creator",
|
||||
args: {
|
||||
doctype: doctype,
|
||||
docname: docname,
|
||||
qty: data.qty,
|
||||
parent: node.data.parent_id,
|
||||
data: data,
|
||||
parent: node.data.parent_id || this.frm.doc.name,
|
||||
},
|
||||
callback: (r) => {
|
||||
node.data.qty = data.qty;
|
||||
for (let key in data) {
|
||||
node.data[key] = data[key];
|
||||
}
|
||||
|
||||
let uom = node.data.uom || this.frm.doc.uom;
|
||||
$(node.parent.get(0))
|
||||
.find(`[data-bom-qty-docname='${docname}']`)
|
||||
@@ -402,7 +667,7 @@ class BOMConfigurator {
|
||||
},
|
||||
});
|
||||
},
|
||||
__("Edit Qty"),
|
||||
__("Edit BOM"),
|
||||
__("Update")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{% $.each(workstations, (idx, row) => { %}
|
||||
<div class="workstation-wrapper">
|
||||
<div class="workstation-status text-right">
|
||||
<span class="indicator-pill no-indicator-dot whitespace-nowrap {{row.color}}" style="margin: 3px 4px 0px 0px;"><span style="font-size:13px">{{row.status}}</span></span>
|
||||
</div>
|
||||
<div class="workstation-image">
|
||||
<div class="flex items-center justify-center h-32 border-b-grey text-6xl text-grey-100">
|
||||
<a class="workstation-image-link" href="{{row.workstation_link}}">
|
||||
@@ -11,9 +14,10 @@
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="workstation-card text-center">
|
||||
<p style="background-color:{{row.background_color}};color:#fff">{{row.status}}</p>
|
||||
<div>{{row.workstation_name}}</div>
|
||||
<div class="workstation-card" style="display: grid;">
|
||||
<span class="ellipsis" title="{{row.name}}">
|
||||
{{row.workstation_name}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{% }); %}
|
||||
@@ -511,6 +511,10 @@ body[data-route="pos"] {
|
||||
padding-bottom: 25px;
|
||||
}
|
||||
|
||||
.workstation-image-cls {
|
||||
height: 9rem;
|
||||
}
|
||||
|
||||
.plant-floor-filter {
|
||||
padding-top: 10px;
|
||||
display: flex;
|
||||
@@ -519,8 +523,8 @@ body[data-route="pos"] {
|
||||
|
||||
.plant-floor-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
gap: var(--margin-xl);
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
gap: var(--margin-lg);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 620px) {
|
||||
@@ -536,7 +540,7 @@ body[data-route="pos"] {
|
||||
.plant-floor-container .workstation-image-link {
|
||||
width: 100%;
|
||||
font-size: 50px;
|
||||
margin: var(--margin-sm);
|
||||
margin: var(--margin-xs);
|
||||
min-height: 9rem;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user