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