diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py
index 13f1f3b8757a..1d87af60bab7 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -369,7 +369,7 @@ def validate_fg_item_for_subcontracting(self):
item.idx, item.fg_item
)
)
- elif not frappe.get_value("Item", item.fg_item, "default_bom"):
+ elif not item.bom and not frappe.get_value("Item", item.fg_item, "default_bom"):
frappe.throw(
_("Row #{0}: Default BOM not found for FG Item {1}").format(
item.idx, item.fg_item
@@ -919,6 +919,14 @@ def get_mapped_subcontracting_order(source_name, target_doc=None):
for idx, item in enumerate(target_doc.items):
item.warehouse = source_doc.items[idx].warehouse
+ for idx, item in enumerate(target_doc.items):
+ item.job_card = source_doc.items[idx].job_card
+ if not target_doc.supplier_warehouse:
+ # WIP warehouse is set as Supplier Warehouse in Job Card
+ target_doc.supplier_warehouse = frappe.get_cached_value(
+ "Job Card", item.job_card, "wip_warehouse"
+ )
+
return target_doc
diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
index bce7ed15b125..5f4c9f0fd43d 100644
--- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
+++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
@@ -110,7 +110,9 @@
"production_plan",
"production_plan_item",
"production_plan_sub_assembly_item",
- "page_break"
+ "page_break",
+ "column_break_pjyo",
+ "job_card"
],
"fields": [
{
@@ -909,13 +911,24 @@
{
"fieldname": "column_break_fyqr",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "column_break_pjyo",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "job_card",
+ "fieldtype": "Link",
+ "label": "Job Card",
+ "options": "Job Card",
+ "search_index": 1
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2024-03-27 13:10:24.979325",
+ "modified": "2024-03-27 13:12:24.979325",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Item",
diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js
index 6267ee4d029d..f2692d21bfff 100644
--- a/erpnext/manufacturing/doctype/bom/bom.js
+++ b/erpnext/manufacturing/doctype/bom/bom.js
@@ -19,6 +19,21 @@ frappe.ui.form.on("BOM", {
};
});
+ frm.set_query("bom_no", "operations", function (doc, cdt, cdn) {
+ let row = locals[cdt][cdn];
+ return {
+ query: "erpnext.controllers.queries.bom",
+ filters: {
+ currency: frm.doc.currency,
+ company: frm.doc.company,
+ item: row.finished_good,
+ is_active: 1,
+ docstatus: 1,
+ track_semi_finished_goods: 0,
+ },
+ };
+ });
+
frm.set_query("source_warehouse", "items", function () {
return {
filters: {
@@ -85,6 +100,27 @@ frappe.ui.form.on("BOM", {
frm.get_field("items").grid.set_multiple_add("item_code", "qty");
},
+ default_source_warehouse(frm) {
+ if (frm.doc.default_source_warehouse) {
+ frm.doc.operations.forEach((d) => {
+ frappe.model.set_value(
+ d.doctype,
+ d.name,
+ "source_warehouse",
+ frm.doc.default_source_warehouse
+ );
+ });
+ }
+ },
+
+ default_target_warehouse(frm) {
+ if (frm.doc.default_source_warehouse) {
+ frm.doc.operations.forEach((d) => {
+ frappe.model.set_value(d.doctype, d.name, "fg_warehouse", frm.doc.default_target_warehouse);
+ });
+ }
+ },
+
refresh(frm) {
frm.toggle_enable("item", frm.doc.__islocal);
@@ -96,22 +132,35 @@ frappe.ui.form.on("BOM", {
});
if (!frm.is_new() && frm.doc.docstatus < 2) {
- frm.add_custom_button(__("Update Cost"), function () {
- frm.events.update_cost(frm, true);
- });
- frm.add_custom_button(__("Browse BOM"), function () {
- frappe.route_options = {
- bom: frm.doc.name,
- };
- frappe.set_route("Tree", "BOM");
- });
+ frm.add_custom_button(
+ __("Update Cost"),
+ function () {
+ frm.events.update_cost(frm, true);
+ },
+ __("Actions")
+ );
+
+ frm.add_custom_button(
+ __("Browse BOM"),
+ function () {
+ frappe.route_options = {
+ bom: frm.doc.name,
+ };
+ frappe.set_route("Tree", "BOM");
+ },
+ __("Actions")
+ );
}
if (!frm.is_new() && !frm.doc.docstatus == 0) {
- frm.add_custom_button(__("New Version"), function () {
- let new_bom = frappe.model.copy_doc(frm.doc);
- frappe.set_route("Form", "BOM", new_bom.name);
- });
+ frm.add_custom_button(
+ __("New Version"),
+ function () {
+ let new_bom = frappe.model.copy_doc(frm.doc);
+ frappe.set_route("Form", "BOM", new_bom.name);
+ },
+ __("Actions")
+ );
}
if (frm.doc.docstatus == 1) {
@@ -432,6 +481,28 @@ frappe.ui.form.on("BOM", {
},
});
+frappe.ui.form.on("BOM Operation", {
+ bom_no(frm, cdt, cdn) {
+ let row = locals[cdt][cdn];
+
+ if (row.bom_no && row.finished_good) {
+ frappe.call({
+ method: "add_materials_from_bom",
+ doc: frm.doc,
+ args: {
+ finished_good: row.finished_good,
+ bom_no: row.bom_no,
+ operation_row_id: row.idx,
+ qty: row.finished_good_qty,
+ },
+ callback(r) {
+ refresh_field("items");
+ },
+ });
+ }
+ },
+});
+
erpnext.bom.BomController = class BomController extends erpnext.TransactionController {
conversion_rate(doc) {
if (this.frm.doc.currency === this.get_company_currency()) {
@@ -801,3 +872,88 @@ function trigger_process_loss_qty_prompt(frm, cdt, cdn, item_code) {
__("Set Quantity")
);
}
+
+frappe.ui.form.on("BOM Operation", {
+ add_raw_materials(frm, cdt, cdn) {
+ let row = locals[cdt][cdn];
+ frm.events._prompt_for_raw_materials(frm, row);
+ },
+});
+
+frappe.ui.form.on("BOM", {
+ _prompt_for_raw_materials(frm, row) {
+ let fields = frm.events.get_fields_for_prompt(frm, row);
+ frm._bom_rm_dialog = new frappe.ui.Dialog({
+ title: __("Add Raw Materials"),
+ fields: fields,
+ primary_action_label: __("Add"),
+ primary_action: () => {
+ let values = frm._bom_rm_dialog.get_values();
+ if (values) {
+ frm.events._add_raw_materials(frm, values);
+ frm._bom_rm_dialog.hide();
+ }
+ },
+ });
+
+ frm._bom_rm_dialog.show();
+ },
+
+ get_fields_for_prompt(frm, row) {
+ return [
+ {
+ 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);
+ },
+ get_query() {
+ return {
+ filters: {
+ name: ["!=", row.finished_good],
+ },
+ };
+ },
+ },
+ {
+ label: __("Qty"),
+ fieldname: "qty",
+ default: 1.0,
+ fieldtype: "Float",
+ reqd: 1,
+ in_list_view: 1,
+ },
+ ],
+ },
+ {
+ fieldname: "operation_row_id",
+ fieldtype: "Data",
+ hidden: 1,
+ default: row.idx,
+ },
+ ];
+ },
+
+ _add_raw_materials(frm, values) {
+ frm.call({
+ method: "add_raw_materials",
+ doc: frm.doc,
+ args: {
+ operation_row_id: values.operation_row_id,
+ items: values.items,
+ },
+ });
+ },
+});
diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json
index 67de6a0632b9..dcc7a4f21768 100644
--- a/erpnext/manufacturing/doctype/bom/bom.json
+++ b/erpnext/manufacturing/doctype/bom/bom.json
@@ -26,19 +26,22 @@
"column_break_ivyw",
"currency",
"conversion_rate",
- "materials_section",
- "items",
- "section_break_21",
"operations_section_section",
"with_operations",
+ "track_semi_finished_goods",
"column_break_23",
"transfer_material_against",
"routing",
"fg_based_operating_cost",
+ "column_break_joxb",
+ "default_source_warehouse",
+ "default_target_warehouse",
"fg_based_section_section",
"operating_cost_per_bom_quantity",
"operations_section",
"operations",
+ "materials_section",
+ "items",
"scrap_section",
"scrap_items_section",
"scrap_items",
@@ -59,8 +62,8 @@
"base_total_cost",
"more_info_tab",
"item_name",
- "description",
"column_break_27",
+ "description",
"has_variants",
"quality_inspection_section_break",
"inspection_required",
@@ -211,7 +214,7 @@
},
{
"default": "Work Order",
- "depends_on": "with_operations",
+ "depends_on": "eval: doc.with_operations === 1 && doc.track_semi_finished_goods === 0",
"fieldname": "transfer_material_against",
"fieldtype": "Select",
"label": "Transfer Material Against",
@@ -406,8 +409,8 @@
{
"depends_on": "eval:!doc.__islocal",
"fieldname": "section_break0",
- "fieldtype": "Section Break",
- "label": "Materials Required (Exploded)"
+ "fieldtype": "Tab Break",
+ "label": "Exploded Items"
},
{
"fieldname": "exploded_items",
@@ -485,11 +488,6 @@
"fieldtype": "Check",
"label": "Show Operations"
},
- {
- "fieldname": "section_break_21",
- "fieldtype": "Tab Break",
- "label": "Operations"
- },
{
"fieldname": "column_break_23",
"fieldtype": "Column Break"
@@ -534,6 +532,8 @@
"show_dashboard": 1
},
{
+ "collapsible": 1,
+ "collapsible_depends_on": "eval:doc.with_operations",
"fieldname": "operations_section_section",
"fieldtype": "Section Break",
"label": "Operations"
@@ -617,7 +617,8 @@
"no_copy": 1,
"options": "BOM Creator",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "search_index": 1
},
{
"fieldname": "bom_creator_item",
@@ -625,11 +626,36 @@
"label": "BOM Creator Item",
"no_copy": 1,
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "search_index": 1
},
{
"fieldname": "column_break_oxbz",
"fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "depends_on": "with_operations",
+ "description": "Users can consume raw materials and add semi-finished goods or final finished goods against the operation using job cards.",
+ "fieldname": "track_semi_finished_goods",
+ "fieldtype": "Check",
+ "label": "Track Semi Finished Goods"
+ },
+ {
+ "fieldname": "column_break_joxb",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "default_source_warehouse",
+ "fieldtype": "Link",
+ "label": "Default Source Warehouse",
+ "options": "Warehouse"
+ },
+ {
+ "fieldname": "default_target_warehouse",
+ "fieldtype": "Link",
+ "label": "Default Target Warehouse",
+ "options": "Warehouse"
}
],
"icon": "fa fa-sitemap",
@@ -637,7 +663,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
- "modified": "2024-04-02 16:22:47.518411",
+ "modified": "2024-04-02 16:24:47.518411",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM",
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 40b4c4f74552..5ff531e797d5 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -10,7 +10,7 @@
from frappe import _
from frappe.core.doctype.version.version import get_diff
from frappe.model.mapper import get_mapped_doc
-from frappe.utils import cint, cstr, flt, today
+from frappe.utils import cint, cstr, flt, parse_json, today
from frappe.website.website_generator import WebsiteGenerator
import erpnext
@@ -125,6 +125,8 @@ class BOM(WebsiteGenerator):
company: DF.Link
conversion_rate: DF.Float
currency: DF.Link
+ default_source_warehouse: DF.Link | None
+ default_target_warehouse: DF.Link | None
description: DF.SmallText | None
exploded_items: DF.Table[BOMExplosionItem]
fg_based_operating_cost: DF.Check
@@ -136,6 +138,7 @@ class BOM(WebsiteGenerator):
item: DF.Link
item_name: DF.Data | None
items: DF.Table[BOMItem]
+ track_semi_finished_goods: DF.Check
operating_cost: DF.Currency
operating_cost_per_bom_quantity: DF.Currency
operations: DF.Table[BOMOperation]
@@ -245,6 +248,7 @@ def validate(self):
self.clear_inspection()
self.validate_main_item()
self.validate_currency()
+ self.set_materials_based_on_operation_bom()
self.set_conversion_rate()
self.set_plc_conversion_rate()
self.validate_uom_is_interger()
@@ -544,6 +548,9 @@ def clear_operations(self):
if not self.with_operations:
self.set("operations", [])
+ if not self.with_operations and self.track_semi_finished_goods:
+ self.track_semi_finished_goods = 0
+
def clear_inspection(self):
if not self.inspection_required:
self.quality_inspection_template = None
@@ -645,6 +652,49 @@ def _throw_error(bom_name):
if self.name in {d.bom_no for d in self.items}:
_throw_error(self.name)
+ def set_materials_based_on_operation_bom(self):
+ if not self.track_semi_finished_goods:
+ return
+
+ for row in self.get("operations"):
+ if row.bom_no and row.finished_good:
+ self.add_materials_from_bom(row.finished_good, row.bom_no, row.idx, qty=row.finished_good_qty)
+
+ @frappe.whitelist()
+ def add_raw_materials(self, operation_row_id, items):
+ if isinstance(items, str):
+ items = parse_json(items)
+
+ for row in items:
+ row = parse_json(row)
+
+ row.update(get_item_details(row.get("item_code")))
+ row.operation_row_id = operation_row_id
+ row.idx = None
+ row.name = None
+ self.append("items", row)
+
+ self.save()
+
+ @frappe.whitelist()
+ def add_materials_from_bom(self, finished_good, bom_no, operation_row_id, qty=None):
+ if not frappe.db.exists("BOM", {"item": finished_good, "name": bom_no, "docstatus": 1}):
+ frappe.throw(_("BOM {0} not found for the item {1}").format(bom_no, finished_good))
+
+ if not qty:
+ qty = 1
+
+ for row in self.items:
+ if row.operation_row_id == operation_row_id:
+ return
+
+ bom_items = get_bom_items(bom_no, self.company, qty=qty, fetch_exploded=0)
+ for row in bom_items:
+ row.uom = row.stock_uom
+ row.operation_row_id = operation_row_id
+ row.idx = None
+ self.append("items", row)
+
def traverse_tree(self, bom_list=None):
def _get_children(bom_no):
children = frappe.cache().hget("bom_children", bom_no)
@@ -1094,6 +1144,11 @@ def get_bom_items_as_dict(
):
item_dict = {}
+ group_by_cond = "group by item_code, stock_uom"
+ if frappe.get_cached_value("BOM", bom, "track_semi_finished_goods"):
+ fetch_exploded = 0
+ group_by_cond = "group by item_code, operation_row_id, stock_uom"
+
# Did not use qty_consumed_per_unit in the query, as it leads to rounding loss
query = """select
bom_item.item_code,
@@ -1122,7 +1177,7 @@ def get_bom_items_as_dict(
and bom.name = %(bom)s
and item.is_stock_item in (1, {is_stock_item})
{where_conditions}
- group by item_code, stock_uom
+ {group_by_cond}
order by idx"""
is_stock_item = 0 if include_non_stock_items else 1
@@ -1132,6 +1187,7 @@ def get_bom_items_as_dict(
where_conditions="",
is_stock_item=is_stock_item,
qty_field="stock_qty",
+ group_by_cond=group_by_cond,
select_columns=""", bom_item.source_warehouse, bom_item.operation,
bom_item.include_item_in_manufacturing, bom_item.description, bom_item.rate, bom_item.sourced_by_supplier,
(Select idx from `tabBOM Item` where item_code = bom_item.item_code and parent = %(parent)s limit 1) as idx""",
@@ -1147,6 +1203,7 @@ def get_bom_items_as_dict(
select_columns=", item.description",
is_stock_item=is_stock_item,
qty_field="stock_qty",
+ group_by_cond=group_by_cond,
)
items = frappe.db.sql(query, {"qty": qty, "bom": bom, "company": company}, as_dict=True)
@@ -1158,15 +1215,20 @@ def get_bom_items_as_dict(
qty_field="stock_qty" if fetch_qty_in_stock_uom else "qty",
select_columns=""", bom_item.uom, bom_item.conversion_factor, bom_item.source_warehouse,
bom_item.operation, bom_item.include_item_in_manufacturing, bom_item.sourced_by_supplier,
- bom_item.description, bom_item.base_rate as rate """,
+ bom_item.description, bom_item.base_rate as rate, bom_item.operation_row_id """,
+ group_by_cond=group_by_cond,
)
items = frappe.db.sql(query, {"qty": qty, "bom": bom, "company": company}, as_dict=True)
for item in items:
- if item.item_code in item_dict:
- item_dict[item.item_code]["qty"] += flt(item.qty)
+ key = item.item_code
+ if item.operation_row_id:
+ key = (item.item_code, item.operation_row_id)
+
+ if key in item_dict:
+ item_dict[key]["qty"] += flt(item.qty)
else:
- item_dict[item.item_code] = item
+ item_dict[key] = item
for item, item_details in item_dict.items():
for d in [
diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.js b/erpnext/manufacturing/doctype/bom_creator/bom_creator.js
index 32231aa49494..b44c9f53f238 100644
--- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.js
+++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.js
@@ -88,9 +88,77 @@ frappe.ui.form.on("BOM Creator", {
reqd: 1,
default: 1.0,
},
+ { fieldtype: "Section Break" },
+ {
+ label: __("Track Operations"),
+ fieldtype: "Check",
+ fieldname: "track_operations",
+ onchange: (r) => {
+ let track_operations = dialog.get_value("track_operations");
+ if (r.type === "input" && !track_operations) {
+ dialog.set_value("track_semi_finished_goods", 0);
+ }
+ },
+ },
+ { fieldtype: "Column Break" },
+ {
+ label: __("Track Semi Finished Goods"),
+ fieldtype: "Check",
+ fieldname: "track_semi_finished_goods",
+ depends_on: "eval:doc.track_operations",
+ },
+ {
+ fieldtype: "Section Break",
+ label: __("Final Product Operation"),
+ depends_on: "eval:doc.track_semi_finished_goods",
+ },
+ {
+ label: __("Operation"),
+ fieldtype: "Link",
+ fieldname: "operation",
+ options: "Operation",
+ default: "Assembly",
+ mandatory_depends_on: "eval:doc.track_semi_finished_goods",
+ depends_on: "eval:doc.track_semi_finished_goods",
+ },
+ {
+ label: __("Operation Time (in mins)"),
+ fieldtype: "Float",
+ fieldname: "operation_time",
+ mandatory_depends_on: "eval:doc.track_semi_finished_goods",
+ depends_on: "eval:doc.track_semi_finished_goods",
+ },
+ { fieldtype: "Column Break" },
+ {
+ label: __("Workstation Type"),
+ fieldtype: "Link",
+ fieldname: "workstation_type",
+ options: "Workstation",
+ depends_on: "eval:doc.track_semi_finished_goods",
+ },
+ {
+ label: __("Workstation"),
+ fieldtype: "Link",
+ fieldname: "workstation",
+ options: "Workstation",
+ depends_on: "eval:doc.track_semi_finished_goods",
+ get_query() {
+ let workstation_type = dialog.get_value("workstation_type");
+
+ if (workstation_type) {
+ return {
+ filters: {
+ workstation_type: dialog.get_value("workstation_type"),
+ },
+ };
+ }
+ },
+ },
],
primary_action_label: __("Create"),
primary_action: (values) => {
+ frm.events.validate_dialog_values(frm, values);
+
values.doctype = frm.doc.doctype;
frappe.db.insert(values).then((doc) => {
frappe.set_route("Form", doc.doctype, doc.name);
@@ -102,6 +170,18 @@ frappe.ui.form.on("BOM Creator", {
dialog.show();
},
+ validate_dialog_values(frm, values) {
+ if (values.track_semi_finished_goods) {
+ if (values.final_operation_time <= 0) {
+ frappe.throw(__("Operation Time must be greater than 0"));
+ }
+
+ if (!values.workstation && !values.workstation_type) {
+ frappe.throw(__("Either Workstation or Workstation Type is mandatory"));
+ }
+ }
+ },
+
set_queries(frm) {
frm.set_query("bom_no", "items", function (doc, cdt, cdn) {
let item = frappe.get_doc(cdt, cdn);
@@ -121,6 +201,16 @@ frappe.ui.form.on("BOM Creator", {
query: "erpnext.controllers.queries.item_query",
};
});
+
+ frm.set_query("workstation", (doc) => {
+ if (doc.workstation_type) {
+ return {
+ filters: {
+ workstation_type: doc.workstation_type,
+ },
+ };
+ }
+ });
},
refresh(frm) {
diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.json b/erpnext/manufacturing/doctype/bom_creator/bom_creator.json
index 1e8237c03f78..2b4b9a055aa3 100644
--- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.json
+++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.json
@@ -37,6 +37,23 @@
"items",
"costing_detail",
"raw_material_cost",
+ "configuration_section",
+ "track_operations",
+ "column_break_obzr",
+ "track_semi_finished_goods",
+ "final_product_operation_section",
+ "operation",
+ "operation_time",
+ "column_break_xnlu",
+ "workstation_type",
+ "workstation",
+ "final_product_warehouse_section",
+ "skip_material_transfer",
+ "backflush_from_wip_warehouse",
+ "source_warehouse",
+ "column_break_buha",
+ "wip_warehouse",
+ "fg_warehouse",
"remarks_tab",
"remarks",
"section_break_yixm",
@@ -278,6 +295,104 @@
"fieldtype": "Text",
"label": "Error Log",
"read_only": 1
+ },
+ {
+ "fieldname": "configuration_section",
+ "fieldtype": "Section Break",
+ "label": "Operation"
+ },
+ {
+ "default": "0",
+ "depends_on": "track_operations",
+ "fieldname": "track_semi_finished_goods",
+ "fieldtype": "Check",
+ "label": "Track Semi Finished Goods"
+ },
+ {
+ "fieldname": "column_break_obzr",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "track_operations",
+ "fieldtype": "Check",
+ "label": "Track Operations"
+ },
+ {
+ "depends_on": "eval:doc.track_semi_finished_goods === 1",
+ "fieldname": "final_product_operation_section",
+ "fieldtype": "Section Break",
+ "label": "Final Product Operation & Workstation"
+ },
+ {
+ "fieldname": "column_break_xnlu",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "operation",
+ "fieldtype": "Link",
+ "label": "Operation",
+ "options": "Operation"
+ },
+ {
+ "fieldname": "operation_time",
+ "fieldtype": "Float",
+ "label": "Operation Time (in mins)"
+ },
+ {
+ "fieldname": "workstation",
+ "fieldtype": "Link",
+ "label": "Workstation",
+ "options": "Workstation"
+ },
+ {
+ "fieldname": "workstation_type",
+ "fieldtype": "Link",
+ "label": "Workstation Type",
+ "options": "Workstation Type"
+ },
+ {
+ "depends_on": "eval:!doc.backflush_from_wip_warehouse",
+ "fieldname": "source_warehouse",
+ "fieldtype": "Link",
+ "label": "Source Warehouse",
+ "options": "Warehouse"
+ },
+ {
+ "depends_on": "eval:!doc.skip_material_transfer || doc.backflush_from_wip_warehouse",
+ "fieldname": "wip_warehouse",
+ "fieldtype": "Link",
+ "label": "Work In Progress Warehouse",
+ "options": "Warehouse"
+ },
+ {
+ "fieldname": "fg_warehouse",
+ "fieldtype": "Link",
+ "label": "Finished Good Warehouse",
+ "options": "Warehouse"
+ },
+ {
+ "depends_on": "eval:doc.track_semi_finished_goods === 1",
+ "fieldname": "final_product_warehouse_section",
+ "fieldtype": "Section Break",
+ "label": "Final Product Warehouse"
+ },
+ {
+ "default": "0",
+ "fieldname": "skip_material_transfer",
+ "fieldtype": "Check",
+ "label": "Skip Material Transfer"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.skip_material_transfer",
+ "fieldname": "backflush_from_wip_warehouse",
+ "fieldtype": "Check",
+ "label": "Backflush Materials From WIP Warehouse"
+ },
+ {
+ "fieldname": "column_break_buha",
+ "fieldtype": "Column Break"
}
],
"icon": "fa fa-sitemap",
@@ -288,7 +403,7 @@
"link_fieldname": "bom_creator"
}
],
- "modified": "2024-04-02 16:30:59.779190",
+ "modified": "2024-05-26 15:47:10.101420",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Creator",
diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py
index e236e7a63453..69455027f905 100644
--- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py
+++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py
@@ -43,16 +43,20 @@ class BOMCreator(Document):
from erpnext.manufacturing.doctype.bom_creator_item.bom_creator_item import BOMCreatorItem
amended_from: DF.Link | None
+ backflush_from_wip_warehouse: DF.Check
buying_price_list: DF.Link | None
company: DF.Link
conversion_rate: DF.Float
currency: DF.Link
default_warehouse: DF.Link | None
error_log: DF.Text | None
+ fg_warehouse: DF.Link | None
item_code: DF.Link
item_group: DF.Link | None
item_name: DF.Data | None
items: DF.Table[BOMCreatorItem]
+ operation: DF.Link | None
+ operation_time: DF.Float
plc_conversion_rate: DF.Float
price_list_currency: DF.Link | None
project: DF.Link | None
@@ -61,8 +65,15 @@ class BOMCreator(Document):
remarks: DF.TextEditor | None
rm_cost_as_per: DF.Literal["Valuation Rate", "Last Purchase Rate", "Price List"]
set_rate_based_on_warehouse: DF.Check
+ skip_material_transfer: DF.Check
+ source_warehouse: DF.Link | None
status: DF.Literal["Draft", "Submitted", "In Progress", "Completed", "Failed", "Cancelled"]
+ track_operations: DF.Check
+ track_semi_finished_goods: DF.Check
uom: DF.Link | None
+ wip_warehouse: DF.Link | None
+ workstation: DF.Link | None
+ workstation_type: DF.Link | None
# end: auto-generated types
def before_save(self):
@@ -236,8 +247,10 @@ def create_boms(self):
self.db_set("status", "In Progress")
production_item_wise_rm = OrderedDict({})
+
+ final_product = (self.item_code, self.name)
production_item_wise_rm.setdefault(
- (self.item_code, self.name), frappe._dict({"items": [], "bom_no": "", "fg_item_data": self})
+ final_product, frappe._dict({"items": [], "bom_no": "", "fg_item_data": self})
)
for row in self.items:
@@ -257,9 +270,15 @@ def create_boms(self):
try:
for d in reverse_tree:
+ if self.track_operations and self.track_semi_finished_goods and final_product == d:
+ continue
+
fg_item_data = production_item_wise_rm.get(d).fg_item_data
self.create_bom(fg_item_data, production_item_wise_rm)
+ if self.track_operations and self.track_semi_finished_goods:
+ self.make_bom_for_final_product(production_item_wise_rm)
+
frappe.msgprint(_("BOMs created successfully"))
except Exception:
traceback = frappe.get_traceback(with_context=True)
@@ -272,6 +291,81 @@ def create_boms(self):
frappe.msgprint(_("BOMs creation failed"))
+ def make_bom_for_final_product(self, production_item_wise_rm):
+ bom = frappe.new_doc("BOM")
+ bom.update(
+ {
+ "item": self.item_code,
+ "bom_type": "Production",
+ "quantity": self.qty,
+ "allow_alternative_item": 1,
+ "bom_creator": self.name,
+ "bom_creator_item": self.name,
+ "rm_cost_as_per": "Manual",
+ "with_operations": 1,
+ "track_semi_finished_goods": 1,
+ }
+ )
+
+ for field in BOM_FIELDS:
+ if self.get(field):
+ bom.set(field, self.get(field))
+
+ for item in self.items:
+ if not item.is_expandable or not item.operation:
+ continue
+
+ bom.append(
+ "operations",
+ {
+ "operation": item.operation,
+ "workstation": item.workstation,
+ "source_warehouse": item.source_warehouse,
+ "wip_warehouse": item.wip_warehouse,
+ "fg_warehouse": item.fg_warehouse,
+ "finished_good": item.item_code,
+ "finished_good_qty": item.qty,
+ "bom_no": production_item_wise_rm[(item.item_code, item.name)].bom_no,
+ "workstation_type": item.workstation_type,
+ "time_in_mins": item.operation_time,
+ "is_subcontracted": item.is_subcontracted,
+ "skip_material_transfer": item.skip_material_transfer,
+ "backflush_from_wip_warehouse": item.backflush_from_wip_warehouse,
+ },
+ )
+
+ operation_row = bom.append(
+ "operations",
+ {
+ "operation": self.operation,
+ "time_in_mins": self.operation_time,
+ "workstation": self.workstation,
+ "workstation_type": self.workstation_type,
+ "finished_good": self.item_code,
+ "finished_good_qty": self.qty,
+ "source_warehouse": self.source_warehouse,
+ "wip_warehouse": self.wip_warehouse,
+ "fg_warehouse": self.fg_warehouse,
+ "skip_material_transfer": self.skip_material_transfer,
+ "backflush_from_wip_warehouse": self.backflush_from_wip_warehouse,
+ },
+ )
+
+ final_product = (self.item_code, self.name)
+ items = production_item_wise_rm.get(final_product).get("items")
+
+ bom.set_materials_based_on_operation_bom()
+
+ for item in items:
+ item_args = {"operation_row_id": operation_row.idx}
+ for field in BOM_ITEM_FIELDS:
+ item_args[field] = item.get(field)
+
+ bom.append("items", item_args)
+
+ bom.save(ignore_permissions=True)
+ bom.submit()
+
def create_bom(self, row, production_item_wise_rm):
bom_creator_item = row.name if row.name != self.name else ""
if frappe.db.exists(
@@ -297,6 +391,24 @@ def create_bom(self, row, production_item_wise_rm):
}
)
+ if self.track_operations and not self.track_semi_finished_goods:
+ if row.item_code == self.item_code:
+ bom.with_operations = 1
+ bom.transfer_material_against = "Work Order"
+ for item in self.items:
+ if not item.operation:
+ continue
+
+ bom.append(
+ "operations",
+ {
+ "operation": item.operation,
+ "workstation_type": item.workstation_type,
+ "workstation": item.workstation,
+ "time_in_mins": item.operation_time,
+ },
+ )
+
for field in BOM_FIELDS:
if self.get(field):
bom.set(field, self.get(field))
@@ -352,6 +464,16 @@ def get_children(doctype=None, parent=None, **kwargs):
"uom",
"rate",
"amount",
+ "workstation_type",
+ "operation",
+ "operation_time",
+ "is_subcontracted",
+ "workstation",
+ "source_warehouse",
+ "wip_warehouse",
+ "fg_warehouse",
+ "skip_material_transfer",
+ "backflush_from_wip_warehouse",
]
query_filters = {
@@ -365,6 +487,12 @@ def get_children(doctype=None, parent=None, **kwargs):
return frappe.get_all("BOM Creator Item", fields=fields, filters=query_filters, order_by="idx")
+def get_parent_row_no(doc, name):
+ for row in doc.items:
+ if row.name == name:
+ return row.idx
+
+
@frappe.whitelist()
def add_item(**kwargs):
if isinstance(kwargs, str):
@@ -375,6 +503,11 @@ def add_item(**kwargs):
doc = frappe.get_doc("BOM Creator", kwargs.parent)
item_info = get_item_details(kwargs.item_code)
+
+ parent_row_no = ""
+ if kwargs.fg_reference_id and doc.name != kwargs.fg_reference_id:
+ parent_row_no = get_parent_row_no(doc, kwargs.fg_reference_id)
+
kwargs.update(
{
"uom": item_info.stock_uom,
@@ -383,6 +516,9 @@ def add_item(**kwargs):
}
)
+ if parent_row_no:
+ kwargs.update({"parent_row_no": parent_row_no})
+
doc.append("items", kwargs)
doc.save()
@@ -402,6 +538,7 @@ def add_sub_assembly(**kwargs):
name = kwargs.fg_reference_id
parent_row_no = ""
+
if not kwargs.convert_to_sub_assembly:
item_info = get_item_details(bom_item.item_code)
item_row = doc.append(
@@ -417,6 +554,14 @@ def add_sub_assembly(**kwargs):
"do_not_explode": 1,
"is_expandable": 1,
"stock_uom": item_info.stock_uom,
+ "operation": bom_item.operation,
+ "workstation_type": bom_item.workstation_type,
+ "operation_time": bom_item.operation_time,
+ "is_subcontracted": bom_item.is_subcontracted,
+ "workstation": bom_item.workstation,
+ "source_warehouse": bom_item.source_warehouse,
+ "wip_warehouse": bom_item.wip_warehouse,
+ "fg_warehouse": bom_item.fg_warehouse,
},
)
@@ -426,6 +571,20 @@ def add_sub_assembly(**kwargs):
parent_row_no = [row.idx for row in doc.items if row.name == kwargs.fg_reference_id]
if parent_row_no:
parent_row_no = parent_row_no[0]
+ doc.items[parent_row_no - 1].update(
+ {
+ "operation": bom_item.operation,
+ "workstation_type": bom_item.workstation_type,
+ "operation_time": bom_item.operation_time,
+ "is_subcontracted": bom_item.is_subcontracted,
+ "workstation": bom_item.workstation,
+ "source_warehouse": bom_item.source_warehouse,
+ "wip_warehouse": bom_item.wip_warehouse,
+ "fg_warehouse": bom_item.fg_warehouse,
+ "skip_material_transfer": bom_item.skip_material_transfer,
+ "backflush_from_wip_warehouse": bom_item.backflush_from_wip_warehouse,
+ }
+ )
for row in bom_item.get("items"):
row = frappe._dict(row)
@@ -482,10 +641,16 @@ def delete_node(**kwargs):
@frappe.whitelist()
-def edit_qty(doctype, docname, qty, parent):
- frappe.db.set_value(doctype, docname, "qty", qty)
+def edit_bom_creator(doctype, docname, data, parent):
+ if isinstance(data, str):
+ data = frappe.parse_json(data)
+
+ frappe.db.set_value(doctype, docname, data)
+
doc = frappe.get_doc("BOM Creator", parent)
doc.set_rate_for_items()
doc.save()
+ frappe.msgprint(_("Updated successfully"), alert=True)
+
return doc
diff --git a/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json
index e9545ac5385a..9be55667f803 100644
--- a/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json
+++ b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json
@@ -11,10 +11,23 @@
"item_group",
"column_break_f63f",
"fg_item",
- "source_warehouse",
"is_expandable",
"sourced_by_supplier",
"bom_created",
+ "is_subcontracted",
+ "operation_section",
+ "operation",
+ "operation_time",
+ "column_break_cbnk",
+ "workstation_type",
+ "workstation",
+ "warehouse_section",
+ "skip_material_transfer",
+ "backflush_from_wip_warehouse",
+ "source_warehouse",
+ "column_break_xutc",
+ "wip_warehouse",
+ "fg_warehouse",
"description_section",
"description",
"quantity_and_rate_section",
@@ -75,16 +88,18 @@
"reqd": 1
},
{
+ "depends_on": "eval:doc.skip_material_transfer && !doc.backflush_from_wip_warehouse",
"fieldname": "source_warehouse",
"fieldtype": "Link",
- "in_list_view": 1,
"label": "Source Warehouse",
"options": "Warehouse"
},
{
+ "columns": 1,
"default": "0",
"fieldname": "is_expandable",
"fieldtype": "Check",
+ "in_list_view": 1,
"label": "Is Expandable",
"read_only": 1
},
@@ -225,12 +240,87 @@
"label": "BOM Created",
"no_copy": 1,
"print_hide": 1
+ },
+ {
+ "fieldname": "operation_section",
+ "fieldtype": "Section Break",
+ "label": "Operation"
+ },
+ {
+ "fieldname": "operation",
+ "fieldtype": "Link",
+ "label": "Operation",
+ "options": "Operation"
+ },
+ {
+ "fieldname": "column_break_cbnk",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "workstation_type",
+ "fieldtype": "Link",
+ "label": "Workstation Type",
+ "options": "Workstation Type"
+ },
+ {
+ "description": "In Mins",
+ "fieldname": "operation_time",
+ "fieldtype": "Int",
+ "label": "Operation Time"
+ },
+ {
+ "fieldname": "workstation",
+ "fieldtype": "Link",
+ "label": "Workstation",
+ "options": "Workstation"
+ },
+ {
+ "fieldname": "warehouse_section",
+ "fieldtype": "Section Break",
+ "label": "Warehouse"
+ },
+ {
+ "depends_on": "eval:!doc.skip_material_transfer || doc.backflush_from_wip_warehouse",
+ "fieldname": "wip_warehouse",
+ "fieldtype": "Link",
+ "label": "Work In Progress Warehouse",
+ "options": "Warehouse"
+ },
+ {
+ "fieldname": "column_break_xutc",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "fg_warehouse",
+ "fieldtype": "Link",
+ "label": "Finished Good Warehouse",
+ "options": "Warehouse"
+ },
+ {
+ "default": "0",
+ "fieldname": "skip_material_transfer",
+ "fieldtype": "Check",
+ "label": "Skip Material Transfer"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.skip_material_transfer",
+ "fieldname": "backflush_from_wip_warehouse",
+ "fieldtype": "Check",
+ "label": "Backflush Materials From WIP Warehouse"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_subcontracted",
+ "fieldtype": "Check",
+ "label": "Is Subcontracted",
+ "read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2024-03-27 13:06:40.764747",
+ "modified": "2024-06-01 18:45:24.339532",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Creator Item",
diff --git a/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.py b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.py
index e172f36224d8..2510a02ddc71 100644
--- a/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.py
+++ b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.py
@@ -15,6 +15,7 @@ class BOMCreatorItem(Document):
from frappe.types import DF
amount: DF.Currency
+ backflush_from_wip_warehouse: DF.Check
base_amount: DF.Currency
base_rate: DF.Currency
bom_created: DF.Check
@@ -23,22 +24,30 @@ class BOMCreatorItem(Document):
do_not_explode: DF.Check
fg_item: DF.Link
fg_reference_id: DF.Data | None
+ fg_warehouse: DF.Link | None
instruction: DF.SmallText | None
is_expandable: DF.Check
+ is_subcontracted: DF.Check
item_code: DF.Link
item_group: DF.Link | None
item_name: DF.Data | None
+ operation: DF.Link | None
+ operation_time: DF.Int
parent: DF.Data
parent_row_no: DF.Data | None
parentfield: DF.Data
parenttype: DF.Data
qty: DF.Float
rate: DF.Currency
+ skip_material_transfer: DF.Check
source_warehouse: DF.Link | None
sourced_by_supplier: DF.Check
stock_qty: DF.Float
stock_uom: DF.Link | None
uom: DF.Link | None
+ wip_warehouse: DF.Link | None
+ workstation: DF.Link | None
+ workstation_type: DF.Link | None
# end: auto-generated types
pass
diff --git a/erpnext/manufacturing/doctype/bom_item/bom_item.json b/erpnext/manufacturing/doctype/bom_item/bom_item.json
index 226cfe0162fb..1d530af34a28 100644
--- a/erpnext/manufacturing/doctype/bom_item/bom_item.json
+++ b/erpnext/manufacturing/doctype/bom_item/bom_item.json
@@ -9,6 +9,7 @@
"item_code",
"item_name",
"operation",
+ "operation_row_id",
"column_break_3",
"do_not_explode",
"bom_no",
@@ -293,13 +294,19 @@
"fieldtype": "Check",
"label": "Is Stock Item",
"read_only": 1
+ },
+ {
+ "depends_on": "eval:parent.track_semi_finished_goods ==1",
+ "fieldname": "operation_row_id",
+ "fieldtype": "Int",
+ "label": "Operation ID"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2024-03-27 13:06:41.079752",
+ "modified": "2024-03-27 13:08:41.079752",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Item",
diff --git a/erpnext/manufacturing/doctype/bom_item/bom_item.py b/erpnext/manufacturing/doctype/bom_item/bom_item.py
index 466253bf0bfa..87430d7d47d2 100644
--- a/erpnext/manufacturing/doctype/bom_item/bom_item.py
+++ b/erpnext/manufacturing/doctype/bom_item/bom_item.py
@@ -25,9 +25,11 @@ class BOMItem(Document):
has_variants: DF.Check
image: DF.Attach | None
include_item_in_manufacturing: DF.Check
+ is_stock_item: DF.Check
item_code: DF.Link
item_name: DF.Data | None
operation: DF.Link | None
+ operation_row_id: DF.Int
original_item: DF.Link | None
parent: DF.Data
parentfield: DF.Data
diff --git a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json
index aa62b027b06f..b9e960ab66e0 100644
--- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json
+++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json
@@ -6,24 +6,36 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
- "sequence_id",
"operation",
+ "sequence_id",
+ "finished_good",
+ "finished_good_qty",
+ "bom_no",
+ "add_raw_materials",
"col_break1",
"workstation_type",
"workstation",
"time_in_mins",
"fixed_time",
+ "is_subcontracted",
+ "is_final_finished_good",
+ "set_cost_based_on_bom_qty",
+ "warehouse_section",
+ "skip_material_transfer",
+ "backflush_from_wip_warehouse",
+ "source_warehouse",
+ "column_break_lbhy",
+ "wip_warehouse",
+ "fg_warehouse",
"costing_section",
"hour_rate",
"base_hour_rate",
- "column_break_9",
- "operating_cost",
- "base_operating_cost",
- "column_break_11",
"batch_size",
- "set_cost_based_on_bom_qty",
+ "column_break_11",
"cost_per_unit",
"base_cost_per_unit",
+ "operating_cost",
+ "base_operating_cost",
"more_information_section",
"description",
"column_break_18",
@@ -71,6 +83,7 @@
"precision": "2"
},
{
+ "columns": 1,
"description": "In minutes",
"fetch_from": "operation.total_operation_time",
"fetch_if_empty": 1,
@@ -87,7 +100,6 @@
"description": "Operation time does not depend on quantity to produce",
"fieldname": "fixed_time",
"fieldtype": "Check",
- "in_list_view": 1,
"label": "Fixed Time"
},
{
@@ -172,10 +184,6 @@
"fieldname": "column_break_18",
"fieldtype": "Column Break"
},
- {
- "fieldname": "column_break_9",
- "fieldtype": "Column Break"
- },
{
"default": "0",
"fieldname": "set_cost_based_on_bom_qty",
@@ -183,18 +191,106 @@
"label": "Set Operating Cost Based On BOM Quantity"
},
{
+ "columns": 1,
"fieldname": "workstation_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Workstation Type",
"options": "Workstation Type"
+ },
+ {
+ "fieldname": "finished_good",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "FG / Semi FG Item",
+ "options": "Item"
+ },
+ {
+ "columns": 1,
+ "fieldname": "bom_no",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "BOM No",
+ "options": "BOM"
+ },
+ {
+ "columns": 1,
+ "default": "1",
+ "fieldname": "finished_good_qty",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "FG Qty"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_final_finished_good",
+ "fieldtype": "Check",
+ "label": "Is Final Finished Good"
+ },
+ {
+ "fieldname": "warehouse_section",
+ "fieldtype": "Section Break",
+ "label": "Warehouse"
+ },
+ {
+ "depends_on": "eval:!doc.skip_material_transfer || doc.backflush_from_wip_warehouse",
+ "fieldname": "wip_warehouse",
+ "fieldtype": "Link",
+ "label": "WIP WH",
+ "options": "Warehouse"
+ },
+ {
+ "fieldname": "column_break_lbhy",
+ "fieldtype": "Column Break"
+ },
+ {
+ "columns": 1,
+ "fieldname": "fg_warehouse",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "FG WH",
+ "options": "Warehouse"
+ },
+ {
+ "columns": 1,
+ "depends_on": "eval:doc.skip_material_transfer && !doc.backflush_from_wip_warehouse",
+ "fieldname": "source_warehouse",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Source WH",
+ "options": "Warehouse"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_subcontracted",
+ "fieldtype": "Check",
+ "label": "Is Subcontracted"
+ },
+ {
+ "depends_on": "eval:!doc.bom_no",
+ "fieldname": "add_raw_materials",
+ "fieldtype": "Button",
+ "label": "Add Raw Materials"
+ },
+ {
+ "default": "0",
+ "fieldname": "skip_material_transfer",
+ "fieldtype": "Check",
+ "label": " Skip Material Transfer"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.skip_material_transfer",
+ "fieldname": "backflush_from_wip_warehouse",
+ "fieldtype": "Check",
+ "label": "Backflush Materials From WIP Warehouse"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2024-03-27 13:06:41.248462",
+ "modified": "2024-05-26 15:46:49.404875",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Operation",
diff --git a/erpnext/manufacturing/doctype/bom_operation/bom_operation.py b/erpnext/manufacturing/doctype/bom_operation/bom_operation.py
index 66ac02891b99..fd197e89e62a 100644
--- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.py
+++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.py
@@ -14,15 +14,22 @@ class BOMOperation(Document):
if TYPE_CHECKING:
from frappe.types import DF
+ backflush_from_wip_warehouse: DF.Check
base_cost_per_unit: DF.Float
base_hour_rate: DF.Currency
base_operating_cost: DF.Currency
batch_size: DF.Int
+ bom_no: DF.Link | None
cost_per_unit: DF.Float
description: DF.TextEditor | None
+ fg_warehouse: DF.Link | None
+ finished_good: DF.Link | None
+ finished_good_qty: DF.Float
fixed_time: DF.Check
hour_rate: DF.Currency
image: DF.Attach | None
+ is_final_finished_good: DF.Check
+ is_subcontracted: DF.Check
operating_cost: DF.Currency
operation: DF.Link
parent: DF.Data
@@ -30,7 +37,10 @@ class BOMOperation(Document):
parenttype: DF.Data
sequence_id: DF.Int
set_cost_based_on_bom_qty: DF.Check
+ skip_material_transfer: DF.Check
+ source_warehouse: DF.Link | None
time_in_mins: DF.Float
+ wip_warehouse: DF.Link | None
workstation: DF.Link | None
workstation_type: DF.Link | None
# end: auto-generated types
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js
index 4cc60a3b4a6d..2de5d9dad115 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.js
+++ b/erpnext/manufacturing/doctype/job_card/job_card.js
@@ -32,21 +32,61 @@ frappe.ui.form.on("Job Card", {
});
},
+ make_fields_read_only(frm) {
+ if (frm.doc.docstatus === 1) {
+ frm.set_df_property("employee", "read_only", 1);
+ frm.set_df_property("time_logs", "read_only", 1);
+ }
+
+ if (frm.doc.is_subcontracted) {
+ frm.set_df_property("wip_warehouse", "label", __("Supplier Warehouse"));
+ }
+ },
+
+ setup_stock_entry(frm) {
+ if (
+ frm.doc.finished_good &&
+ frm.doc.docstatus === 1 &&
+ !frm.doc.is_subcontracted &&
+ flt(frm.doc.for_quantity) + flt(frm.doc.process_loss_qty) > flt(frm.doc.manufactured_qty)
+ ) {
+ frm.add_custom_button(__("Make Stock Entry"), () => {
+ frm.call({
+ method: "make_stock_entry_for_semi_fg_item",
+ args: {
+ auto_submit: 1,
+ },
+ doc: frm.doc,
+ freeze: true,
+ callback() {
+ frm.reload_doc();
+ },
+ });
+ }).addClass("btn-primary");
+ }
+ },
+
refresh: function (frm) {
- frappe.flags.pause_job = 0;
- frappe.flags.resume_job = 0;
+ frm.trigger("setup_stock_entry");
+
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) {
frm.disable_save();
return;
}
+ if (frm.doc.is_subcontracted) {
+ frm.trigger("make_subcontracting_po");
+ return;
+ }
+
let has_stock_entry = frm.doc.__onload && frm.doc.__onload.has_stock_entry ? true : false;
frm.toggle_enable("for_quantity", !has_stock_entry);
- if (!frm.is_new() && has_items && frm.doc.docstatus < 2) {
+ if (!frm.is_new() && !frm.doc.skip_material_transfer && has_items && 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;
@@ -63,11 +103,11 @@ frappe.ui.form.on("Job Card", {
if (to_transfer || excess_transfer_allowed) {
frm.add_custom_button(__("Material Transfer"), () => {
frm.trigger("make_stock_entry");
- }).addClass("btn-primary");
+ });
}
}
- if (frm.doc.docstatus == 1 && !frm.doc.is_corrective_job_card) {
+ if (frm.doc.docstatus == 1 && !frm.doc.is_corrective_job_card && !frm.doc.finished_good) {
frm.trigger("setup_corrective_job_card");
}
@@ -84,31 +124,67 @@ frappe.ui.form.on("Job Card", {
frm.trigger("toggle_operation_number");
if (
- frm.doc.docstatus == 0 &&
- !frm.is_new() &&
- (frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity) &&
- (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)
+ 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)
) {
- // if Job Card is link to Work Order, the job card must not be able to start if Work Order not "Started"
- // and if stock mvt for WIP is required
- if (frm.doc.work_order) {
- frappe.db.get_value(
- "Work Order",
- frm.doc.work_order,
- ["skip_transfer", "status"],
- (result) => {
- if (
- result.skip_transfer === 1 ||
- result.status == "In Process" ||
- frm.doc.transferred_qty > 0 ||
- !frm.doc.items.length
- ) {
- frm.trigger("prepare_timer_buttons");
- }
+ if (!frm.doc.time_logs?.length) {
+ 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",
+ },
+ (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 {
- frm.trigger("prepare_timer_buttons");
+ if (frm.doc.for_quantity - frm.doc.manufactured_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");
}
}
@@ -116,7 +192,7 @@ frappe.ui.form.on("Job Card", {
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") {
+ if (r.message.transfer_material_against == "Work Order" && !frm.doc.operation_row_id) {
frm.set_df_property("items", "hidden", 1);
}
});
@@ -134,6 +210,75 @@ frappe.ui.form.on("Job Card", {
}
},
+ make_subcontracting_po(frm) {
+ if (frm.doc.docstatus === 1 && frm.doc.for_quantity > frm.doc.manufactured_qty) {
+ frm.add_custom_button(__("Make Subcontracting PO"), () => {
+ frappe.model.open_mapped_doc({
+ method: "erpnext.manufacturing.doctype.job_card.job_card.make_subcontracting_po",
+ frm: frm,
+ });
+ }).addClass("btn-primary");
+ }
+ },
+
+ start_timer(frm, start_time, employees) {
+ frm.call({
+ method: "start_timer",
+ doc: frm.doc,
+ args: {
+ start_time: start_time,
+ employees: employees,
+ },
+ callback: function (r) {
+ frm.reload_doc();
+ frm.trigger("make_dashboard");
+ },
+ });
+ },
+
+ make_finished_good(frm) {
+ let fields = [
+ {
+ fieldtype: "Float",
+ label: __("Completed Quantity"),
+ fieldname: "qty",
+ reqd: 1,
+ default: frm.doc.for_quantity - frm.doc.manufactured_qty,
+ },
+ {
+ fieldtype: "Datetime",
+ label: __("End Time"),
+ fieldname: "end_time",
+ default: frappe.datetime.now_datetime(),
+ },
+ ];
+
+ frappe.prompt(
+ fields,
+ (data) => {
+ if (data.qty <= 0) {
+ frappe.throw(__("Quantity should be greater than 0"));
+ }
+
+ 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);
+ frappe.set_route("Form", doc[0].doctype, doc[0].name);
+ },
+ });
+ },
+ __("Enter Value"),
+ __("Update"),
+ __("Set Finished Good Quantity")
+ );
+ },
+
setup_quality_inspection: function (frm) {
let quality_inspection_field = frm.get_docfield("quality_inspection");
quality_inspection_field.get_route_options_for_new_doc = function (frm) {
@@ -262,90 +407,6 @@ frappe.ui.form.on("Job Card", {
frm.toggle_reqd("operation_row_number", !frm.doc.operation_id && frm.doc.operation);
},
- prepare_timer_buttons: function (frm) {
- frm.trigger("make_dashboard");
-
- if (!frm.doc.started_time && !frm.doc.current_time) {
- frm.add_custom_button(__("Start Job"), () => {
- 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",
- },
- (d) => {
- frm.events.start_job(frm, "Work In Progress", d.employees);
- },
- __("Assign Job to Employee")
- );
- } else {
- frm.events.start_job(frm, "Work In Progress", frm.doc.employee);
- }
- }).addClass("btn-primary");
- } else if (frm.doc.status == "On Hold") {
- frm.add_custom_button(__("Resume Job"), () => {
- frm.events.start_job(frm, "Resume Job", frm.doc.employee);
- }).addClass("btn-primary");
- } else {
- frm.add_custom_button(__("Pause Job"), () => {
- frm.events.complete_job(frm, "On Hold");
- });
-
- frm.add_custom_button(__("Complete Job"), () => {
- var sub_operations = frm.doc.sub_operations;
-
- let set_qty = true;
- if (sub_operations && sub_operations.length > 1) {
- set_qty = false;
- let last_op_row = sub_operations[sub_operations.length - 2];
-
- if (last_op_row.status == "Complete") {
- set_qty = true;
- }
- }
-
- if (set_qty) {
- frappe.prompt(
- {
- fieldtype: "Float",
- label: __("Completed Quantity"),
- fieldname: "qty",
- default: frm.doc.for_quantity,
- },
- (data) => {
- frm.events.complete_job(frm, "Complete", data.qty);
- },
- __("Enter Value")
- );
- } else {
- frm.events.complete_job(frm, "Complete", 0.0);
- }
- }).addClass("btn-primary");
- }
- },
-
- start_job: function (frm, status, employee) {
- const args = {
- job_card_id: frm.doc.name,
- start_time: frappe.datetime.now_datetime(),
- employees: employee,
- status: status,
- };
- frm.events.make_time_log(frm, args);
- },
-
- complete_job: function (frm, status, completed_qty) {
- const args = {
- job_card_id: frm.doc.name,
- complete_time: frappe.datetime.now_datetime(),
- status: status,
- completed_qty: completed_qty,
- };
- frm.events.make_time_log(frm, args);
- },
-
make_time_log: function (frm, args) {
frm.events.update_sub_operation(frm, args);
@@ -392,7 +453,7 @@ frappe.ui.form.on("Job Card", {
function updateStopwatch(increment) {
var hours = Math.floor(increment / 3600);
var minutes = Math.floor((increment - hours * 3600) / 60);
- var seconds = increment - hours * 3600 - minutes * 60;
+ var seconds = flt(increment - hours * 3600 - minutes * 60, 2);
$(section)
.find(".hours")
@@ -415,7 +476,7 @@ frappe.ui.form.on("Job Card", {
frm.dashboard.refresh();
const timer = `
00
:
00
@@ -424,19 +485,32 @@ frappe.ui.form.on("Job Card", {
`;
var section = frm.toolbar.page.add_inner_message(timer);
+ let currentIncrement = frm.events.get_current_time(frm);
+ if (frm.doc.time_logs?.length && frm.doc.time_logs[cint(frm.doc.time_logs.length) - 1].to_time) {
+ updateStopwatch(currentIncrement);
+ } else if (frm.doc.status == "On Hold") {
+ updateStopwatch(currentIncrement);
+ } else {
+ initialiseTimer();
+ }
+ },
- let currentIncrement = frm.doc.current_time || 0;
- if (frm.doc.started_time || frm.doc.current_time) {
- if (frm.doc.status == "On Hold") {
- updateStopwatch(currentIncrement);
+ get_current_time(frm) {
+ let current_time = 0;
+
+ frm.doc.time_logs.forEach((d) => {
+ if (d.to_time) {
+ if (d.time_in_mins) {
+ current_time += flt(d.time_in_mins, 2) * 60;
+ } else {
+ current_time += get_seconds_diff(d.to_time, d.from_time);
+ }
} else {
- currentIncrement += moment(frappe.datetime.now_datetime()).diff(
- moment(frm.doc.started_time),
- "seconds"
- );
- initialiseTimer();
+ current_time += get_seconds_diff(frappe.datetime.now_datetime(), d.from_time);
}
- }
+ });
+
+ return current_time;
},
hide_timer: function (frm) {
@@ -492,6 +566,14 @@ frappe.ui.form.on("Job Card", {
refresh_field("total_completed_qty");
},
+
+ source_warehouse(frm) {
+ if (frm.doc.source_warehouse) {
+ frm.doc.items.forEach((d) => {
+ frappe.model.set_value(d.doctype, d.name, "source_warehouse", frm.doc.source_warehouse);
+ });
+ }
+ },
});
frappe.ui.form.on("Job Card Time Log", {
@@ -503,3 +585,7 @@ frappe.ui.form.on("Job Card Time Log", {
frm.set_value("started_time", "");
},
});
+
+function get_seconds_diff(d1, d2) {
+ return moment(d1).diff(d2, "seconds");
+}
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json
index 531c71f9c634..9a0d1c69a1b7 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.json
+++ b/erpnext/manufacturing/doctype/job_card/job_card.json
@@ -8,43 +8,57 @@
"field_order": [
"naming_series",
"work_order",
- "bom_no",
- "production_item",
"employee",
+ "is_subcontracted",
"column_break_4",
"posting_date",
"company",
- "for_quantity",
+ "project",
+ "bom_no",
+ "semi_finished_good__finished_good_section",
+ "finished_good",
+ "production_item",
+ "semi_fg_bom",
"total_completed_qty",
+ "column_break_mcnb",
+ "for_quantity",
+ "transferred_qty",
+ "manufactured_qty",
"process_loss_qty",
+ "production_section",
+ "operation",
+ "source_warehouse",
+ "wip_warehouse",
+ "skip_material_transfer",
+ "backflush_from_wip_warehouse",
+ "column_break_12",
+ "workstation_type",
+ "workstation",
+ "target_warehouse",
+ "section_break_8",
+ "items",
+ "quality_inspection_section",
+ "quality_inspection_template",
+ "column_break_fcmp",
+ "quality_inspection",
+ "scheduled_time_tab",
"scheduled_time_section",
"expected_start_date",
"time_required",
"column_break_jkir",
"expected_end_date",
- "section_break_05am",
+ "section_break_rzeo",
"scheduled_time_logs",
"timing_detail",
- "time_logs",
"section_break_13",
"actual_start_date",
"total_time_in_mins",
"column_break_15",
"actual_end_date",
- "production_section",
- "operation",
- "wip_warehouse",
- "column_break_12",
- "workstation_type",
- "workstation",
- "quality_inspection_section",
- "quality_inspection_template",
- "column_break_fcmp",
- "quality_inspection",
+ "section_break_jbas",
+ "time_logs",
"section_break_21",
"sub_operations",
- "section_break_8",
- "items",
"scrap_items_section",
"scrap_items",
"corrective_operation_section",
@@ -54,11 +68,11 @@
"hour_rate",
"for_operation",
"more_information",
- "project",
"item_name",
- "transferred_qty",
"requested_qty",
"status",
+ "operation_row_id",
+ "is_paused",
"column_break_20",
"operation_row_number",
"operation_id",
@@ -68,7 +82,6 @@
"batch_no",
"serial_no",
"barcode",
- "job_started",
"started_time",
"current_time",
"amended_from",
@@ -86,10 +99,11 @@
"search_index": 1
},
{
+ "depends_on": "eval:!doc.finished_good",
"fetch_from": "work_order.bom_no",
"fieldname": "bom_no",
"fieldtype": "Link",
- "label": "BOM No",
+ "label": "Final BOM",
"options": "BOM",
"read_only": 1
},
@@ -105,6 +119,7 @@
"fieldname": "operation",
"fieldtype": "Link",
"in_list_view": 1,
+ "in_preview": 1,
"label": "Operation",
"options": "Operation",
"reqd": 1
@@ -130,22 +145,24 @@
"fieldname": "for_quantity",
"fieldtype": "Float",
"in_list_view": 1,
+ "in_preview": 1,
"label": "Qty To Manufacture"
},
{
"fieldname": "wip_warehouse",
"fieldtype": "Link",
"label": "WIP Warehouse",
- "options": "Warehouse",
- "reqd": 1
+ "mandatory_depends_on": "eval:!doc.finished_good || doc.skip_material_transfer === 0 || (doc.skip_material_transfer && doc.backflush_from_wip_warehouse)",
+ "options": "Warehouse"
},
{
"fieldname": "timing_detail",
- "fieldtype": "Section Break",
+ "fieldtype": "Tab Break",
"label": "Actual Time"
},
{
"allow_bulk_edit": 1,
+ "allow_on_submit": 1,
"fieldname": "time_logs",
"fieldtype": "Table",
"label": "Time Logs",
@@ -157,9 +174,12 @@
"hide_border": 1
},
{
+ "allow_on_submit": 1,
"default": "0",
+ "depends_on": "eval:doc.is_subcontracted===0",
"fieldname": "total_completed_qty",
"fieldtype": "Float",
+ "in_preview": 1,
"label": "Total Completed Qty",
"read_only": 1
},
@@ -175,7 +195,7 @@
},
{
"fieldname": "section_break_8",
- "fieldtype": "Tab Break",
+ "fieldtype": "Section Break",
"label": "Raw Materials"
},
{
@@ -199,9 +219,10 @@
},
{
"default": "0",
+ "depends_on": "items",
"fieldname": "transferred_qty",
"fieldtype": "Float",
- "label": "FG Qty from Transferred Raw Materials",
+ "label": "Transferred Raw Materials",
"read_only": 1
},
{
@@ -227,6 +248,7 @@
"fieldtype": "Column Break"
},
{
+ "allow_on_submit": 1,
"default": "Open",
"fieldname": "status",
"fieldtype": "Select",
@@ -236,16 +258,7 @@
"read_only": 1
},
{
- "default": "0",
- "fieldname": "job_started",
- "fieldtype": "Check",
- "hidden": 1,
- "label": "Job Started",
- "no_copy": 1,
- "print_hide": 1,
- "read_only": 1
- },
- {
+ "allow_on_submit": 1,
"fieldname": "started_time",
"fieldtype": "Datetime",
"hidden": 1,
@@ -273,18 +286,19 @@
},
{
"fieldname": "production_section",
- "fieldtype": "Tab Break",
- "label": "Operation & Workstation"
+ "fieldtype": "Section Break",
+ "label": "Operation & Materials"
},
{
"fieldname": "column_break_12",
"fieldtype": "Column Break"
},
{
+ "depends_on": "eval:!doc.finished_good",
"fetch_from": "work_order.production_item",
"fieldname": "production_item",
"fieldtype": "Link",
- "label": "Production Item",
+ "label": "Final Product",
"options": "Item",
"read_only": 1
},
@@ -302,6 +316,7 @@
"label": "Item Name"
},
{
+ "allow_on_submit": 1,
"fieldname": "current_time",
"fieldtype": "Int",
"hidden": 1,
@@ -384,6 +399,7 @@
"options": "Operation"
},
{
+ "allow_on_submit": 1,
"fieldname": "employee",
"fieldtype": "Table MultiSelect",
"label": "Employee",
@@ -463,6 +479,7 @@
"show_dashboard": 1
},
{
+ "depends_on": "expected_start_date",
"fieldname": "scheduled_time_section",
"fieldtype": "Section Break",
"label": "Scheduled Time"
@@ -476,10 +493,6 @@
"fieldtype": "Float",
"label": "Expected Time Required (In Mins)"
},
- {
- "fieldname": "section_break_05am",
- "fieldtype": "Section Break"
- },
{
"fieldname": "scheduled_time_logs",
"fieldtype": "Table",
@@ -507,11 +520,101 @@
{
"fieldname": "column_break_fcmp",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "finished_good",
+ "fieldtype": "Link",
+ "in_preview": 1,
+ "label": "Finished Good",
+ "options": "Item",
+ "read_only": 1
+ },
+ {
+ "fieldname": "target_warehouse",
+ "fieldtype": "Link",
+ "label": "Target Warehouse",
+ "mandatory_depends_on": "eval:doc.finished_good",
+ "options": "Warehouse"
+ },
+ {
+ "fieldname": "operation_row_id",
+ "fieldtype": "Int",
+ "label": "Operation Row ID"
+ },
+ {
+ "fieldname": "source_warehouse",
+ "fieldtype": "Link",
+ "label": "Source Warehouse",
+ "options": "Warehouse"
+ },
+ {
+ "fieldname": "semi_finished_good__finished_good_section",
+ "fieldtype": "Section Break",
+ "label": "Semi Finished Good / Finished Good"
+ },
+ {
+ "fieldname": "semi_fg_bom",
+ "fieldtype": "Link",
+ "label": "Semi FG BOM",
+ "options": "BOM",
+ "read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "is_subcontracted",
+ "fieldtype": "Check",
+ "label": " Is Subcontracted",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_mcnb",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_rzeo",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "section_break_jbas",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "scheduled_time_tab",
+ "fieldtype": "Tab Break",
+ "label": "Scheduled Time"
+ },
+ {
+ "depends_on": "finished_good",
+ "fieldname": "manufactured_qty",
+ "fieldtype": "Float",
+ "label": "Manufactured Qty",
+ "read_only": 1
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.finished_good",
+ "fieldname": "skip_material_transfer",
+ "fieldtype": "Check",
+ "label": "Skip Material Transfer to WIP"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.finished_good && doc.skip_material_transfer === 1",
+ "fieldname": "backflush_from_wip_warehouse",
+ "fieldtype": "Check",
+ "label": "Backflush Materials From WIP Warehouse"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_paused",
+ "fieldtype": "Check",
+ "label": "Is Paused",
+ "read_only": 1
}
],
"is_submittable": 1,
"links": [],
- "modified": "2024-03-27 13:09:56.634418",
+ "modified": "2024-05-26 17:44:18.324743",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Job Card",
@@ -564,6 +667,7 @@
"write": 1
}
],
+ "show_preview_popup": 1,
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index c565c910c4e2..100ca45a9f09 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -9,7 +9,7 @@
from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder import Criterion
-from frappe.query_builder.functions import IfNull, Max, Min
+from frappe.query_builder.functions import IfNull, Max, Min, Sum
from frappe.utils import (
add_days,
add_to_date,
@@ -21,13 +21,15 @@
getdate,
time_diff,
time_diff_in_hours,
- time_diff_in_seconds,
)
from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import (
get_mins_between_operations,
)
from erpnext.manufacturing.doctype.workstation_type.workstation_type import get_workstations
+from erpnext.subcontracting.doctype.subcontracting_bom.subcontracting_bom import (
+ get_subcontracting_boms_for_finished_goods,
+)
class OverlapError(frappe.ValidationError):
@@ -64,14 +66,13 @@ class JobCard(Document):
from erpnext.manufacturing.doctype.job_card_scheduled_time.job_card_scheduled_time import (
JobCardScheduledTime,
)
- from erpnext.manufacturing.doctype.job_card_scrap_item.job_card_scrap_item import (
- JobCardScrapItem,
- )
+ from erpnext.manufacturing.doctype.job_card_scrap_item.job_card_scrap_item import JobCardScrapItem
from erpnext.manufacturing.doctype.job_card_time_log.job_card_time_log import JobCardTimeLog
actual_end_date: DF.Datetime | None
actual_start_date: DF.Datetime | None
amended_from: DF.Link | None
+ backflush_from_wip_warehouse: DF.Check
barcode: DF.Barcode | None
batch_no: DF.Link | None
bom_no: DF.Link | None
@@ -80,18 +81,22 @@ class JobCard(Document):
employee: DF.TableMultiSelect[JobCardTimeLog]
expected_end_date: DF.Datetime | None
expected_start_date: DF.Datetime | None
+ finished_good: DF.Link | None
for_job_card: DF.Link | None
for_operation: DF.Link | None
for_quantity: DF.Float
hour_rate: DF.Currency
is_corrective_job_card: DF.Check
+ is_paused: DF.Check
+ is_subcontracted: DF.Check
item_name: DF.ReadOnly | None
items: DF.Table[JobCardItem]
- job_started: DF.Check
+ manufactured_qty: DF.Float
naming_series: DF.Literal["PO-JOB.#####"]
operation: DF.Link
operation_id: DF.Data | None
- operation_row_number: DF.Literal
+ operation_row_id: DF.Int
+ operation_row_number: DF.Literal[None]
posting_date: DF.Date | None
process_loss_qty: DF.Float
production_item: DF.Link | None
@@ -102,9 +107,12 @@ class JobCard(Document):
requested_qty: DF.Float
scheduled_time_logs: DF.Table[JobCardScheduledTime]
scrap_items: DF.Table[JobCardScrapItem]
+ semi_fg_bom: DF.Link | None
sequence_id: DF.Int
serial_and_batch_bundle: DF.Link | None
serial_no: DF.SmallText | None
+ skip_material_transfer: DF.Check
+ source_warehouse: DF.Link | None
started_time: DF.Datetime | None
status: DF.Literal[
"Open",
@@ -116,12 +124,13 @@ class JobCard(Document):
"Completed",
]
sub_operations: DF.Table[JobCardOperation]
+ target_warehouse: DF.Link | None
time_logs: DF.Table[JobCardTimeLog]
time_required: DF.Float
total_completed_qty: DF.Float
total_time_in_mins: DF.Float
transferred_qty: DF.Float
- wip_warehouse: DF.Link
+ wip_warehouse: DF.Link | None
work_order: DF.Link
workstation: DF.Link
workstation_type: DF.Link | None
@@ -141,6 +150,7 @@ def before_validate(self):
def validate(self):
self.validate_time_logs()
+ self.validate_on_hold()
self.set_status()
self.validate_operation_id()
self.validate_sequence_id()
@@ -151,6 +161,31 @@ def validate(self):
def on_update(self):
self.validate_job_card_qty()
+ def validate_on_hold(self):
+ if self.is_paused and not self.time_logs:
+ self.is_paused = 0
+
+ def set_manufactured_qty(self):
+ table_name = "Stock Entry"
+ if self.is_subcontracted:
+ table_name = "Subcontracting Receipt Item"
+
+ table = frappe.qb.DocType(table_name)
+ query = frappe.qb.from_(table).where((table.job_card == self.name) & (table.docstatus == 1))
+
+ if self.is_subcontracted:
+ query = query.select(Sum(table.qty))
+ else:
+ query = query.select(Sum(table.fg_completed_qty))
+ query = query.where(table.purpose == "Manufacture")
+
+ qty = query.run()[0][0] or 0.0
+ self.manufactured_qty = flt(qty)
+ self.db_set("manufactured_qty", self.manufactured_qty)
+
+ self.update_semi_finished_good_details()
+ self.set_status(update_status=True)
+
def validate_job_card_qty(self):
if not (self.operation_id and self.work_order):
return
@@ -511,13 +546,14 @@ def add_time_log(self, args):
if self.time_logs and len(self.time_logs) > 0:
last_row = self.time_logs[-1]
- self.reset_timer_value(args)
if last_row and args.get("complete_time"):
for row in self.time_logs:
if not row.to_time:
- row.update(
+ to_time = get_datetime(args.get("complete_time"))
+ row.db_set(
{
- "to_time": get_datetime(args.get("complete_time")),
+ "to_time": to_time,
+ "time_in_mins": time_diff_in_minutes(to_time, row.from_time),
"operation": args.get("sub_operation"),
"completed_qty": (args.get("completed_qty") if last_row.idx == row.idx else 0.0),
}
@@ -538,35 +574,17 @@ def add_time_log(self, args):
else:
self.add_start_time_log(new_args)
- if not self.employee and employees:
- self.set_employees(employees)
-
- if self.status == "On Hold":
- self.current_time = time_diff_in_seconds(last_row.to_time, last_row.from_time)
-
- self.save()
-
def add_start_time_log(self, args):
- self.append("time_logs", args)
+ if args.from_time and args.to_time:
+ args.time_in_mins = time_diff_in_minutes(args.to_time, args.from_time)
+
+ row = self.append("time_logs", args)
+ row.db_update()
def set_employees(self, employees):
for name in employees:
self.append("employee", {"employee": name.get("employee"), "completed_qty": 0.0})
-
- def reset_timer_value(self, args):
- self.started_time = None
-
- if args.get("status") in ["Work In Progress", "Complete"]:
- self.current_time = 0.0
-
- if args.get("status") == "Work In Progress":
- self.started_time = get_datetime(args.get("start_time"))
-
- if args.get("status") == "Resume Job":
- args["status"] = "Work In Progress"
-
- if args.get("status"):
- self.status = args.get("status")
+ self.save()
def update_sub_operation_status(self):
if not (self.sub_operations and self.time_logs):
@@ -628,23 +646,25 @@ def get_required_items(self):
return
doc = frappe.get_doc("Work Order", self.get("work_order"))
- if doc.transfer_material_against == "Work Order" or doc.skip_transfer:
+ if not doc.track_semi_finished_goods and (
+ doc.transfer_material_against == "Work Order" or doc.skip_transfer
+ ):
return
for d in doc.required_items:
- if not d.operation:
+ if not d.operation and not d.operation_row_id:
frappe.throw(
_("Row {0} : Operation is required against the raw material item {1}").format(
d.idx, d.item_code
)
)
- if self.get("operation") == d.operation:
+ if self.get("operation") == d.operation or self.operation_row_id == d.operation_row_id:
self.append(
"items",
{
"item_code": d.item_code,
- "source_warehouse": d.source_warehouse,
+ "source_warehouse": self.source_warehouse or d.source_warehouse,
"uom": frappe.db.get_value("Item", d.item_code, "stock_uom"),
"item_name": d.item_name,
"description": d.description,
@@ -669,7 +689,7 @@ def on_cancel(self):
self.set_transferred_qty()
def validate_transfer_qty(self):
- if self.items and self.transferred_qty < self.for_quantity:
+ if not self.finished_good and self.items and self.transferred_qty < self.for_quantity:
frappe.throw(
_(
"Materials needs to be transferred to the work in progress warehouse for the job card {0}"
@@ -677,6 +697,9 @@ def validate_transfer_qty(self):
)
def validate_job_card(self):
+ if self.finished_good:
+ return
+
if self.work_order and frappe.get_cached_value("Work Order", self.work_order, "status") == "Stopped":
frappe.throw(
_("Transaction not allowed against stopped Work Order {0}").format(
@@ -745,6 +768,9 @@ def set_process_loss(self):
)
def update_work_order(self):
+ if self.finished_good:
+ return
+
if not self.work_order:
return
@@ -756,7 +782,6 @@ def update_work_order(self):
return
for_quantity, time_in_mins, process_loss_qty = 0, 0, 0
- _from_time_list, _to_time_list = [], []
data = self.get_current_operation_data()
if data and len(data) > 0:
@@ -773,6 +798,20 @@ def update_work_order(self):
self.validate_produced_quantity(for_quantity, process_loss_qty, wo)
self.update_work_order_data(for_quantity, process_loss_qty, time_in_mins, wo)
+ def update_semi_finished_good_details(self):
+ if self.operation_id:
+ frappe.db.set_value(
+ "Work Order Operation", self.operation_id, "completed_qty", self.manufactured_qty
+ )
+ if (
+ self.finished_good
+ and frappe.get_cached_value("Work Order", self.work_order, "production_item")
+ == self.finished_good
+ ):
+ _wo_doc = frappe.get_doc("Work Order", self.work_order)
+ _wo_doc.db_set("produced_qty", self.manufactured_qty)
+ _wo_doc.db_set("status", _wo_doc.get_status())
+
def update_corrective_in_work_order(self, wo):
wo.corrective_operation_cost = 0.0
for row in frappe.get_all(
@@ -913,64 +952,71 @@ def _validate_over_transfer(row, transferred_qty):
frappe.db.set_value("Job Card Item", row.job_card_item, "transferred_qty", flt(transferred_qty))
def set_transferred_qty(self, update_status=False):
- "Set total FG Qty in Job Card for which RM was transferred."
- if not self.items:
- self.transferred_qty = self.for_quantity if self.docstatus == 1 else 0
+ from frappe.query_builder.functions import Sum
- doc = frappe.get_doc("Work Order", self.get("work_order"))
- if doc.transfer_material_against == "Work Order" or doc.skip_transfer:
- return
+ stock_entry = frappe.qb.DocType("Stock Entry")
- if self.items:
- # sum of 'For Quantity' of Stock Entries against JC
- self.transferred_qty = (
- frappe.db.get_value(
- "Stock Entry",
- {
- "job_card": self.name,
- "work_order": self.work_order,
- "docstatus": 1,
- "purpose": "Material Transfer for Manufacture",
- },
- "sum(fg_completed_qty)",
- )
- or 0
+ query = (
+ frappe.qb.from_(stock_entry)
+ .select(Sum(stock_entry.fg_completed_qty))
+ .where(
+ (stock_entry.job_card == self.name)
+ & (stock_entry.docstatus == 1)
+ & (stock_entry.purpose == "Material Transfer for Manufacture")
)
+ .groupby(stock_entry.job_card)
+ )
- self.db_set("transferred_qty", self.transferred_qty)
-
+ query = query.run()
qty = 0
- if self.work_order:
- doc = frappe.get_doc("Work Order", self.work_order)
- if doc.transfer_material_against == "Job Card" and not doc.skip_transfer:
- completed = True
- for d in doc.operations:
- if d.status != "Completed":
- completed = False
- break
-
- if completed:
- job_cards = frappe.get_all(
- "Job Card",
- filters={"work_order": self.work_order, "docstatus": ("!=", 2)},
- fields="sum(transferred_qty) as qty",
- group_by="operation_id",
- )
- if job_cards:
- qty = min(d.qty for d in job_cards)
-
- doc.db_set("material_transferred_for_manufacturing", qty)
+ if query and query[0][0]:
+ qty = flt(query[0][0])
+ self.db_set("transferred_qty", qty)
self.set_status(update_status)
- def set_status(self, update_status=False):
- if self.status == "On Hold" and self.docstatus == 0:
- return
+ if self.work_order and not frappe.get_cached_value(
+ "Work Order", self.work_order, "track_semi_finished_goods"
+ ):
+ self.set_transferred_qty_in_work_order()
+
+ def set_transferred_qty_in_work_order(self):
+ doc = frappe.get_doc("Work Order", self.work_order)
+
+ qty = 0.0
+ if doc.transfer_material_against == "Job Card" and not doc.skip_transfer:
+ completed = True
+ for d in doc.operations:
+ if d.status != "Completed":
+ completed = False
+ break
+
+ if completed:
+ job_cards = frappe.get_all(
+ "Job Card",
+ filters={"work_order": self.work_order, "docstatus": ("!=", 2)},
+ fields="sum(transferred_qty) as qty",
+ group_by="operation_id",
+ )
+
+ if job_cards:
+ qty = min(d.qty for d in job_cards)
+
+ doc.db_set("material_transferred_for_manufacturing", qty)
+ def set_status(self, update_status=False):
self.status = {0: "Open", 1: "Submitted", 2: "Cancelled"}[self.docstatus or 0]
+ if self.finished_good and self.docstatus == 1:
+ if self.manufactured_qty >= self.for_quantity:
+ self.status = "Completed"
+ elif self.transferred_qty > 0 or self.skip_material_transfer:
+ self.status = "Work In Progress"
- if self.docstatus < 2:
+ if self.docstatus == 0 and self.time_logs:
+ self.status = "Work In Progress"
+
+ if not self.finished_good and self.docstatus < 2:
if flt(self.for_quantity) <= flt(self.transferred_qty):
self.status = "Material Transferred"
@@ -980,16 +1026,14 @@ def set_status(self, update_status=False):
if self.docstatus == 1 and (self.for_quantity <= self.total_completed_qty or not self.items):
self.status = "Completed"
+ if self.is_paused:
+ self.status = "On Hold"
+
if update_status:
self.db_set("status", self.status)
- if self.status in ["Completed", "Work In Progress"]:
- status = {
- "Completed": "Off",
- "Work In Progress": "Production",
- }.get(self.status)
-
- self.update_status_in_workstation(status)
+ if self.workstation:
+ self.update_workstation_status()
def set_wip_warehouse(self):
if not self.wip_warehouse:
@@ -1012,7 +1056,30 @@ def validate_operation_id(self):
OperationMismatchError,
)
+ @frappe.whitelist()
+ def pause_job(self, **kwargs):
+ if isinstance(kwargs, dict):
+ kwargs = frappe._dict(kwargs)
+
+ self.db_set("is_paused", 1)
+ self.add_time_logs(to_time=kwargs.end_time, completed_qty=0.0, employees=self.employee)
+
+ @frappe.whitelist()
+ def resume_job(self, **kwargs):
+ if isinstance(kwargs, dict):
+ kwargs = frappe._dict(kwargs)
+
+ self.db_set("is_paused", 0)
+ self.add_time_logs(
+ from_time=kwargs.start_time,
+ employees=self.employee,
+ completed_qty=0.0,
+ )
+
def validate_sequence_id(self):
+ if self.is_new():
+ return
+
if self.is_corrective_job_card:
return
@@ -1038,6 +1105,14 @@ def validate_sequence_id(self):
)
for row in data:
+ if not row.completed_qty:
+ frappe.throw(
+ _("{0}, complete the operation {1} before the operation {2}.").format(
+ message, bold(row.operation), bold(self.operation)
+ ),
+ OperationSequenceError,
+ )
+
if row.status != "Completed" and row.completed_qty < current_operation_qty:
frappe.throw(
_("{0}, complete the operation {1} before the operation {2}.").format(
@@ -1075,16 +1150,173 @@ def update_status_in_workstation(self, status):
frappe.db.set_value("Workstation", self.workstation, "status", status)
+ def add_time_logs(self, **kwargs):
+ row = None
+ kwargs = frappe._dict(kwargs)
+
+ update_status = False
+ for employee in kwargs.employees:
+ kwargs.employee = employee.get("employee")
+ if kwargs.from_time and not kwargs.to_time:
+ row = self.append("time_logs", kwargs)
+ row.db_update()
+ self.db_set("status", "Work In Progress")
+ else:
+ update_status = True
+ for row in self.time_logs:
+ if row.to_time or row.employee != kwargs.employee:
+ continue
+
+ row.to_time = kwargs.to_time
+ row.time_in_mins = time_diff_in_minutes(row.to_time, row.from_time)
+
+ if kwargs.employees[-1].get("employee") == row.employee:
+ row.completed_qty = kwargs.completed_qty
+
+ row.db_update()
+
+ self.set_status(update_status=update_status)
+
+ if not self.employee and kwargs.employees:
+ self.set_employees(kwargs.employees)
+
+ def update_workstation_status(self):
+ status_map = {
+ "Open": "Off",
+ "Work In Progress": "Production",
+ "Completed": "Off",
+ "On Hold": "Idle",
+ }
+
+ job_cards = frappe.get_all(
+ "Job Card",
+ fields=["name", "status"],
+ filters={"workstation": self.workstation, "docstatus": 0, "status": ("!=", "Completed")},
+ order_by="status desc",
+ )
+
+ if not job_cards:
+ frappe.db.set_value("Workstation", self.workstation, "status", "Off")
+
+ for row in job_cards:
+ frappe.db.set_value("Workstation", self.workstation, "status", status_map.get(row.status))
+ return
+
+ @frappe.whitelist()
+ def start_timer(self, **kwargs):
+ if isinstance(kwargs, dict):
+ kwargs = frappe._dict(kwargs)
+
+ if isinstance(kwargs.employees, str):
+ kwargs.employees = [{"employee": kwargs.employees}]
+
+ if kwargs.start_time:
+ self.add_time_logs(from_time=kwargs.start_time, employees=kwargs.employees)
+
+ @frappe.whitelist()
+ def complete_job_card(self, **kwargs):
+ if isinstance(kwargs, dict):
+ kwargs = frappe._dict(kwargs)
+
+ if kwargs.end_time:
+ self.add_time_logs(to_time=kwargs.end_time, completed_qty=kwargs.qty, employees=self.employee)
+ self.save()
+
+ if kwargs.auto_submit:
+ self.submit()
+ self.make_stock_entry_for_semi_fg_item(kwargs.auto_submit)
+ frappe.msgprint(
+ _("Job Card {0} has been completed").format(get_link_to_form("Job Card", self.name))
+ )
+
+ @frappe.whitelist()
+ def make_stock_entry_for_semi_fg_item(self, auto_submit=False):
+ from erpnext.stock.doctype.stock_entry_type.stock_entry_type import ManufactureEntry
+
+ ste = ManufactureEntry(
+ {
+ "for_quantity": self.for_quantity - self.manufactured_qty,
+ "job_card": self.name,
+ "skip_material_transfer": self.skip_material_transfer,
+ "backflush_from_wip_warehouse": self.backflush_from_wip_warehouse,
+ "work_order": self.work_order,
+ "purpose": "Manufacture",
+ "production_item": self.finished_good,
+ "company": self.company,
+ "wip_warehouse": self.wip_warehouse,
+ "fg_warehouse": self.target_warehouse,
+ "bom_no": self.semi_fg_bom,
+ "project": frappe.db.get_value("Work Order", self.work_order, "project"),
+ }
+ )
+
+ ste.make_stock_entry()
+ ste.stock_entry.flags.ignore_mandatory = True
+ ste.stock_entry.save()
+
+ if auto_submit:
+ ste.stock_entry.submit()
+
+ frappe.msgprint(
+ _("Stock Entry {0} has created").format(get_link_to_form("Stock Entry", ste.stock_entry.name))
+ )
+
+ return ste.stock_entry.as_dict()
+
+
+@frappe.whitelist()
+def make_subcontracting_po(source_name, target_doc=None):
+ def set_missing_values(source, target):
+ _item_details = get_subcontracting_boms_for_finished_goods(source.finished_good)
+
+ pending_qty = source.for_quantity - source.manufactured_qty
+ service_item_qty = flt(_item_details.service_item_qty) or 1.0
+ fg_item_qty = flt(_item_details.finished_good_qty) or 1.0
+
+ target.is_subcontracted = 1
+ target.supplier_warehouse = source.wip_warehouse
+ target.append(
+ "items",
+ {
+ "item_code": _item_details.service_item,
+ "fg_item": source.finished_good,
+ "uom": _item_details.service_item_uom,
+ "stock_uom": _item_details.service_item_uom,
+ "conversion_factor": _item_details.conversion_factor or 1,
+ "item_name": _item_details.service_item,
+ "qty": pending_qty * service_item_qty / fg_item_qty,
+ "fg_item_qty": pending_qty,
+ "job_card": source.name,
+ "bom": source.semi_fg_bom,
+ "warehouse": source.target_warehouse,
+ },
+ )
+
+ doclist = get_mapped_doc(
+ "Job Card",
+ source_name,
+ {
+ "Job Card": {
+ "doctype": "Purchase Order",
+ },
+ },
+ target_doc,
+ set_missing_values,
+ )
+
+ return doclist
+
@frappe.whitelist()
-def make_time_log(args):
- if isinstance(args, str):
- args = json.loads(args)
+def make_time_log(kwargs):
+ if isinstance(kwargs, str):
+ kwargs = json.loads(kwargs)
- args = frappe._dict(args)
- doc = frappe.get_doc("Job Card", args.job_card_id)
+ kwargs = frappe._dict(kwargs)
+ doc = frappe.get_doc("Job Card", kwargs.job_card_id)
doc.validate_sequence_id()
- doc.add_time_log(args)
+ doc.add_time_log(kwargs)
+ doc.set_status(update_status=True)
@frappe.whitelist()
@@ -1271,7 +1503,6 @@ def set_missing_values(source, target):
target.set("sub_operations", [])
target.set_sub_operations()
target.get_required_items()
- target.validate_time_logs()
doclist = get_mapped_doc(
"Job Card",
diff --git a/erpnext/manufacturing/doctype/job_card/job_card_dashboard.py b/erpnext/manufacturing/doctype/job_card/job_card_dashboard.py
index 14c1f36d0dcf..1718ea547955 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card_dashboard.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card_dashboard.py
@@ -7,6 +7,7 @@ def get_data():
"non_standard_fieldnames": {"Quality Inspection": "reference_name"},
"transactions": [
{"label": _("Transactions"), "items": ["Material Request", "Stock Entry"]},
+ {"label": _("Subcontracting"), "items": ["Purchase Order", "Subcontracting Order"]},
{"label": _("Reference"), "items": ["Quality Inspection"]},
],
}
diff --git a/erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json b/erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json
index 884e83e0f262..aed78636aa5e 100644
--- a/erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json
+++ b/erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json
@@ -15,12 +15,14 @@
],
"fields": [
{
+ "allow_on_submit": 1,
"fieldname": "from_time",
"fieldtype": "Datetime",
"in_list_view": 1,
"label": "From Time"
},
{
+ "allow_on_submit": 1,
"fieldname": "to_time",
"fieldtype": "Datetime",
"in_list_view": 1,
@@ -31,6 +33,7 @@
"fieldtype": "Column Break"
},
{
+ "allow_on_submit": 1,
"fieldname": "time_in_mins",
"fieldtype": "Float",
"in_list_view": 1,
@@ -38,6 +41,7 @@
"read_only": 1
},
{
+ "allow_on_submit": 1,
"default": "0",
"fieldname": "completed_qty",
"fieldtype": "Float",
@@ -63,7 +67,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2024-05-21 12:40:55.765860",
+ "modified": "2024-05-21 12:41:55.765860",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Job Card Time Log",
diff --git a/erpnext/manufacturing/doctype/operation/operation.json b/erpnext/manufacturing/doctype/operation/operation.json
index ba531164e08a..c712a9f140d1 100644
--- a/erpnext/manufacturing/doctype/operation/operation.json
+++ b/erpnext/manufacturing/doctype/operation/operation.json
@@ -40,6 +40,7 @@
{
"fieldname": "description",
"fieldtype": "Text",
+ "in_preview": 1,
"label": "Description"
},
{
@@ -104,7 +105,7 @@
"icon": "fa fa-wrench",
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2024-03-27 13:10:06.841479",
+ "modified": "2024-05-26 17:59:44.338741",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Operation",
@@ -134,6 +135,7 @@
}
],
"quick_entry": 1,
+ "show_preview_popup": 1,
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js
index 1da33f0ad9b1..8a806c179e9d 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.js
+++ b/erpnext/manufacturing/doctype/work_order/work_order.js
@@ -149,13 +149,7 @@ frappe.ui.form.on("Work Order", {
frm.doc.operations &&
frm.doc.operations.length
) {
- const not_completed = frm.doc.operations.filter((d) => {
- if (d.status != "Completed") {
- return true;
- }
- });
-
- if (not_completed && not_completed.length) {
+ if (frm.doc.__onload?.show_create_job_card_button) {
frm.add_custom_button(__("Create Job Card"), () => {
frm.trigger("make_job_card");
}).addClass("btn-primary");
@@ -277,6 +271,18 @@ frappe.ui.form.on("Work Order", {
label: __("Sequence Id"),
read_only: 1,
},
+ {
+ fieldtype: "Check",
+ fieldname: "skip_material_transfer",
+ label: __("Skip Material Transfer"),
+ read_only: 1,
+ },
+ {
+ fieldtype: "Check",
+ fieldname: "backflush_from_wip_warehouse",
+ label: __("Backflush Materials From WIP Warehouse"),
+ read_only: 1,
+ },
],
data: operations_data,
in_place_edit: true,
@@ -317,6 +323,8 @@ frappe.ui.form.on("Work Order", {
qty: pending_qty,
pending_qty: pending_qty,
sequence_id: data.sequence_id,
+ skip_material_transfer: data.skip_material_transfer,
+ backflush_from_wip_warehouse: data.backflush_from_wip_warehouse,
});
}
}
@@ -615,22 +623,25 @@ erpnext.work_order = {
);
}
- const show_start_btn =
- frm.doc.skip_transfer || frm.doc.transfer_material_against == "Job Card" ? 0 : 1;
+ if (!frm.doc.track_semi_finished_goods) {
+ const show_start_btn =
+ frm.doc.skip_transfer || frm.doc.transfer_material_against == "Job Card" ? 0 : 1;
- if (show_start_btn) {
- let pending_to_transfer = frm.doc.required_items.some(
- (item) => flt(item.transferred_qty) < flt(item.required_qty)
- );
- if (pending_to_transfer && frm.doc.status != "Stopped") {
- frm.has_start_btn = true;
- frm.add_custom_button(__("Create Pick List"), function () {
- erpnext.work_order.create_pick_list(frm);
- });
- var start_btn = frm.add_custom_button(__("Start"), function () {
- erpnext.work_order.make_se(frm, "Material Transfer for Manufacture");
- });
- start_btn.addClass("btn-primary");
+ if (show_start_btn) {
+ let pending_to_transfer = frm.doc.required_items.some(
+ (item) => flt(item.transferred_qty) < flt(item.required_qty)
+ );
+ if (pending_to_transfer && frm.doc.status != "Stopped") {
+ frm.has_start_btn = true;
+ frm.add_custom_button(__("Create Pick List"), function () {
+ erpnext.work_order.create_pick_list(frm);
+ });
+
+ var start_btn = frm.add_custom_button(__("Start"), function () {
+ erpnext.work_order.make_se(frm, "Material Transfer for Manufacture");
+ });
+ start_btn.addClass("btn-primary");
+ }
}
}
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json
index 36b992d0de5e..e564af27d95d 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.json
+++ b/erpnext/manufacturing/doctype/work_order/work_order.json
@@ -22,6 +22,16 @@
"produced_qty",
"process_loss_qty",
"project",
+ "track_semi_finished_goods",
+ "warehouses",
+ "source_warehouse",
+ "wip_warehouse",
+ "column_break_12",
+ "fg_warehouse",
+ "scrap_warehouse",
+ "operations_section",
+ "transfer_material_against",
+ "operations",
"section_break_ndpq",
"required_items",
"work_order_configuration",
@@ -32,22 +42,11 @@
"skip_transfer",
"from_wip_warehouse",
"update_consumed_material_cost_in_project",
- "warehouses",
- "source_warehouse",
- "wip_warehouse",
- "column_break_12",
- "fg_warehouse",
- "scrap_warehouse",
"serial_no_and_batch_for_finished_good_section",
"has_serial_no",
"has_batch_no",
"column_break_18",
"batch_size",
- "required_items_section",
- "materials_and_operations_tab",
- "operations_section",
- "transfer_material_against",
- "operations",
"time",
"planned_start_date",
"planned_end_date",
@@ -196,7 +195,7 @@
},
{
"default": "0",
- "depends_on": "eval:doc.docstatus==1 && doc.skip_transfer==0",
+ "depends_on": "eval:doc.docstatus==1 && doc.skip_transfer==0 && doc.track_semi_finished_goods === 0",
"fieldname": "material_transferred_for_manufacturing",
"fieldtype": "Float",
"label": "Material Transferred for Manufacturing",
@@ -248,7 +247,7 @@
"fieldname": "wip_warehouse",
"fieldtype": "Link",
"label": "Work-in-Progress Warehouse",
- "mandatory_depends_on": "eval:!doc.skip_transfer || doc.from_wip_warehouse",
+ "mandatory_depends_on": "eval:(!doc.skip_transfer || doc.from_wip_warehouse) && !doc.track_semi_finished_goods",
"options": "Warehouse"
},
{
@@ -256,8 +255,7 @@
"fieldname": "fg_warehouse",
"fieldtype": "Link",
"label": "Target Warehouse",
- "options": "Warehouse",
- "reqd": 1
+ "options": "Warehouse"
},
{
"fieldname": "column_break_12",
@@ -270,15 +268,9 @@
"label": "Scrap Warehouse",
"options": "Warehouse"
},
- {
- "fieldname": "required_items_section",
- "fieldtype": "Section Break",
- "label": "Required Items"
- },
{
"fieldname": "required_items",
"fieldtype": "Table",
- "label": "Required Items",
"no_copy": 1,
"options": "Work Order Item",
"print_hide": 1
@@ -336,7 +328,7 @@
"options": "fa fa-wrench"
},
{
- "depends_on": "operations",
+ "depends_on": "eval: doc.operations?.length && doc.track_semi_finished_goods === 0",
"fetch_from": "bom_no.transfer_material_against",
"fetch_if_empty": 1,
"fieldname": "transfer_material_against",
@@ -579,13 +571,19 @@
"label": "Configuration"
},
{
- "fieldname": "materials_and_operations_tab",
- "fieldtype": "Tab Break",
- "label": "Operations"
+ "collapsible": 1,
+ "collapsible_depends_on": "eval:!doc.operations?.length",
+ "fieldname": "section_break_ndpq",
+ "fieldtype": "Section Break",
+ "label": "Required Items"
},
{
- "fieldname": "section_break_ndpq",
- "fieldtype": "Section Break"
+ "default": "0",
+ "fetch_from": "bom_no.track_semi_finished_goods",
+ "fieldname": "track_semi_finished_goods",
+ "fieldtype": "Check",
+ "label": "Track Semi Finished Goods",
+ "read_only": 1
}
],
"icon": "fa fa-cogs",
@@ -593,7 +591,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
- "modified": "2024-03-27 13:11:00.129434",
+ "modified": "2024-03-27 13:13:00.129434",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Work Order",
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index b5c6cd9330f6..b4d1e9d26935 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -143,6 +143,24 @@ def onload(self):
self.set_onload("material_consumption", ms.material_consumption)
self.set_onload("backflush_raw_materials_based_on", ms.backflush_raw_materials_based_on)
self.set_onload("overproduction_percentage", ms.overproduction_percentage_for_work_order)
+ self.set_onload("show_create_job_card_button", self.show_create_job_card_button())
+
+ def show_create_job_card_button(self):
+ operation_details = frappe._dict(
+ frappe.get_all(
+ "Job Card",
+ fields=["operation", "for_quantity"],
+ filters={"docstatus": ("<", 2), "work_order": self.name},
+ as_list=1,
+ )
+ )
+
+ for d in self.operations:
+ job_card_qty = self.qty - flt(operation_details.get(d.operation))
+ if job_card_qty > 0:
+ return True
+
+ return False
def validate(self):
self.validate_production_item()
@@ -422,15 +440,20 @@ def update_production_plan_status(self):
self.update_status()
production_plan.run_method("update_produced_pending_qty", produced_qty, self.production_plan_item)
- def before_submit(self):
- self.create_serial_no_batch_no()
+ def validate_warehouse(self):
+ if self.track_semi_finished_goods:
+ return
- def on_submit(self):
if not self.wip_warehouse and not self.skip_transfer:
frappe.throw(_("Work-in-Progress Warehouse is required before Submit"))
if not self.fg_warehouse:
- frappe.throw(_("For Warehouse is required before Submit"))
+ frappe.throw(_("Target Warehouse is required before Submit"))
+
+ def before_submit(self):
+ self.create_serial_no_batch_no()
+ def on_submit(self):
+ self.validate_warehouse()
if self.production_plan and frappe.db.exists(
"Production Plan Item Reference", {"parent": self.production_plan}
):
@@ -667,6 +690,9 @@ def validate_cancel(self):
)
def update_planned_qty(self):
+ if self.track_semi_finished_goods:
+ return
+
from erpnext.manufacturing.doctype.production_plan.production_plan import (
get_reserved_qty_for_sub_assembly,
)
@@ -811,13 +837,21 @@ def _get_operations(bom_no, qty=1):
"description",
"workstation",
"idx",
+ "finished_good",
+ "is_subcontracted",
+ "wip_warehouse",
+ "source_warehouse",
+ "fg_warehouse",
"workstation_type",
"base_hour_rate as hour_rate",
"time_in_mins",
"parent as bom",
+ "bom_no",
"batch_size",
"sequence_id",
"fixed_time",
+ "skip_material_transfer",
+ "backflush_from_wip_warehouse",
],
order_by="idx",
)
@@ -827,6 +861,9 @@ def _get_operations(bom_no, qty=1):
d.time_in_mins = flt(d.time_in_mins) * flt(qty)
d.status = "Pending"
+ if self.track_semi_finished_goods and not d.sequence_id:
+ d.sequence_id = d.idx
+
return data
self.set("operations", [])
@@ -1084,6 +1121,7 @@ def set_required_items(self, reset_only_qty=False):
"required_qty": item.qty,
"source_warehouse": item.source_warehouse or item.default_warehouse,
"include_item_in_manufacturing": item.include_item_in_manufacturing,
+ "operation_row_id": item.operation_row_id,
},
)
@@ -1284,6 +1322,7 @@ def make_work_order(bom_no, item, qty=0, project=None, variant_items=None):
item_details = get_item_details(item, project)
wo_doc = frappe.new_doc("Work Order")
+ wo_doc.track_semi_finished_goods = frappe.db.get_value("BOM", bom_no, "track_semi_finished_goods")
wo_doc.production_item = item
wo_doc.update(item_details)
wo_doc.bom_no = bom_no
@@ -1450,6 +1489,8 @@ def make_job_card(work_order, operations):
work_order = frappe.get_doc("Work Order", work_order)
for row in operations:
row = frappe._dict(row)
+ row.update(get_operation_details(row.name, work_order))
+
validate_operation_data(row)
qty = row.get("qty")
while qty > 0:
@@ -1458,6 +1499,21 @@ def make_job_card(work_order, operations):
create_job_card(work_order, row, auto_create=True)
+def get_operation_details(name, work_order):
+ for row in work_order.operations:
+ if row.name == name:
+ return {
+ "workstation": row.workstation,
+ "workstation_type": row.workstation_type,
+ "source_warehouse": row.source_warehouse,
+ "fg_warehouse": row.fg_warehouse,
+ "wip_warehouse": row.wip_warehouse,
+ "finished_good": row.finished_good,
+ "bom_no": row.get("bom_no"),
+ "is_subcontracted": row.get("is_subcontracted"),
+ }
+
+
@frappe.whitelist()
def close_work_order(work_order, status):
if not frappe.has_permission("Work Order", "write"):
@@ -1558,6 +1614,7 @@ def create_job_card(work_order, row, enable_capacity_planning=False, auto_create
"workstation_type": row.get("workstation_type"),
"operation": row.get("operation"),
"workstation": row.get("workstation"),
+ "operation_row_id": cint(row.idx),
"posting_date": nowdate(),
"for_quantity": row.job_card_qty or work_order.get("qty", 0),
"operation_id": row.get("name"),
@@ -1565,13 +1622,22 @@ def create_job_card(work_order, row, enable_capacity_planning=False, auto_create
"project": work_order.project,
"company": work_order.company,
"sequence_id": row.get("sequence_id"),
- "wip_warehouse": work_order.wip_warehouse,
"hour_rate": row.get("hour_rate"),
"serial_no": row.get("serial_no"),
+ "source_warehouse": row.get("source_warehouse"),
+ "target_warehouse": row.get("fg_warehouse"),
+ "wip_warehouse": work_order.wip_warehouse or row.get("wip_warehouse"),
+ "skip_material_transfer": row.get("skip_material_transfer"),
+ "backflush_from_wip_warehouse": row.get("backflush_from_wip_warehouse"),
+ "finished_good": row.get("finished_good"),
+ "semi_fg_bom": row.get("bom_no"),
+ "is_subcontracted": row.get("is_subcontracted"),
}
)
- if work_order.transfer_material_against == "Job Card" and not work_order.skip_transfer:
+ if work_order.track_semi_finished_goods or (
+ work_order.transfer_material_against == "Job Card" and not work_order.skip_transfer
+ ):
doc.get_required_items()
if auto_create:
diff --git a/erpnext/manufacturing/doctype/work_order_item/work_order_item.json b/erpnext/manufacturing/doctype/work_order_item/work_order_item.json
index 0d3500a96c18..abb73f0ccc80 100644
--- a/erpnext/manufacturing/doctype/work_order_item/work_order_item.json
+++ b/erpnext/manufacturing/doctype/work_order_item/work_order_item.json
@@ -8,6 +8,7 @@
"operation",
"item_code",
"source_warehouse",
+ "operation_row_id",
"column_break_3",
"item_name",
"description",
@@ -138,11 +139,17 @@
"in_list_view": 1,
"label": "Returned Qty ",
"read_only": 1
+ },
+ {
+ "fieldname": "operation_row_id",
+ "fieldtype": "Int",
+ "label": "Operation Row Id",
+ "read_only": 1
}
],
"istable": 1,
"links": [],
- "modified": "2024-03-27 13:11:00.429838",
+ "modified": "2024-03-27 13:12:00.429838",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Work Order Item",
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 83c27e8c2f71..9146122a858f 100644
--- a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json
+++ b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json
@@ -15,6 +15,16 @@
"workstation_type",
"workstation",
"sequence_id",
+ "section_break_insy",
+ "bom_no",
+ "finished_good",
+ "is_subcontracted",
+ "skip_material_transfer",
+ "backflush_from_wip_warehouse",
+ "column_break_vjih",
+ "source_warehouse",
+ "wip_warehouse",
+ "fg_warehouse",
"section_break_10",
"description",
"estimated_time_and_cost",
@@ -52,7 +62,6 @@
"columns": 2,
"fieldname": "bom",
"fieldtype": "Link",
- "in_list_view": 1,
"label": "BOM",
"no_copy": 1,
"options": "BOM",
@@ -66,11 +75,10 @@
"oldfieldtype": "Text"
},
{
- "columns": 2,
+ "columns": 1,
"description": "Operation completed for how many finished goods?",
"fieldname": "completed_qty",
"fieldtype": "Float",
- "in_list_view": 1,
"label": "Completed Qty",
"no_copy": 1
},
@@ -213,16 +221,83 @@
"columns": 2,
"fieldname": "process_loss_qty",
"fieldtype": "Float",
- "in_list_view": 1,
"label": "Process Loss Qty",
"no_copy": 1,
"read_only": 1
+ },
+ {
+ "depends_on": "eval:parent.track_semi_finished_goods === 1",
+ "fieldname": "section_break_insy",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "bom_no",
+ "fieldtype": "Link",
+ "label": "BOM No (For Semi-FG)",
+ "options": "BOM",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_vjih",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "finished_good",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Semi FG / FG",
+ "options": "Item",
+ "read_only": 1
+ },
+ {
+ "columns": 1,
+ "fieldname": "wip_warehouse",
+ "fieldtype": "Link",
+ "label": "WIP WH",
+ "options": "Warehouse"
+ },
+ {
+ "columns": 2,
+ "fieldname": "fg_warehouse",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "FG Warehouse",
+ "options": "Warehouse"
+ },
+ {
+ "columns": 2,
+ "fieldname": "source_warehouse",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Source Warehouse",
+ "options": "Warehouse"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_subcontracted",
+ "fieldtype": "Check",
+ "label": "Is Subcontracted",
+ "read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "skip_material_transfer",
+ "fieldtype": "Check",
+ "label": "Skip Material Transfer",
+ "read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "backflush_from_wip_warehouse",
+ "fieldtype": "Check",
+ "label": "Backflush Materials From WIP Warehouse",
+ "read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2024-03-27 13:11:00.595376",
+ "modified": "2024-05-26 15:57:17.958543",
"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 5bd3ab1b21f7..fb8b3feb4dd7 100644
--- a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.py
+++ b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.py
@@ -18,11 +18,16 @@ class WorkOrderOperation(Document):
actual_operating_cost: DF.Currency
actual_operation_time: DF.Float
actual_start_time: DF.Datetime | None
+ backflush_from_wip_warehouse: DF.Check
batch_size: DF.Float
bom: DF.Link | None
+ bom_no: DF.Link | None
completed_qty: DF.Float
description: DF.TextEditor | None
+ fg_warehouse: DF.Link | None
+ finished_good: DF.Link | None
hour_rate: DF.Float
+ is_subcontracted: DF.Check
operation: DF.Link
parent: DF.Data
parentfield: DF.Data
@@ -32,8 +37,11 @@ class WorkOrderOperation(Document):
planned_start_time: DF.Datetime | None
process_loss_qty: DF.Float
sequence_id: DF.Int
+ skip_material_transfer: DF.Check
+ source_warehouse: DF.Link | None
status: DF.Literal["Pending", "Work in Progress", "Completed"]
time_in_mins: DF.Float
+ wip_warehouse: DF.Link | None
workstation: DF.Link | None
workstation_type: DF.Link | None
# end: auto-generated types
diff --git a/erpnext/manufacturing/doctype/workstation/workstation.js b/erpnext/manufacturing/doctype/workstation/workstation.js
index c3bf9ef5c8cd..dbcb0113c8fe 100644
--- a/erpnext/manufacturing/doctype/workstation/workstation.js
+++ b/erpnext/manufacturing/doctype/workstation/workstation.js
@@ -105,16 +105,118 @@ class WorkstationDashboard {
}
render_job_cards() {
- let template = frappe.render_template("workstation_job_card", {
+ this.template = frappe.render_template("workstation_job_card", {
data: this.job_cards,
});
- this.$wrapper.html(template);
+ this.timer_job_cards = {};
+ this.$wrapper.html(this.template);
+ this.setup_qrcode_fields();
this.prepare_timer();
+ this.setup_menu_actions();
this.toggle_job_card();
this.bind_events();
}
+ setup_qrcode_fields() {
+ this.start_job_qrcode = frappe.ui.form.make_control({
+ df: {
+ label: __("Start Job"),
+ fieldtype: "Data",
+ options: "Barcode",
+ placeholder: __("Scan Job Card Qrcode"),
+ },
+ parent: this.$wrapper.find(".qrcode-fields"),
+ render_input: true,
+ });
+
+ this.start_job_qrcode.$wrapper.addClass("form-column col-sm-6");
+
+ this.start_job_qrcode.$input.on("input", (e) => {
+ clearTimeout(this.start_job_qrcode_search);
+ this.start_job_qrcode_search = setTimeout(() => {
+ let job_card = this.start_job_qrcode.get_value();
+ if (job_card) {
+ this.validate_job_card(job_card, "Open", (job_card, qty) => {
+ this.start_job(job_card);
+ });
+
+ this.start_job_qrcode.set_value("");
+ }
+ }, 300);
+ });
+
+ this.complete_job_qrcode = frappe.ui.form.make_control({
+ df: {
+ label: __("Complete Job"),
+ fieldtype: "Data",
+ options: "Barcode",
+ placeholder: __("Scan Job Card Qrcode"),
+ },
+ parent: this.$wrapper.find(".qrcode-fields"),
+ render_input: true,
+ });
+
+ this.complete_job_qrcode.$input.on("input", (e) => {
+ clearTimeout(this.complete_job_qrcode_search);
+ this.complete_job_qrcode_search = setTimeout(() => {
+ let job_card = this.complete_job_qrcode.get_value();
+ if (job_card) {
+ this.validate_job_card(job_card, "Work In Progress", (job_card, qty) => {
+ this.complete_job(job_card, qty);
+ });
+
+ this.complete_job_qrcode.set_value("");
+ }
+ }, 300);
+ });
+
+ this.complete_job_qrcode.$wrapper.addClass("form-column col-sm-6");
+ }
+
+ validate_job_card(job_card, status, callback) {
+ frappe.call({
+ method: "erpnext.manufacturing.doctype.workstation.workstation.validate_job_card",
+ args: {
+ job_card: job_card,
+ status: status,
+ },
+ callback(r) {
+ callback(job_card, r.message);
+ },
+ });
+ }
+
+ setup_menu_actions() {
+ let me = this;
+ this.job_cards.forEach((data) => {
+ me.menu_actions = me.$wrapper.find(`.menu-actions[data-job-card='${data.name}']`);
+ $(me.menu_actions).find(".btn-start").hide();
+ $(me.menu_actions).find(".btn-resume").hide();
+ $(me.menu_actions).find(".btn-pause").hide();
+ $(me.menu_actions).find(".btn-complete").hide();
+
+ if (
+ data.for_quantity + data.process_loss_qty > data.total_completed_qty &&
+ (data.skip_material_transfer ||
+ data.transferred_qty >= data.for_quantity + data.process_loss_qty ||
+ !data.finished_good)
+ ) {
+ if (!data.time_logs?.length) {
+ $(me.menu_actions).find(".btn-start").show();
+ } else if (data.is_paused) {
+ $(me.menu_actions).find(".btn-resume").show();
+ } else if (data.for_quantity - data.manufactured_qty > 0) {
+ if (!data.is_paused) {
+ $(me.menu_actions).find(".btn-pause").show();
+ }
+
+ $(me.menu_actions).find(".btn-complete").show();
+ }
+ }
+ });
+ }
+
toggle_job_card() {
this.$wrapper.find(".collapse-indicator-job").on("click", (e) => {
$(e.currentTarget)
@@ -133,133 +235,295 @@ class WorkstationDashboard {
}
bind_events() {
- this.$wrapper.find(".make-material-request").on("click", (e) => {
- let job_card = $(e.currentTarget).attr("job-card");
+ let me = this;
+
+ this.$wrapper.find(".btn-transfer-materials").on("click", (e) => {
+ let job_card = $(e.currentTarget).closest("ul").attr("data-job-card");
this.make_material_request(job_card);
});
this.$wrapper.find(".btn-start").on("click", (e) => {
- let job_card = $(e.currentTarget).attr("job-card");
+ let job_card = $(e.currentTarget).closest("ul").attr("data-job-card");
this.start_job(job_card);
});
+ this.$wrapper.find(".btn-pause").on("click", (e) => {
+ let job_card = $(e.currentTarget).closest("ul").attr("data-job-card");
+ me.update_job_card(job_card, "pause_job", {
+ end_time: frappe.datetime.now_datetime(),
+ });
+ });
+
+ this.$wrapper.find(".btn-resume").on("click", (e) => {
+ let job_card = $(e.currentTarget).closest("ul").attr("data-job-card");
+ me.update_job_card(job_card, "resume_job", {
+ start_time: frappe.datetime.now_datetime(),
+ });
+ });
+
this.$wrapper.find(".btn-complete").on("click", (e) => {
- let job_card = $(e.currentTarget).attr("job-card");
- let pending_qty = flt($(e.currentTarget).attr("pending-qty"));
- this.complete_job(job_card, pending_qty);
+ let job_card = $(e.currentTarget).closest("ul").attr("data-job-card");
+ let for_quantity = $(e.currentTarget).attr("data-qty");
+ me.complete_job(job_card, for_quantity);
});
}
start_job(job_card) {
let me = this;
+
+ let fields = this.get_fields_for_employee();
+
+ this.employee_dialog = frappe.prompt(fields, (values) => {
+ me.update_job_card(job_card, "start_timer", values);
+ });
+
+ let default_employee = this.job_cards[0]?.user_employee;
+ if (default_employee) {
+ this.employee_dialog.fields_dict.employees.df.data.push({
+ employee: default_employee,
+ });
+ this.employee_dialog.fields_dict.employees.grid.refresh();
+ }
+ }
+
+ complete_job(job_card, for_quantity) {
frappe.prompt(
- [
- {
- fieldtype: "Datetime",
- label: __("Start Time"),
- fieldname: "start_time",
- reqd: 1,
- default: frappe.datetime.now_datetime(),
- },
- {
- label: __("Operator"),
- fieldname: "employee",
- fieldtype: "Link",
- options: "Employee",
- },
- ],
+ {
+ fieldname: "qty",
+ label: __("Completed Quantity"),
+ fieldtype: "Float",
+ reqd: 1,
+ default: flt(for_quantity || 0),
+ },
(data) => {
- this.frm.call({
- method: "start_job",
- doc: this.frm.doc,
- args: {
- job_card: job_card,
- from_time: data.start_time,
- employee: data.employee,
- },
- callback(r) {
- if (r.message) {
- me.job_cards = [r.message];
- me.prepare_timer();
- me.update_job_card_details();
- me.frm.reload_doc();
- }
- },
+ if (flt(data.qty) <= 0) {
+ frappe.throw(__("Quantity should be greater than 0"));
+ }
+
+ this.update_job_card(job_card, "complete_job_card", {
+ qty: flt(data.qty),
+ end_time: frappe.datetime.now_datetime(),
+ auto_submit: 1,
});
},
__("Enter Value"),
- __("Start Job")
+ __("Submit")
);
}
- complete_job(job_card, qty_to_manufacture) {
+ get_fields_for_employee() {
let me = this;
- let fields = [
+
+ return [
{
- fieldtype: "Float",
- label: __("Completed Quantity"),
- fieldname: "qty",
- reqd: 1,
- default: flt(qty_to_manufacture || 0),
+ label: __("Employee"),
+ fieldname: "employee",
+ fieldtype: "Link",
+ options: "Employee",
+ change() {
+ let employee = this.get_value();
+ let employees = me.employee_dialog.fields_dict.employees.df.data;
+
+ if (employee) {
+ let employee_exists = employees.find((d) => d.employee === employee);
+
+ if (!employee_exists) {
+ me.employee_dialog.fields_dict.employees.df.data.push({
+ employee: employee,
+ });
+
+ me.employee_dialog.fields_dict.employees.grid.refresh();
+ }
+ }
+ },
},
{
+ label: __("Start Time"),
+ fieldname: "start_time",
fieldtype: "Datetime",
- label: __("End Time"),
- fieldname: "end_time",
default: frappe.datetime.now_datetime(),
},
+ { fieldtype: "Section Break" },
+ {
+ label: __("Employees"),
+ fieldname: "employees",
+ fieldtype: "Table",
+ data: [],
+ cannot_add_rows: 1,
+ cannot_delete_rows: 1,
+ fields: [
+ {
+ label: __("Employee"),
+ fieldname: "employee",
+ fieldtype: "Link",
+ options: "Employee",
+ in_list_view: 1,
+ },
+ ],
+ },
];
+ }
- frappe.prompt(
- fields,
- (data) => {
- if (data.qty <= 0) {
- frappe.throw(__("Quantity should be greater than 0"));
- }
+ update_job_card(job_card, method, data) {
+ let me = this;
- this.frm.call({
- method: "complete_job",
- doc: this.frm.doc,
- args: {
- job_card: job_card,
- qty: data.qty,
- to_time: data.end_time,
- },
- callback: function (r) {
- if (r.message) {
- me.job_cards = [r.message];
- me.prepare_timer();
- me.update_job_card_details();
- me.frm.reload_doc();
- }
- },
+ frappe.call({
+ method: "erpnext.manufacturing.doctype.workstation.workstation.update_job_card",
+ args: {
+ job_card: job_card,
+ method: method,
+ start_time: data.start_time || "",
+ employees: data.employees || [],
+ end_time: data.end_time || "",
+ qty: data.qty || 0,
+ auto_submit: data.auto_submit || 0,
+ },
+ callback: () => {
+ $.each(me.timer_job_cards, (index, value) => {
+ clearInterval(value);
});
+
+ me.frm.reload_doc();
},
- __("Enter Value"),
- __("Submit")
- );
+ });
}
make_material_request(job_card) {
+ let me = this;
frappe.call({
- method: "erpnext.manufacturing.doctype.job_card.job_card.make_material_request",
+ method: "erpnext.manufacturing.doctype.workstation.workstation.get_raw_materials",
args: {
- source_name: job_card,
+ job_card: job_card,
},
callback: (r) => {
if (r.message) {
- var doc = frappe.model.sync(r.message)[0];
- frappe.set_route("Form", doc.doctype, doc.name);
+ me.prepare_materials_modal(r.message, job_card, (job_card) => {
+ frappe.call({
+ method: "erpnext.manufacturing.doctype.job_card.job_card.make_stock_entry",
+ args: {
+ source_name: job_card,
+ },
+ callback: (r) => {
+ var doc = frappe.model.sync(r.message);
+ frappe.set_route("Form", doc[0].doctype, doc[0].name);
+ },
+ });
+ });
}
},
});
}
+ prepare_materials_modal(raw_materials, job_card, callback) {
+ let fields = this.get_raw_material_fields(raw_materials);
+
+ this.materials_dialog = new frappe.ui.Dialog({
+ title: "Raw Materials",
+ fields: fields,
+ size: "large",
+ primary_action_label: __("Make Transfer Entry"),
+ primary_action: () => {
+ this.materials_dialog.hide();
+ callback(job_card);
+ },
+ });
+
+ raw_materials.forEach((row) => {
+ this.materials_dialog.fields_dict.items.df.data.push(row);
+ });
+
+ this.materials_dialog.fields_dict.items.grid.refresh();
+ this.materials_dialog.show();
+ }
+
+ get_raw_material_fields(raw_materials) {
+ return [
+ {
+ label: __("Warehouse"),
+ fieldname: "warehouse",
+ fieldtype: "Link",
+ options: "Warehouse",
+ read_only: 1,
+ default: raw_materials[0].warehouse,
+ },
+ { fieldtype: "Column Break" },
+ {
+ label: __("Skip Material Transfer"),
+ fieldname: "skip_material_transfer",
+ fieldtype: "Check",
+ read_only: 1,
+ default: raw_materials[0].skip_material_transfer,
+ },
+ { fieldtype: "Section Break" },
+ {
+ label: __("Raw Materials"),
+ fieldname: "items",
+ fieldtype: "Table",
+ cannot_add_rows: 1,
+ cannot_delete_rows: 1,
+ data: [],
+ size: "extra-large",
+ fields: [
+ {
+ label: __("Item Code"),
+ fieldname: "item_code",
+ fieldtype: "Link",
+ options: "Item",
+ in_list_view: 1,
+ read_only: 1,
+ columns: 2,
+ },
+ {
+ label: __("UOM"),
+ fieldname: "uom",
+ fieldtype: "Link",
+ options: "UOM",
+ in_list_view: 1,
+ read_only: 1,
+ columns: 1,
+ },
+ {
+ label: __("Reqired Qty"),
+ fieldname: "required_qty",
+ fieldtype: "Float",
+ in_list_view: 1,
+ read_only: 1,
+ columns: 2,
+ },
+ {
+ label: __("Transferred Qty"),
+ fieldname: "transferred_qty",
+ fieldtype: "Float",
+ in_list_view: 1,
+ read_only: 1,
+ columns: 2,
+ },
+ {
+ label: __("Available Qty"),
+ fieldname: "stock_qty",
+ fieldtype: "Float",
+ in_list_view: 1,
+ read_only: 1,
+ columns: 2,
+ },
+ {
+ label: __("Available"),
+ fieldname: "material_availability_status",
+ fieldtype: "Check",
+ in_list_view: 1,
+ read_only: 1,
+ columns: 1,
+ },
+ ],
+ },
+ ];
+ }
+
prepare_timer() {
this.job_cards.forEach((data) => {
if (data.time_logs?.length) {
data._current_time = this.get_current_time(data);
- if (data.time_logs[cint(data.time_logs.length) - 1].to_time) {
+ if (data.time_logs[cint(data.time_logs.length) - 1].to_time || data.is_paused) {
this.updateStopwatch(data);
} else {
this.initialiseTimer(data);
@@ -283,23 +547,23 @@ class WorkstationDashboard {
[data-name='${data.name}']`);
$(job_card_selector).find(".job-card-status").text(data.status);
- $(job_card_selector).find(".job-card-status").css("backgroundColor", color_map[data.status]);
- if (data.status === "Work In Progress") {
- $(job_card_selector).find(".btn-start").addClass("hide");
- $(job_card_selector).find(".btn-complete").removeClass("hide");
- } else if (data.status === "Completed") {
- $(job_card_selector).find(".btn-start").addClass("hide");
- $(job_card_selector).find(".btn-complete").addClass("hide");
- }
+ ["blue", "gray", "green", "orange", "yellow"].forEach((color) => {
+ $(job_card_selector).find(".job-card-status").removeClass(color);
+ });
+
+ $(job_card_selector).find(".job-card-status").addClass(data.status_color);
+ $(job_card_selector).find(".job-card-status").css("backgroundColor", color_map[data.status]);
});
}
initialiseTimer(data) {
- setInterval(() => {
+ let timeout = setInterval(() => {
data._current_time += 1;
this.updateStopwatch(data);
}, 1000);
+
+ this.timer_job_cards[data.name] = timeout;
}
updateStopwatch(data) {
diff --git a/erpnext/manufacturing/doctype/workstation/workstation.json b/erpnext/manufacturing/doctype/workstation/workstation.json
index 97d7216af81c..7df7cb727a04 100644
--- a/erpnext/manufacturing/doctype/workstation/workstation.json
+++ b/erpnext/manufacturing/doctype/workstation/workstation.json
@@ -9,6 +9,7 @@
"engine": "InnoDB",
"field_order": [
"dashboard_tab",
+ "section_break_mqqv",
"workstation_dashboard",
"details_tab",
"workstation_name",
@@ -246,13 +247,18 @@
"fieldname": "workstation_dashboard",
"fieldtype": "HTML",
"label": "Workstation Dashboard"
+ },
+ {
+ "fieldname": "section_break_mqqv",
+ "fieldtype": "Section Break",
+ "hide_border": 1
}
],
"icon": "icon-wrench",
"idx": 1,
"image_field": "on_status_image",
"links": [],
- "modified": "2024-03-27 13:11:00.760717",
+ "modified": "2024-06-01 14:48:47.341354",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Workstation",
diff --git a/erpnext/manufacturing/doctype/workstation/workstation.py b/erpnext/manufacturing/doctype/workstation/workstation.py
index 47cb74228be4..8d2e1e92e56c 100644
--- a/erpnext/manufacturing/doctype/workstation/workstation.py
+++ b/erpnext/manufacturing/doctype/workstation/workstation.py
@@ -55,7 +55,14 @@ class Workstation(Document):
hour_rate_electricity: DF.Currency
hour_rate_labour: DF.Currency
hour_rate_rent: DF.Currency
+ off_status_image: DF.AttachImage | None
+ on_status_image: DF.AttachImage | None
+ parts_per_hour: DF.Float
+ plant_floor: DF.Link | None
production_capacity: DF.Int
+ status: DF.Literal["Production", "Off", "Idle", "Problem", "Maintenance", "Setup"]
+ total_working_hours: DF.Float
+ warehouse: DF.Link | None
working_hours: DF.Table[WorkstationWorkingHour]
workstation_name: DF.Data
workstation_type: DF.Link | None
@@ -189,7 +196,7 @@ def complete_job(self, job_card, qty, to_time):
@frappe.whitelist()
-def get_job_cards(workstation):
+def get_job_cards(workstation, job_card=None):
if frappe.has_permission("Job Card", "read"):
jc_data = frappe.get_all(
"Job Card",
@@ -200,15 +207,22 @@ def get_job_cards(workstation):
"operation",
"total_completed_qty",
"for_quantity",
+ "process_loss_qty",
+ "finished_good",
"transferred_qty",
"status",
"expected_start_date",
"expected_end_date",
"time_required",
"wip_warehouse",
+ "skip_material_transfer",
+ "backflush_from_wip_warehouse",
+ "is_paused",
+ "manufactured_qty",
],
filters={
"workstation": workstation,
+ "is_subcontracted": 0,
"docstatus": ("<", 2),
"status": ["not in", ["Completed", "Stopped"]],
},
@@ -216,64 +230,98 @@ def get_job_cards(workstation):
)
job_cards = [row.name for row in jc_data]
- raw_materials = get_raw_materials(job_cards)
time_logs = get_time_logs(job_cards)
allow_excess_transfer = frappe.db.get_single_value(
"Manufacturing Settings", "job_card_excess_transfer"
)
+ user_employee = frappe.db.get_value("Employee", {"user_id": frappe.session.user}, "name")
+
for row in jc_data:
- row.progress_percent = (
- flt(row.total_completed_qty / row.for_quantity * 100, 2) if row.for_quantity else 0
- )
- row.progress_title = _("Total completed quantity: {0}").format(row.total_completed_qty)
+ item_code = row.finished_good or row.production_item
+ row.fg_uom = frappe.get_cached_value("Item", item_code, "stock_uom")
+
row.status_color = get_status_color(row.status)
- row.job_card_link = get_link_to_form("Job Card", row.name)
+ row.job_card_link = f"""
+ {row.name}
+ """
+
+ row.operation_link = f"""
+ {row.operation}
+ """
row.work_order_link = get_link_to_form("Work Order", row.work_order)
- row.raw_materials = raw_materials.get(row.name, [])
row.time_logs = time_logs.get(row.name, [])
row.make_material_request = False
if row.for_quantity > row.transferred_qty or allow_excess_transfer:
row.make_material_request = True
+ row.user_employee = user_employee
+
return jc_data
def get_status_color(status):
color_map = {
- "Pending": "var(--bg-blue)",
- "In Process": "var(--bg-yellow)",
- "Submitted": "var(--bg-blue)",
- "Open": "var(--bg-gray)",
- "Closed": "var(--bg-green)",
- "Work In Progress": "var(--bg-orange)",
+ "Pending": "blue",
+ "In Process": "yellow",
+ "Submitted": "blue",
+ "Open": "gray",
+ "Closed": "green",
+ "Work In Progress": "orange",
}
- return color_map.get(status, "var(--bg-blue)")
-
+ return color_map.get(status, "blue")
-def get_raw_materials(job_cards):
- raw_materials = {}
- data = frappe.get_all(
- "Job Card Item",
+@frappe.whitelist()
+def get_raw_materials(job_card):
+ raw_materials = frappe.get_all(
+ "Job Card",
fields=[
- "parent",
- "item_code",
- "item_group",
- "uom",
- "item_name",
- "source_warehouse",
- "required_qty",
- "transferred_qty",
+ "`tabJob Card`.`skip_material_transfer`",
+ "`tabJob Card`.`backflush_from_wip_warehouse`",
+ "`tabJob Card`.`wip_warehouse`",
+ "`tabJob Card Item`.`parent`",
+ "`tabJob Card Item`.`item_code`",
+ "`tabJob Card Item`.`item_group`",
+ "`tabJob Card Item`.`uom`",
+ "`tabJob Card Item`.`item_name`",
+ "`tabJob Card Item`.`source_warehouse`",
+ "`tabJob Card Item`.`required_qty`",
+ "`tabJob Card Item`.`transferred_qty`",
],
- filters={"parent": ["in", job_cards]},
+ filters={"name": job_card},
)
- for row in data:
- raw_materials.setdefault(row.parent, []).append(row)
+ if not raw_materials:
+ return []
+
+ for row in raw_materials:
+ warehouse = row.source_warehouse
+ if row.skip_material_transfer and row.backflush_from_wip_warehouse:
+ warehouse = row.wip_warehouse
+
+ row.stock_qty = (
+ frappe.db.get_value(
+ "Bin",
+ {
+ "item_code": row.item_code,
+ "warehouse": warehouse,
+ },
+ "actual_qty",
+ )
+ or 0.0
+ )
+
+ row.warehouse = warehouse
+
+ row.material_availability_status = 0
+ if row.skip_material_transfer and row.stock_qty >= row.required_qty:
+ row.material_availability_status = 1
+ elif row.transferred_qty >= row.required_qty:
+ row.material_availability_status = 1
return raw_materials
@@ -392,20 +440,57 @@ def get_workstations(**kwargs):
data = query.run(as_dict=True)
color_map = {
- "Production": "var(--green-600)",
- "Off": "var(--gray-600)",
- "Idle": "var(--gray-600)",
- "Problem": "var(--red-600)",
- "Maintenance": "var(--yellow-600)",
- "Setup": "var(--blue-600)",
+ "Production": "green",
+ "Off": "gray",
+ "Idle": "gray",
+ "Problem": "red",
+ "Maintenance": "yellow",
+ "Setup": "blue",
}
for d in data:
d.workstation_name = get_link_to_form("Workstation", d.name)
d.status_image = d.on_status_image
- d.background_color = color_map.get(d.status, "var(--red-600)")
+ d.color = color_map.get(d.status, "red")
d.workstation_link = get_url_to_form("Workstation", d.name)
if d.status != "Production":
d.status_image = d.off_status_image
return data
+
+
+@frappe.whitelist()
+def update_job_card(job_card, method, **kwargs):
+ if isinstance(kwargs, dict):
+ kwargs = frappe._dict(kwargs)
+
+ if kwargs.get("employees"):
+ kwargs.employees = frappe.parse_json(kwargs.employees)
+
+ if kwargs.qty and isinstance(kwargs.qty, str):
+ kwargs.qty = flt(kwargs.qty)
+
+ doc = frappe.get_doc("Job Card", job_card)
+ doc.run_method(method, **kwargs)
+
+
+@frappe.whitelist()
+def validate_job_card(job_card, status):
+ job_card_details = frappe.db.get_value("Job Card", job_card, ["status", "for_quantity"], as_dict=1)
+
+ current_status = job_card_details.status
+ if current_status != status:
+ if status == "Open":
+ frappe.throw(
+ _("The job card {0} is in {1} state and you cannot start it again.").format(
+ job_card, current_status
+ )
+ )
+ else:
+ frappe.throw(
+ _("The job card {0} is in {1} state and you cannot complete.").format(
+ job_card, current_status
+ )
+ )
+
+ return job_card_details.for_quantity
diff --git a/erpnext/manufacturing/doctype/workstation/workstation_job_card.html b/erpnext/manufacturing/doctype/workstation/workstation_job_card.html
index 97707855db0c..847963b70885 100644
--- a/erpnext/manufacturing/doctype/workstation/workstation_job_card.html
+++ b/erpnext/manufacturing/doctype/workstation/workstation_job_card.html
@@ -1,6 +1,6 @@
-
+
+
+
+
{% $.each(data, (idx, d) => { %}