diff --git a/erpnext/manufacturing/doctype/plant_floor/__init__.py b/erpnext/manufacturing/doctype/plant_floor/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/erpnext/manufacturing/doctype/plant_floor/plant_floor.js b/erpnext/manufacturing/doctype/plant_floor/plant_floor.js new file mode 100644 index 000000000000..427893743afc --- /dev/null +++ b/erpnext/manufacturing/doctype/plant_floor/plant_floor.js @@ -0,0 +1,19 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on("Plant Floor", { + refresh(frm) { + frm.trigger('prepare_dashboard') + }, + + prepare_dashboard(frm) { + let wrapper = $(frm.fields_dict["plant_dashboard"].wrapper); + wrapper.empty(); + + frappe.visual_plant_floor = new frappe.ui.VisualPlantFloor({ + wrapper: wrapper, + skip_filters: true, + plant_floor: frm.doc.name, + }); + }, +}); diff --git a/erpnext/manufacturing/doctype/plant_floor/plant_floor.json b/erpnext/manufacturing/doctype/plant_floor/plant_floor.json new file mode 100644 index 000000000000..aa6eb1dd40ff --- /dev/null +++ b/erpnext/manufacturing/doctype/plant_floor/plant_floor.json @@ -0,0 +1,81 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "field:floor_name", + "creation": "2023-10-06 15:06:07.976066", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "workstations_tab", + "plant_dashboard", + "details_tab", + "column_break_mvbx", + "floor_name", + "section_break_cczv", + "volumetric_weight" + ], + "fields": [ + { + "fieldname": "floor_name", + "fieldtype": "Data", + "label": "Floor Name", + "unique": 1 + }, + { + "depends_on": "eval:!doc.__islocal", + "fieldname": "workstations_tab", + "fieldtype": "Tab Break", + "label": "Dashboard" + }, + { + "fieldname": "plant_dashboard", + "fieldtype": "HTML", + "label": "Plant Dashboard" + }, + { + "fieldname": "details_tab", + "fieldtype": "Tab Break", + "label": "Details" + }, + { + "fieldname": "column_break_mvbx", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_cczv", + "fieldtype": "Section Break" + }, + { + "fieldname": "volumetric_weight", + "fieldtype": "Float", + "label": "Volumetric Weight" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2023-12-04 15:36:09.641203", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Plant Floor", + "naming_rule": "By fieldname", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/plant_floor/plant_floor.py b/erpnext/manufacturing/doctype/plant_floor/plant_floor.py new file mode 100644 index 000000000000..729cc3337a98 --- /dev/null +++ b/erpnext/manufacturing/doctype/plant_floor/plant_floor.py @@ -0,0 +1,21 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class PlantFloor(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + floor_name: DF.Data | None + volumetric_weight: DF.Float + # end: auto-generated types + + pass diff --git a/erpnext/manufacturing/doctype/plant_floor/test_plant_floor.py b/erpnext/manufacturing/doctype/plant_floor/test_plant_floor.py new file mode 100644 index 000000000000..2fac21133666 --- /dev/null +++ b/erpnext/manufacturing/doctype/plant_floor/test_plant_floor.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestPlantFloor(FrappeTestCase): + pass diff --git a/erpnext/manufacturing/doctype/workstation/workstation.js b/erpnext/manufacturing/doctype/workstation/workstation.js index f830b170ed0e..4ffc506f52e8 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.js +++ b/erpnext/manufacturing/doctype/workstation/workstation.js @@ -2,6 +2,28 @@ // License: GNU General Public License v3. See license.txt frappe.ui.form.on("Workstation", { + set_illustration_image(frm) { + let status_image_field = frm.doc.status == "Production" ? frm.doc.on_status_image : frm.doc.off_status_image; + if (status_image_field) { + frm.sidebar.image_wrapper.find(".sidebar-image").attr("src", status_image_field); + } + }, + + refresh(frm) { + frm.trigger("set_illustration_image"); + frm.trigger("prepapre_dashboard"); + }, + + prepapre_dashboard(frm) { + let $parent = $(frm.fields_dict["workstation_dashboard"].wrapper); + $parent.empty(); + + let workstation_dashboard = new WorkstationDashboard({ + wrapper: $parent, + frm: frm + }); + }, + onload(frm) { if(frm.is_new()) { @@ -54,3 +76,42 @@ frappe.tour['Workstation'] = [ ]; + + +class WorkstationDashboard { + constructor({ wrapper, frm }) { + this.$wrapper = $(wrapper); + this.frm = frm; + + this.prepapre_dashboard(); + } + + prepapre_dashboard() { + frappe.call({ + method: "erpnext.manufacturing.doctype.workstation.workstation.get_job_cards", + args: { + workstation: this.frm.doc.name + }, + callback: (r) => { + if (r.message) { + this.render_job_cards(r.message); + } + } + }); + } + + render_job_cards(job_cards) { + let template = frappe.render_template("workstation_job_card", { + data: job_cards + }); + + this.$wrapper.html(template); + this.$wrapper.find(".collapse-indicator-job").on("click", (e) => { + $(e.currentTarget).closest(".form-dashboard-section").find(".section-body-job-card").toggleClass("hide") + if ($(e.currentTarget).closest(".form-dashboard-section").find(".section-body-job-card").hasClass("hide")) + $(e.currentTarget).html(frappe.utils.icon("es-line-down", "sm", "mb-1")) + else + $(e.currentTarget).html(frappe.utils.icon("es-line-up", "sm", "mb-1")) + }); + } +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/workstation/workstation.json b/erpnext/manufacturing/doctype/workstation/workstation.json index 881cba0cce00..5912714052be 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.json +++ b/erpnext/manufacturing/doctype/workstation/workstation.json @@ -8,10 +8,24 @@ "document_type": "Setup", "engine": "InnoDB", "field_order": [ + "dashboard_tab", + "workstation_dashboard", + "details_tab", "workstation_name", - "production_capacity", - "column_break_3", "workstation_type", + "plant_floor", + "column_break_3", + "production_capacity", + "warehouse", + "production_capacity_section", + "parts_per_hour", + "workstation_status_tab", + "status", + "column_break_glcv", + "illustration_section", + "on_status_image", + "column_break_etmc", + "off_status_image", "over_heads", "hour_rate_electricity", "hour_rate_consumable", @@ -24,7 +38,9 @@ "description", "working_hours_section", "holiday_list", - "working_hours" + "working_hours", + "total_working_hours", + "connections_tab" ], "fields": [ { @@ -120,9 +136,10 @@ }, { "default": "1", + "description": "Run parallel job cards in a workstation", "fieldname": "production_capacity", "fieldtype": "Int", - "label": "Production Capacity", + "label": "Job Capacity", "reqd": 1 }, { @@ -145,12 +162,97 @@ { "fieldname": "section_break_11", "fieldtype": "Section Break" + }, + { + "fieldname": "plant_floor", + "fieldtype": "Link", + "label": "Plant Floor", + "options": "Plant Floor" + }, + { + "fieldname": "workstation_status_tab", + "fieldtype": "Tab Break", + "label": "Workstation Status" + }, + { + "fieldname": "illustration_section", + "fieldtype": "Section Break", + "label": "Status Illustration" + }, + { + "fieldname": "column_break_etmc", + "fieldtype": "Column Break" + }, + { + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Status", + "options": "Production\nOff\nIdle\nProblem\nMaintenance\nSetup" + }, + { + "fieldname": "column_break_glcv", + "fieldtype": "Column Break" + }, + { + "fieldname": "on_status_image", + "fieldtype": "Attach Image", + "label": "Active Status" + }, + { + "fieldname": "off_status_image", + "fieldtype": "Attach Image", + "label": "Inactive Status" + }, + { + "fieldname": "warehouse", + "fieldtype": "Link", + "label": "Warehouse", + "options": "Warehouse" + }, + { + "fieldname": "production_capacity_section", + "fieldtype": "Section Break", + "label": "Production Capacity" + }, + { + "fieldname": "parts_per_hour", + "fieldtype": "Float", + "label": "Parts Per Hour" + }, + { + "fieldname": "total_working_hours", + "fieldtype": "Float", + "label": "Total Working Hours" + }, + { + "depends_on": "eval:!doc.__islocal", + "fieldname": "dashboard_tab", + "fieldtype": "Tab Break", + "label": "Job Cards" + }, + { + "fieldname": "details_tab", + "fieldtype": "Tab Break", + "label": "Details" + }, + { + "fieldname": "connections_tab", + "fieldtype": "Tab Break", + "label": "Connections", + "show_dashboard": 1 + }, + { + "fieldname": "workstation_dashboard", + "fieldtype": "HTML", + "label": "Workstation Dashboard" } ], "icon": "icon-wrench", "idx": 1, + "image_field": "on_status_image", "links": [], - "modified": "2022-11-04 17:39:01.549346", + "modified": "2023-11-30 12:43:35.808845", "modified_by": "Administrator", "module": "Manufacturing", "name": "Workstation", diff --git a/erpnext/manufacturing/doctype/workstation/workstation.py b/erpnext/manufacturing/doctype/workstation/workstation.py index 0f05eaac00b7..973c99421d45 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.py +++ b/erpnext/manufacturing/doctype/workstation/workstation.py @@ -11,7 +11,11 @@ comma_and, flt, formatdate, + get_link_to_form, + get_time, + get_url_to_form, getdate, + time_diff_in_hours, time_diff_in_seconds, to_timedelta, ) @@ -60,6 +64,23 @@ class Workstation(Document): def before_save(self): self.set_data_based_on_workstation_type() self.set_hour_rate() + self.set_total_working_hours() + + def set_total_working_hours(self): + self.total_working_hours = 0.0 + for row in self.working_hours: + self.validate_working_hours(row) + + if row.start_time and row.end_time: + row.hours = flt(time_diff_in_hours(row.end_time, row.start_time), row.precision("hours")) + self.total_working_hours += row.hours + + def validate_working_hours(self, row): + if not (row.start_time and row.end_time): + frappe.throw(_("Row #{0}: Start Time and End Time are required").format(row.idx)) + + if get_time(row.start_time) >= get_time(row.end_time): + frappe.throw(_("Row #{0}: Start Time must be before End Time").format(row.idx)) def set_hour_rate(self): self.hour_rate = ( @@ -144,6 +165,86 @@ def validate_workstation_holiday(self, schedule_date, skip_holiday_list_check=Fa return schedule_date +@frappe.whitelist() +def get_job_cards(workstation): + if frappe.has_permission("Job Card", "read"): + jc_data = frappe.get_all( + "Job Card", + fields=[ + "name", + "production_item", + "work_order", + "operation", + "total_completed_qty", + "for_quantity", + "status", + "expected_start_date", + "expected_end_date", + "time_required", + "wip_warehouse", + ], + filters={ + "workstation": workstation, + "docstatus": ("<", 2), + "status": ["not in", ["Completed", "Stopped"]], + }, + order_by="expected_start_date, expected_end_date", + ) + + job_cards = [row.name for row in jc_data] + raw_materials = get_raw_materials(job_cards) + + 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) + row.status_color = get_status_color(row.status) + row.job_card_link = get_link_to_form("Job Card", row.name) + row.work_order_link = get_link_to_form("Work Order", row.work_order) + + row.raw_materials = raw_materials.get(row.name, []) + + return jc_data + + +def get_status_color(status): + colos_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)", + } + + return colos_map.get(status, "var(--bg-blue)") + + +def get_raw_materials(job_cards): + raw_materials = {} + + data = frappe.get_all( + "Job Card Item", + fields=[ + "parent", + "item_code", + "item_group", + "uom", + "item_name", + "source_warehouse", + "required_qty", + "transferred_qty", + ], + filters={"parent": ["in", job_cards]}, + ) + + for row in data: + raw_materials.setdefault(row.parent, []).append(row) + + return raw_materials + + @frappe.whitelist() def get_default_holiday_list(): return frappe.get_cached_value( @@ -201,3 +302,52 @@ def check_workstation_for_holiday(workstation, from_datetime, to_datetime): + "\n".join(applicable_holidays), WorkstationHolidayError, ) + + +@frappe.whitelist() +def get_workstations(**kwargs): + kwargs = frappe._dict(kwargs) + _workstation = frappe.qb.DocType("Workstation") + + query = ( + frappe.qb.from_(_workstation) + .select( + _workstation.name, + _workstation.description, + _workstation.status, + _workstation.on_status_image, + _workstation.off_status_image, + ) + .orderby(_workstation.workstation_type, _workstation.name) + .where(_workstation.plant_floor == kwargs.plant_floor) + ) + + if kwargs.workstation: + query = query.where(_workstation.name == kwargs.workstation) + + if kwargs.workstation_type: + query = query.where(_workstation.workstation_type == kwargs.workstation_type) + + if kwargs.workstation_status: + query = query.where(_workstation.status == kwargs.workstation_status) + + 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)", + } + + 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.workstation_link = get_url_to_form("Workstation", d.name) + if d.status != "Production": + d.status_image = d.off_status_image + + return data diff --git a/erpnext/manufacturing/doctype/workstation/workstation_job_card.html b/erpnext/manufacturing/doctype/workstation/workstation_job_card.html new file mode 100644 index 000000000000..3c0ef6d837dd --- /dev/null +++ b/erpnext/manufacturing/doctype/workstation/workstation_job_card.html @@ -0,0 +1,97 @@ + + +
+{% $.each(data, (idx, d) => { %} + +{% }); %} +
\ No newline at end of file diff --git a/erpnext/manufacturing/doctype/workstation/workstation_list.js b/erpnext/manufacturing/doctype/workstation/workstation_list.js index 61f2062ec0b8..86928cafcb22 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation_list.js +++ b/erpnext/manufacturing/doctype/workstation/workstation_list.js @@ -1,5 +1,16 @@ frappe.listview_settings['Workstation'] = { - // add_fields: ["status"], - // filters:[["status","=", "Open"]] + add_fields: ["status"], + get_indicator: function(doc) { + let color_map = { + "Production": "green", + "Off": "gray", + "Idle": "gray", + "Problem": "red", + "Maintenance": "yellow", + "Setup": "blue", + } + + return [__(doc.status), color_map[doc.status], true]; + } }; diff --git a/erpnext/manufacturing/doctype/workstation_working_hour/workstation_working_hour.json b/erpnext/manufacturing/doctype/workstation_working_hour/workstation_working_hour.json index a79182fb31b3..b185f7d29de8 100644 --- a/erpnext/manufacturing/doctype/workstation_working_hour/workstation_working_hour.json +++ b/erpnext/manufacturing/doctype/workstation_working_hour/workstation_working_hour.json @@ -1,150 +1,58 @@ { - "allow_copy": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2014-12-24 14:46:40.678236", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "creation": "2014-12-24 14:46:40.678236", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "start_time", + "hours", + "column_break_2", + "end_time", + "enabled" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "start_time", - "fieldtype": "Time", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Start Time", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "start_time", + "fieldtype": "Time", + "in_list_view": 1, + "label": "Start Time", + "reqd": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_2", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "end_time", - "fieldtype": "Time", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "End Time", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "end_time", + "fieldtype": "Time", + "in_list_view": 1, + "label": "End Time", + "reqd": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "1", - "fieldname": "enabled", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Enabled", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "default": "1", + "fieldname": "enabled", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Enabled" + }, + { + "fieldname": "hours", + "fieldtype": "Float", + "label": "Hours", + "read_only": 1 } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2016-12-13 05:02:36.754145", - "modified_by": "Administrator", - "module": "Manufacturing", - "name": "Workstation Working Hour", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_seen": 0 + ], + "istable": 1, + "links": [], + "modified": "2023-10-25 14:48:29.697498", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Workstation Working Hour", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/manufacturing/page/visual_plant_floor/__init__.py b/erpnext/manufacturing/page/visual_plant_floor/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/erpnext/manufacturing/page/visual_plant_floor/visual_plant_floor.js b/erpnext/manufacturing/page/visual_plant_floor/visual_plant_floor.js new file mode 100644 index 000000000000..38667e8d795e --- /dev/null +++ b/erpnext/manufacturing/page/visual_plant_floor/visual_plant_floor.js @@ -0,0 +1,13 @@ + + +frappe.pages['visual-plant-floor'].on_page_load = function(wrapper) { + var page = frappe.ui.make_app_page({ + parent: wrapper, + title: 'Visual Plant Floor', + single_column: true + }); + + frappe.visual_plant_floor = new frappe.ui.VisualPlantFloor( + {wrapper: $(wrapper).find('.layout-main-section')}, wrapper.page + ); +} \ No newline at end of file diff --git a/erpnext/manufacturing/page/visual_plant_floor/visual_plant_floor.json b/erpnext/manufacturing/page/visual_plant_floor/visual_plant_floor.json new file mode 100644 index 000000000000..a907e973e345 --- /dev/null +++ b/erpnext/manufacturing/page/visual_plant_floor/visual_plant_floor.json @@ -0,0 +1,29 @@ +{ + "content": null, + "creation": "2023-10-06 15:17:39.215300", + "docstatus": 0, + "doctype": "Page", + "idx": 0, + "modified": "2023-10-06 15:18:00.622073", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "visual-plant-floor", + "owner": "Administrator", + "page_name": "visual-plant-floor", + "roles": [ + { + "role": "Manufacturing User" + }, + { + "role": "Manufacturing Manager" + }, + { + "role": "Operator" + } + ], + "script": null, + "standard": "Yes", + "style": null, + "system_page": 0, + "title": "Visual Plant Floor" +} \ No newline at end of file diff --git a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json index 8e0785074faa..e3b632dba2e2 100644 --- a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json +++ b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json @@ -1,6 +1,6 @@ { "charts": [], - "content": "[{\"id\":\"csBCiDglCE\",\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"id\":\"YHCQG3wAGv\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Creator\",\"col\":3}},{\"id\":\"xit0dg7KvY\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM\",\"col\":3}},{\"id\":\"LRhGV9GAov\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Plan\",\"col\":3}},{\"id\":\"69KKosI6Hg\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order\",\"col\":3}},{\"id\":\"PwndxuIpB3\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Job Card\",\"col\":3}},{\"id\":\"OaiDqTT03Y\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"col\":3}},{\"id\":\"OtMcArFRa5\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Stock Report\",\"col\":3}},{\"id\":\"76yYsI5imF\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Planning Report\",\"col\":3}},{\"id\":\"PIQJYZOMnD\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Learn Manufacturing\",\"col\":3}},{\"id\":\"bN_6tHS-Ct\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"yVEFZMqVwd\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"rwrmsTI58-\",\"type\":\"card\",\"data\":{\"card_name\":\"Production\",\"col\":4}},{\"id\":\"6dnsyX-siZ\",\"type\":\"card\",\"data\":{\"card_name\":\"Bill of Materials\",\"col\":4}},{\"id\":\"CIq-v5f5KC\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"8RRiQeYr0G\",\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"id\":\"Pu8z7-82rT\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]", + "content": "[{\"id\":\"csBCiDglCE\",\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"id\":\"YHCQG3wAGv\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Creator\",\"col\":3}},{\"id\":\"xit0dg7KvY\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM\",\"col\":3}},{\"id\":\"LRhGV9GAov\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Plan\",\"col\":3}},{\"id\":\"69KKosI6Hg\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order\",\"col\":3}},{\"id\":\"PwndxuIpB3\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Job Card\",\"col\":3}},{\"id\":\"Bw3jwRMiei\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Plant Floor\",\"col\":3}},{\"id\":\"4hPVRQke_x\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Visual Plant Floor\",\"col\":3}},{\"id\":\"OaiDqTT03Y\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"col\":3}},{\"id\":\"OtMcArFRa5\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Stock Report\",\"col\":3}},{\"id\":\"76yYsI5imF\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Planning Report\",\"col\":3}},{\"id\":\"PIQJYZOMnD\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Learn Manufacturing\",\"col\":3}},{\"id\":\"bN_6tHS-Ct\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"yVEFZMqVwd\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"rwrmsTI58-\",\"type\":\"card\",\"data\":{\"card_name\":\"Production\",\"col\":4}},{\"id\":\"6dnsyX-siZ\",\"type\":\"card\",\"data\":{\"card_name\":\"Bill of Materials\",\"col\":4}},{\"id\":\"CIq-v5f5KC\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"8RRiQeYr0G\",\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"id\":\"Pu8z7-82rT\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]", "creation": "2020-03-02 17:11:37.032604", "custom_blocks": [], "docstatus": 0, @@ -316,7 +316,7 @@ "type": "Link" } ], - "modified": "2023-08-08 22:28:39.633891", + "modified": "2023-11-30 15:21:14.577990", "modified_by": "Administrator", "module": "Manufacturing", "name": "Manufacturing", @@ -336,6 +336,14 @@ "type": "URL", "url": "https://frappe.school/courses/manufacturing?utm_source=in_app" }, + { + "color": "Grey", + "doc_view": "List", + "label": "Plant Floor", + "link_to": "Plant Floor", + "stats_filter": "[]", + "type": "DocType" + }, { "color": "Grey", "doc_view": "List", @@ -343,6 +351,13 @@ "link_to": "BOM Creator", "type": "DocType" }, + { + "color": "Grey", + "doc_view": "List", + "label": "Visual Plant Floor", + "link_to": "visual-plant-floor", + "type": "Page" + }, { "color": "Grey", "doc_view": "List", diff --git a/erpnext/public/js/erpnext.bundle.js b/erpnext/public/js/erpnext.bundle.js index dee9a06f0524..b847e5729f5f 100644 --- a/erpnext/public/js/erpnext.bundle.js +++ b/erpnext/public/js/erpnext.bundle.js @@ -5,6 +5,8 @@ import "./sms_manager"; import "./utils/party"; import "./controllers/stock_controller"; import "./payment/payments"; +import "./templates/visual_plant_floor_template.html"; +import "./plant_floor_visual/visual_plant"; import "./controllers/taxes_and_totals"; import "./controllers/transaction"; import "./templates/item_selector.html"; diff --git a/erpnext/public/js/plant_floor_visual/visual_plant.js b/erpnext/public/js/plant_floor_visual/visual_plant.js new file mode 100644 index 000000000000..b1d120fd9343 --- /dev/null +++ b/erpnext/public/js/plant_floor_visual/visual_plant.js @@ -0,0 +1,157 @@ +class VisualPlantFloor { + constructor({wrapper, skip_filters=false, plant_floor=null}, page=null) { + this.wrapper = wrapper; + this.plant_floor = plant_floor; + this.skip_filters = skip_filters; + + this.make(); + if (!this.skip_filters) { + this.page = page; + this.add_filter(); + this.prepare_menu(); + } + } + + make() { + this.wrapper.append(` +
+
+
+
+
+
+ `); + + if (!this.skip_filters) { + this.filter_wrapper = this.wrapper.find('.plant-floor-filter'); + this.visualization_wrapper = this.wrapper.find('.plant-floor-visualization'); + } else if(this.plant_floor) { + this.prepare_data(); + } + } + + prepare_data() { + frappe.call({ + method: 'erpnext.manufacturing.doctype.workstation.workstation.get_workstations', + args: { + plant_floor: this.plant_floor, + }, + callback: (r) => { + this.workstations = r.message; + this.render_workstations(); + } + }); + } + + add_filter() { + this.plant_floor = frappe.ui.form.make_control({ + df: { + fieldtype: 'Link', + options: 'Plant Floor', + fieldname: 'plant_floor', + label: __('Plant Floor'), + reqd: 1, + onchange: () => { + this.render_plant_visualization(); + } + }, + parent: this.filter_wrapper, + render_input: true, + }); + + this.plant_floor.$wrapper.addClass('form-column col-sm-2'); + + this.workstation_type = frappe.ui.form.make_control({ + df: { + fieldtype: 'Link', + options: 'Workstation Type', + fieldname: 'workstation_type', + label: __('Machine Type'), + onchange: () => { + this.render_plant_visualization(); + } + }, + parent: this.filter_wrapper, + render_input: true, + }); + + this.workstation_type.$wrapper.addClass('form-column col-sm-2'); + + this.workstation = frappe.ui.form.make_control({ + df: { + fieldtype: 'Link', + options: 'Workstation', + fieldname: 'workstation', + label: __('Machine'), + onchange: () => { + this.render_plant_visualization(); + }, + get_query: () => { + if (this.workstation_type.get_value()) { + return { + filters: { + 'workstation_type': this.workstation_type.get_value() || '' + } + } + } + } + }, + parent: this.filter_wrapper, + render_input: true, + }); + + this.workstation.$wrapper.addClass('form-column col-sm-2'); + + this.workstation_status = frappe.ui.form.make_control({ + df: { + fieldtype: 'Select', + options: '\nProduction\nOff\nIdle\nProblem\nMaintenance\nSetup', + fieldname: 'workstation_status', + label: __('Status'), + onchange: () => { + this.render_plant_visualization(); + }, + }, + parent: this.filter_wrapper, + render_input: true, + }); + } + + render_plant_visualization() { + let plant_floor = this.plant_floor.get_value(); + + if (plant_floor) { + frappe.call({ + method: 'erpnext.manufacturing.doctype.workstation.workstation.get_workstations', + args: { + plant_floor: plant_floor, + workstation_type: this.workstation_type.get_value(), + workstation: this.workstation.get_value(), + workstation_status: this.workstation_status.get_value() + }, + callback: (r) => { + this.workstations = r.message; + this.render_workstations(); + } + }); + } + } + + render_workstations() { + console.log(this.wrapper.find('.plant-floor-container')) + this.wrapper.find('.plant-floor-container').empty(); + let template = frappe.render_template("visual_plant_floor_template", { + workstations: this.workstations + }); + + $(template).appendTo(this.wrapper.find('.plant-floor-container')); + } + + prepare_menu() { + this.page.add_menu_item(__('Refresh'), () => { + this.render_plant_visualization(); + }); + } +} + +frappe.ui.VisualPlantFloor = VisualPlantFloor; \ No newline at end of file diff --git a/erpnext/public/js/templates/visual_plant_floor_template.html b/erpnext/public/js/templates/visual_plant_floor_template.html new file mode 100644 index 000000000000..2e67085c0221 --- /dev/null +++ b/erpnext/public/js/templates/visual_plant_floor_template.html @@ -0,0 +1,19 @@ +{% $.each(workstations, (idx, row) => { %} +
+
+ +
+
+

{{row.status}}

+
{{row.workstation_name}}
+
+
+{% }); %} \ No newline at end of file diff --git a/erpnext/public/scss/erpnext.scss b/erpnext/public/scss/erpnext.scss index 8ab5973debdb..ef09854c08be 100644 --- a/erpnext/public/scss/erpnext.scss +++ b/erpnext/public/scss/erpnext.scss @@ -490,3 +490,54 @@ body[data-route="pos"] { .exercise-col { padding: 10px; } + +.plant-floor, .workstation-wrapper, .workstation-card p { + border-radius: var(--border-radius-md); + border: 1px solid var(--border-color); + box-shadow: none; + background-color: var(--card-bg); + position: relative; +} + +.plant-floor { + padding-bottom: 25px; +} + +.plant-floor-filter { + padding-top: 10px; + display: flex; + flex-wrap: wrap; +} + +.plant-floor-container { + padding-top: 10px; + display: grid; + grid-template-columns: repeat(6,minmax(0,1fr)); + gap: var(--margin-xl); +} + +@media screen and (max-width: 620px) { + .plant-floor-container { + grid-template-columns: repeat(2,minmax(0,1fr)); + } +} + +.plant-floor-container .workstation-card { + padding: 5px; +} + +.plant-floor-container .workstation-image-link { + width: 100%; + font-size: 50px; + margin: var(--margin-sm); + min-height: 11rem; +} + +.workstation-abbr { + display: flex; + background-color: var(--control-bg); + height:100%; + width:100%; + align-items: center; + justify-content: center; +} \ No newline at end of file