From 33c4764784b7b6121b904a0ab625ba0e6c42e7a2 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 17 Jan 2024 18:26:23 +0530 Subject: [PATCH] feat: option to add raw materials manually against operation --- .../purchase_order_item.json | 2 +- erpnext/manufacturing/doctype/bom/bom.js | 158 +++++-- erpnext/manufacturing/doctype/bom/bom.json | 24 +- erpnext/manufacturing/doctype/bom/bom.py | 30 +- .../doctype/bom_creator/bom_creator.js | 82 +++- .../doctype/bom_creator/bom_creator.json | 100 +++- .../doctype/bom_creator/bom_creator.py | 116 ++++- .../bom_creator_item/bom_creator_item.json | 63 ++- .../bom_creator_item/bom_creator_item.py | 9 + .../doctype/bom_item/bom_item.json | 6 +- .../doctype/bom_item/bom_item.py | 2 + .../doctype/bom_operation/bom_operation.json | 28 +- .../doctype/bom_operation/bom_operation.py | 10 + .../doctype/job_card/job_card.js | 310 +++++------- .../doctype/job_card/job_card.json | 33 +- .../doctype/job_card/job_card.py | 232 +++++++-- .../doctype/operation/operation.json | 4 +- .../doctype/work_order/work_order.js | 35 +- .../doctype/work_order/work_order.json | 16 +- .../doctype/work_order/work_order.py | 19 +- .../work_order_operation.json | 20 +- .../work_order_operation.py | 8 + .../doctype/workstation/workstation.js | 440 ++++++++++++++---- .../doctype/workstation/workstation.json | 8 +- .../doctype/workstation/workstation.py | 161 +++++-- .../workstation/workstation_job_card.html | 149 +++--- .../bom_configurator.bundle.js | 368 ++++++++++++--- .../visual_plant_floor_template.html | 10 +- erpnext/public/scss/erpnext.scss | 10 +- .../stock/doctype/stock_entry/stock_entry.py | 9 +- .../stock_entry_type/stock_entry_type.py | 63 ++- .../subcontracting_bom/subcontracting_bom.py | 2 +- .../subcontracting_receipt.py | 2 +- 33 files changed, 1881 insertions(+), 648 deletions(-) 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 254db63eb356..5f4c9f0fd43d 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -928,7 +928,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2024-03-27 13:11: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 3dc8685e5cd1..f2692d21bfff 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -19,18 +19,18 @@ frappe.ui.form.on("BOM", { }; }); - frm.set_query("bom_no", "operations", function(doc, cdt, cdn) { + 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, - 'make_finished_good_against_job_card': 0 - } + currency: frm.doc.currency, + company: frm.doc.company, + item: row.finished_good, + is_active: 1, + docstatus: 1, + track_semi_finished_goods: 0, + }, }; }); @@ -103,7 +103,12 @@ frappe.ui.form.on("BOM", { 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); + frappe.model.set_value( + d.doctype, + d.name, + "source_warehouse", + frm.doc.default_source_warehouse + ); }); } }, @@ -127,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) { @@ -463,8 +481,7 @@ frappe.ui.form.on("BOM", { }, }); - -frappe.ui.form.on('BOM Operation', { +frappe.ui.form.on("BOM Operation", { bom_no(frm, cdt, cdn) { let row = locals[cdt][cdn]; @@ -480,11 +497,11 @@ frappe.ui.form.on('BOM Operation', { }, callback(r) { refresh_field("items"); - } - }) + }, + }); } - } -}) + }, +}); erpnext.bom.BomController = class BomController extends erpnext.TransactionController { conversion_rate(doc) { @@ -855,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 027d3ee50493..dcc7a4f21768 100644 --- a/erpnext/manufacturing/doctype/bom/bom.json +++ b/erpnext/manufacturing/doctype/bom/bom.json @@ -28,7 +28,7 @@ "conversion_rate", "operations_section_section", "with_operations", - "make_finished_good_against_job_card", + "track_semi_finished_goods", "column_break_23", "transfer_material_against", "routing", @@ -62,8 +62,8 @@ "base_total_cost", "more_info_tab", "item_name", - "description", "column_break_27", + "description", "has_variants", "quality_inspection_section_break", "inspection_required", @@ -214,7 +214,7 @@ }, { "default": "Work Order", - "depends_on": "eval: doc.with_operations === 1 && doc.make_finished_good_against_job_card === 0", + "depends_on": "eval: doc.with_operations === 1 && doc.track_semi_finished_goods === 0", "fieldname": "transfer_material_against", "fieldtype": "Select", "label": "Transfer Material Against", @@ -409,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", @@ -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,7 +626,8 @@ "label": "BOM Creator Item", "no_copy": 1, "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "column_break_oxbz", @@ -634,10 +636,10 @@ { "default": "0", "depends_on": "with_operations", - "description": "User can consume the raw materials, Add Semi Finished Goods / Final Finished Good against the Operation using Job Cards", - "fieldname": "make_finished_good_against_job_card", + "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": "Make Finished Good Against Job Card" + "label": "Track Semi Finished Goods" }, { "fieldname": "column_break_joxb", @@ -661,7 +663,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2024-04-02 16:23: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 69ea9fb112a7..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] @@ -545,8 +548,8 @@ def clear_operations(self): if not self.with_operations: self.set("operations", []) - if not self.with_operations and self.make_finished_good_against_job_card: - self.make_finished_good_against_job_card = 0 + 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: @@ -650,13 +653,29 @@ def _throw_error(bom_name): _throw_error(self.name) def set_materials_based_on_operation_bom(self): - if not self.make_finished_good_against_job_card: + 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}): @@ -1126,7 +1145,7 @@ def get_bom_items_as_dict( item_dict = {} group_by_cond = "group by item_code, stock_uom" - if frappe.get_cached_value("BOM", bom, "make_finished_good_against_job_card"): + 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" @@ -1221,7 +1240,6 @@ def get_bom_items_as_dict( if not item_details.get(d[1]) or (company_in_record and company != company_in_record): item_dict[item][d[1]] = frappe.get_cached_value("Company", company, d[2]) if d[2] else None - print(item_dict) return item_dict diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.js b/erpnext/manufacturing/doctype/bom_creator/bom_creator.js index a83295529109..b44c9f53f238 100644 --- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.js +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.js @@ -96,29 +96,69 @@ frappe.ui.form.on("BOM Creator", { onchange: (r) => { let track_operations = dialog.get_value("track_operations"); if (r.type === "input" && !track_operations) { - dialog.set_value("make_finished_good_against_job_card", 0); + dialog.set_value("track_semi_finished_goods", 0); } - } + }, }, + { fieldtype: "Column Break" }, { - label: __("Make Finished Good Against Job Card"), + label: __("Track Semi Finished Goods"), fieldtype: "Check", - fieldname: "make_finished_good_against_job_card", - depends_on: "eval:doc.track_operations" + fieldname: "track_semi_finished_goods", + depends_on: "eval:doc.track_operations", }, - { fieldtype: "Column Break" }, { - label: __("Final Operation"), + fieldtype: "Section Break", + label: __("Final Product Operation"), + depends_on: "eval:doc.track_semi_finished_goods", + }, + { + label: __("Operation"), fieldtype: "Link", - fieldname: "final_operation", + fieldname: "operation", options: "Operation", default: "Assembly", - mandatory_depends_on: "eval:doc.make_finished_good_against_job_card", - depends_on: "eval:doc.make_finished_good_against_job_card" + 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); @@ -130,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); @@ -149,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 1fccf0f3317c..2b4b9a055aa3 100644 --- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.json +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.json @@ -39,9 +39,21 @@ "raw_material_cost", "configuration_section", "track_operations", - "make_finished_good_against_job_card", "column_break_obzr", - "final_operation", + "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", @@ -292,25 +304,95 @@ { "default": "0", "depends_on": "track_operations", - "fieldname": "make_finished_good_against_job_card", + "fieldname": "track_semi_finished_goods", "fieldtype": "Check", - "label": "Make Finished Good Against Job Card" + "label": "Track Semi Finished Goods" }, { "fieldname": "column_break_obzr", "fieldtype": "Column Break" }, { - "fieldname": "final_operation", + "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": "Final Operation", + "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": "track_operations", + "fieldname": "skip_material_transfer", "fieldtype": "Check", - "label": "Track Operations" + "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", @@ -321,7 +403,7 @@ "link_fieldname": "bom_creator" } ], - "modified": "2024-04-02 16:31: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 08adc8ef9301..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): @@ -207,8 +218,7 @@ def validate_fields(self): frappe.throw(_("Please set {0} in BOM Creator {1}").format(label, self.name)) def on_submit(self): - pass - # self.enqueue_create_boms() + self.enqueue_create_boms() @frappe.whitelist() def enqueue_create_boms(self): @@ -237,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: @@ -258,10 +270,13 @@ 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.make_finished_good_against_job_card: + 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")) @@ -288,7 +303,7 @@ def make_bom_for_final_product(self, production_item_wise_rm): "bom_creator_item": self.name, "rm_cost_as_per": "Manual", "with_operations": 1, - "make_finished_good_against_job_card": 1, + "track_semi_finished_goods": 1, } ) @@ -304,24 +319,50 @@ def make_bom_for_final_product(self, production_item_wise_rm): "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, }, ) - bom.append( + operation_row = bom.append( "operations", { - "operation": self.final_operation, + "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, - "bom_no": production_item_wise_rm[(self.item_code, self.name)].bom_no, + "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() @@ -350,9 +391,10 @@ def create_bom(self, row, production_item_wise_rm): } ) - if self.track_operations and not self.make_finished_good_against_job_card: + 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 @@ -362,6 +404,7 @@ def create_bom(self, row, production_item_wise_rm): { "operation": item.operation, "workstation_type": item.workstation_type, + "workstation": item.workstation, "time_in_mins": item.operation_time, }, ) @@ -421,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 = { @@ -434,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): @@ -444,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, @@ -452,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() @@ -471,7 +538,7 @@ def add_sub_assembly(**kwargs): name = kwargs.fg_reference_id parent_row_no = "" - print(kwargs) + if not kwargs.convert_to_sub_assembly: item_info = get_item_details(bom_item.item_code) item_row = doc.append( @@ -490,6 +557,11 @@ def add_sub_assembly(**kwargs): "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, }, ) @@ -499,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) @@ -555,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 fc555b309874..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,15 +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", - "operation_time", + "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", @@ -80,6 +88,7 @@ "reqd": 1 }, { + "depends_on": "eval:doc.skip_material_transfer && !doc.backflush_from_wip_warehouse", "fieldname": "source_warehouse", "fieldtype": "Link", "label": "Source Warehouse", @@ -258,12 +267,60 @@ "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:41.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 55ef70f24d23..1d530af34a28 100644 --- a/erpnext/manufacturing/doctype/bom_item/bom_item.json +++ b/erpnext/manufacturing/doctype/bom_item/bom_item.json @@ -296,17 +296,17 @@ "read_only": 1 }, { - "depends_on": "eval:parent.make_finished_good_against_job_card ==1", + "depends_on": "eval:parent.track_semi_finished_goods ==1", "fieldname": "operation_row_id", "fieldtype": "Int", - "label": "Operation Row ID" + "label": "Operation ID" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2024-03-27 13:07: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 47f7c506a0fa..b9e960ab66e0 100644 --- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json +++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json @@ -11,6 +11,7 @@ "finished_good", "finished_good_qty", "bom_no", + "add_raw_materials", "col_break1", "workstation_type", "workstation", @@ -20,9 +21,11 @@ "is_final_finished_good", "set_cost_based_on_bom_qty", "warehouse_section", + "skip_material_transfer", + "backflush_from_wip_warehouse", "source_warehouse", - "wip_warehouse", "column_break_lbhy", + "wip_warehouse", "fg_warehouse", "costing_section", "hour_rate", @@ -230,6 +233,7 @@ "label": "Warehouse" }, { + "depends_on": "eval:!doc.skip_material_transfer || doc.backflush_from_wip_warehouse", "fieldname": "wip_warehouse", "fieldtype": "Link", "label": "WIP WH", @@ -249,6 +253,7 @@ }, { "columns": 1, + "depends_on": "eval:doc.skip_material_transfer && !doc.backflush_from_wip_warehouse", "fieldname": "source_warehouse", "fieldtype": "Link", "in_list_view": 1, @@ -260,13 +265,32 @@ "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:07: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 61aec73506a3..2de5d9dad115 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -43,9 +43,32 @@ frappe.ui.form.on("Job Card", { } }, + 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"); @@ -63,8 +86,8 @@ frappe.ui.form.on("Job Card", { frm.toggle_enable("for_quantity", !has_stock_entry); - if (!frm.is_new() && !frm.doc.skip_material_transfer && has_items && frm.doc.docstatus === 1) { - let to_request = frm.doc.for_quantity > frm.doc.material_transferred_for_manufacturing; + 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; if (to_request || excess_transfer_allowed) { @@ -80,7 +103,7 @@ 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"); + }); } } @@ -100,58 +123,65 @@ frappe.ui.form.on("Job Card", { frm.trigger("toggle_operation_number"); - if (!frm.doc.finished_good && frm.doc.docstatus == 1 && !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)) { - - // 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"); - } - } - ); - } else { - frm.trigger("prepare_timer_buttons"); - } - } else if (frm.doc.finished_good && frm.doc.docstatus == 1 - && (frm.doc.for_quantity - frm.doc.material_transferred_for_manufacturing <= 0 || frm.doc.skip_material_transfer)) { + if ( + frm.doc.for_quantity + frm.doc.process_loss_qty > frm.doc.total_completed_qty && + (frm.doc.skip_material_transfer || + frm.doc.transferred_qty >= frm.doc.for_quantity + frm.doc.process_loss_qty || + !frm.doc.finished_good) + ) { if (!frm.doc.time_logs?.length) { frm.add_custom_button(__("Start Job"), () => { - frappe.prompt([ - { - fieldtype: "Datetime", - label: __("Start Time"), - fieldname: "start_time", - reqd: 1, - default: frappe.datetime.now_datetime() + 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(), }, - { - label: __("Operator"), - fieldname: "employee", - fieldtype: "Link", - options: "Employee", - } - ], data => { - frm.events.start_timer(frm, data.start_time, data.employee); - }, __("Enter Value"), __("Start Job")); - }).addClass("btn-primary"); + callback() { + frm.reload_doc(); + }, + }); + }); } else { if (frm.doc.for_quantity - frm.doc.manufactured_qty > 0) { - frm.add_custom_button(__("Complete Job"), () => { - frm.trigger("make_finished_good"); - }).addClass("btn-primary"); + 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"); @@ -161,8 +191,7 @@ frappe.ui.form.on("Job Card", { frm.trigger("setup_quality_inspection"); if (frm.doc.work_order) { - frappe.db.get_value("Work Order", frm.doc.work_order, - "transfer_material_against").then((r) => { + frappe.db.get_value("Work Order", frm.doc.work_order, "transfer_material_against").then((r) => { if (r.message.transfer_material_against == "Work Order" && !frm.doc.operation_row_id) { frm.set_df_property("items", "hidden", 1); } @@ -186,24 +215,24 @@ frappe.ui.form.on("Job Card", { 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 + frm: frm, }); }).addClass("btn-primary"); } }, - start_timer(frm, start_time, employee) { + start_timer(frm, start_time, employees) { frm.call({ method: "start_timer", doc: frm.doc, args: { start_time: start_time, - employee: employee + employees: employees, }, - callback: function(r) { + callback: function (r) { frm.reload_doc(); frm.trigger("make_dashboard"); - } + }, }); }, @@ -214,37 +243,43 @@ frappe.ui.form.on("Job Card", { label: __("Completed Quantity"), fieldname: "qty", reqd: 1, - default: frm.doc.for_quantity - frm.doc.manufactured_qty + default: frm.doc.for_quantity - frm.doc.manufactured_qty, }, { fieldtype: "Datetime", label: __("End Time"), fieldname: "end_time", - default: frappe.datetime.now_datetime() + 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); + frappe.prompt( + fields, + (data) => { + if (data.qty <= 0) { + frappe.throw(__("Quantity should be greater than 0")); } - }); - }, __("Enter Value"), __("Make Stock Entry"), __("Set Finished Good Quantity")); + + 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) { + 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) { return { @@ -372,101 +407,6 @@ frappe.ui.form.on("Job Card", { frm.toggle_reqd("operation_row_number", !frm.doc.operation_id && frm.doc.operation); }, - prepare_timer_buttons(frm) { - if (in_list(["Completed", "Materials Not Consumed"], frm.doc.status)) { - return; - } - - 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, "Work In Progress", 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) { - let fields = [{ - fieldtype: "Float", - label: __("Completed Quantity"), - fieldname: "qty", - default: frm.doc.for_quantity - }]; - - fields.push({ - fieldtype: "Datetime", - label: __("End Time"), - fieldname: "to_time", - default: frappe.datetime.now_datetime() - }); - - let label = frm.doc.finished_good ? __("Make Stock Entry") : __("Complete Job"); - - frappe.prompt(fields, data => { - frm.events.complete_job(frm, "Completed", data.qty, data.to_time); - }, __("Enter Value"), label, __("Set Quantity")); - } else { - frm.events.complete_job(frm, "Completed", 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, to_time) { - const args = { - job_card_id: frm.doc.name, - status: status, - completed_qty: completed_qty, - complete_time: to_time || frappe.datetime.now_datetime(), - }; - frm.events.make_time_log(frm, args); - }, - make_time_log: function (frm, args) { frm.events.update_sub_operation(frm, args); @@ -513,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") @@ -546,9 +486,7 @@ 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.finished_good && frm.doc.time_logs?.length - && frm.doc.time_logs[0].to_time - ) { + 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); @@ -560,17 +498,17 @@ frappe.ui.form.on("Job Card", { get_current_time(frm) { let current_time = 0; - frm.doc.time_logs.forEach(d => { + 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 += frappe.datetime.get_minute_diff(d.to_time, d.from_time) * 60; + current_time += get_seconds_diff(d.to_time, d.from_time); } } else { - current_time += frappe.datetime.get_minute_diff(frappe.datetime.now_datetime(), d.from_time) * 60; + current_time += get_seconds_diff(frappe.datetime.now_datetime(), d.from_time); } - }) + }); return current_time; }, @@ -631,11 +569,11 @@ frappe.ui.form.on("Job Card", { source_warehouse(frm) { if (frm.doc.source_warehouse) { - frm.doc.items.forEach(d => { + 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", { @@ -647,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 2faaf8c21433..9a0d1c69a1b7 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.json +++ b/erpnext/manufacturing/doctype/job_card/job_card.json @@ -22,7 +22,7 @@ "total_completed_qty", "column_break_mcnb", "for_quantity", - "material_transferred_for_manufacturing", + "transferred_qty", "manufactured_qty", "process_loss_qty", "production_section", @@ -69,10 +69,10 @@ "for_operation", "more_information", "item_name", - "transferred_qty", "requested_qty", "status", "operation_row_id", + "is_paused", "column_break_20", "operation_row_number", "operation_id", @@ -119,6 +119,7 @@ "fieldname": "operation", "fieldtype": "Link", "in_list_view": 1, + "in_preview": 1, "label": "Operation", "options": "Operation", "reqd": 1 @@ -144,6 +145,7 @@ "fieldname": "for_quantity", "fieldtype": "Float", "in_list_view": 1, + "in_preview": 1, "label": "Qty To Manufacture" }, { @@ -177,6 +179,7 @@ "depends_on": "eval:doc.is_subcontracted===0", "fieldname": "total_completed_qty", "fieldtype": "Float", + "in_preview": 1, "label": "Total Completed Qty", "read_only": 1 }, @@ -216,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 }, { @@ -294,7 +298,7 @@ "fetch_from": "work_order.production_item", "fieldname": "production_item", "fieldtype": "Link", - "label": "Finished Good", + "label": "Final Product", "options": "Item", "read_only": 1 }, @@ -520,7 +524,8 @@ { "fieldname": "finished_good", "fieldtype": "Link", - "label": "Semi Finished Good", + "in_preview": 1, + "label": "Finished Good", "options": "Item", "read_only": 1 }, @@ -536,14 +541,6 @@ "fieldtype": "Int", "label": "Operation Row ID" }, - { - "depends_on": "eval:doc.is_subcontracted===0 && doc.finished_good", - "fieldname": "material_transferred_for_manufacturing", - "fieldtype": "Float", - "label": "Material Transferred for Manufacturing", - "no_copy": 1, - "read_only": 1 - }, { "fieldname": "source_warehouse", "fieldtype": "Link", @@ -606,11 +603,18 @@ "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:10:56.634418", + "modified": "2024-05-26 17:44:18.324743", "modified_by": "Administrator", "module": "Manufacturing", "name": "Job Card", @@ -663,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 012743454f5b..100ca45a9f09 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -20,6 +20,7 @@ get_time, getdate, time_diff, + time_diff_in_hours, ) from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import ( @@ -65,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 @@ -81,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 @@ -103,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", @@ -117,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 +149,8 @@ def before_validate(self): self.set_wip_warehouse() 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,10 @@ 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: @@ -570,6 +584,7 @@ def add_start_time_log(self, args): def set_employees(self, employees): for name in employees: self.append("employee", {"employee": name.get("employee"), "completed_qty": 0.0}) + self.save() def update_sub_operation_status(self): if not (self.sub_operations and self.time_logs): @@ -631,7 +646,7 @@ def get_required_items(self): return doc = frappe.get_doc("Work Order", self.get("work_order")) - if not doc.make_finished_good_against_job_card and ( + if not doc.track_semi_finished_goods and ( doc.transfer_material_against == "Work Order" or doc.skip_transfer ): return @@ -767,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: @@ -959,22 +973,51 @@ def set_transferred_qty(self, update_status=False): if query and query[0][0]: qty = flt(query[0][0]) - self.db_set("material_transferred_for_manufacturing", qty) + 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.material_transferred_for_manufacturing > 0: + elif self.transferred_qty > 0 or self.skip_material_transfer: self.status = "Work In Progress" + 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.material_transferred_for_manufacturing): + if flt(self.for_quantity) <= flt(self.transferred_qty): self.status = "Material Transferred" if self.time_logs: @@ -983,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: @@ -1015,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 @@ -1041,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( @@ -1081,38 +1153,89 @@ def update_status_in_workstation(self, status): def add_time_logs(self, **kwargs): row = None kwargs = frappe._dict(kwargs) - if kwargs.start_time and not kwargs.end_time: - row = self.append( - "time_logs", - { - "from_time": kwargs.start_time, - "employee": kwargs.employee, - }, - ) - else: - row = self.time_logs[-1] - row.to_time = kwargs.end_time - row.time_in_mins = time_diff_in_minutes(kwargs.end_time, row.from_time) - row.completed_qty = kwargs.completed_qty - if row: - row.db_update() + 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, start_time=None, employee=None): - if start_time: - self.add_time_logs(start_time=start_time, employee=employee) + 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 make_finished_good(self, qty, end_time=None): - from erpnext.stock.doctype.stock_entry_type.stock_entry_type import ManufactureEntry + 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)) + ) - if end_time: - self.add_time_logs(end_time=end_time, completed_qty=qty) + @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( { - "qty_to_manufacture": qty, + "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, @@ -1130,6 +1253,14 @@ def make_finished_good(self, qty, end_time=None): 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() @@ -1137,7 +1268,6 @@ def make_finished_good(self, qty, end_time=None): 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) - print(_item_details) pending_qty = source.for_quantity - source.manufactured_qty service_item_qty = flt(_item_details.service_item_qty) or 1.0 @@ -1178,14 +1308,14 @@ def set_missing_values(source, target): @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) 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 72442aafae35..8a806c179e9d 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -143,9 +143,12 @@ frappe.ui.form.on("Work Order", { } if (frm.doc.status != "Closed") { - if (frm.doc.docstatus === 1 && frm.doc.status !== "Completed" - && frm.doc.operations && frm.doc.operations.length) { - + if ( + frm.doc.docstatus === 1 && + frm.doc.status !== "Completed" && + frm.doc.operations && + frm.doc.operations.length + ) { if (frm.doc.__onload?.show_create_job_card_button) { frm.add_custom_button(__("Create Job Card"), () => { frm.trigger("make_job_card"); @@ -268,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, @@ -308,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, }); } } @@ -606,21 +623,21 @@ erpnext.work_order = { ); } - if (!frm.doc.make_finished_good_against_job_card) { - 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) + (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() { + frm.add_custom_button(__("Create Pick List"), function () { erpnext.work_order.create_pick_list(frm); }); - var start_btn = frm.add_custom_button(__("Start"), function() { + 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 a08bd0de2696..e564af27d95d 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -22,7 +22,7 @@ "produced_qty", "process_loss_qty", "project", - "make_finished_good_against_job_card", + "track_semi_finished_goods", "warehouses", "source_warehouse", "wip_warehouse", @@ -195,7 +195,7 @@ }, { "default": "0", - "depends_on": "eval:doc.docstatus==1 && doc.skip_transfer==0 && doc.make_finished_good_against_job_card === 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", @@ -247,7 +247,7 @@ "fieldname": "wip_warehouse", "fieldtype": "Link", "label": "Work-in-Progress Warehouse", - "mandatory_depends_on": "eval:(!doc.skip_transfer || doc.from_wip_warehouse) && !doc.make_finished_good_against_job_card", + "mandatory_depends_on": "eval:(!doc.skip_transfer || doc.from_wip_warehouse) && !doc.track_semi_finished_goods", "options": "Warehouse" }, { @@ -328,7 +328,7 @@ "options": "fa fa-wrench" }, { - "depends_on": "eval: doc.operations?.length && doc.make_finished_good_against_job_card === 0", + "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,10 +579,10 @@ }, { "default": "0", - "fetch_from": "bom_no.make_finished_good_against_job_card", - "fieldname": "make_finished_good_against_job_card", + "fetch_from": "bom_no.track_semi_finished_goods", + "fieldname": "track_semi_finished_goods", "fieldtype": "Check", - "label": "Make Finished Good Against Job Card", + "label": "Track Semi Finished Goods", "read_only": 1 } ], @@ -591,7 +591,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2024-03-27 13:12: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 243a1a0cb135..b4d1e9d26935 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -441,13 +441,13 @@ def update_production_plan_status(self): production_plan.run_method("update_produced_pending_qty", produced_qty, self.production_plan_item) def validate_warehouse(self): - if self.make_finished_good_against_job_card: + if self.track_semi_finished_goods: return 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() @@ -690,7 +690,7 @@ def validate_cancel(self): ) def update_planned_qty(self): - if self.make_finished_good_against_job_card: + if self.track_semi_finished_goods: return from erpnext.manufacturing.doctype.production_plan.production_plan import ( @@ -850,6 +850,8 @@ def _get_operations(bom_no, qty=1): "batch_size", "sequence_id", "fixed_time", + "skip_material_transfer", + "backflush_from_wip_warehouse", ], order_by="idx", ) @@ -859,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", []) @@ -1317,9 +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.make_finished_good_against_job_card = frappe.db.get_value( - "BOM", bom_no, "make_finished_good_against_job_card" - ) + 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 @@ -1624,13 +1627,15 @@ def create_job_card(work_order, row, enable_capacity_planning=False, auto_create "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.make_finished_good_against_job_card or ( + 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() 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 45e3d77f749d..9146122a858f 100644 --- a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json +++ b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json @@ -19,6 +19,8 @@ "bom_no", "finished_good", "is_subcontracted", + "skip_material_transfer", + "backflush_from_wip_warehouse", "column_break_vjih", "source_warehouse", "wip_warehouse", @@ -224,7 +226,7 @@ "read_only": 1 }, { - "depends_on": "eval:parent.make_finished_good_against_job_card === 1", + "depends_on": "eval:parent.track_semi_finished_goods === 1", "fieldname": "section_break_insy", "fieldtype": "Section Break" }, @@ -276,12 +278,26 @@ "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:12: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) => { %}