diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 23650b687369..079350b63b21 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -955,6 +955,14 @@ def set_status(self, update_status=False): 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) + def set_wip_warehouse(self): if not self.wip_warehouse: self.wip_warehouse = frappe.db.get_single_value( @@ -1035,6 +1043,12 @@ def is_work_order_closed(self): return False + def update_status_in_workstation(self, status): + if not self.workstation: + return + + frappe.db.set_value("Workstation", self.workstation, "status", status) + @frappe.whitelist() def make_time_log(args): 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..67e5acd9da80 --- /dev/null +++ b/erpnext/manufacturing/doctype/plant_floor/plant_floor.js @@ -0,0 +1,256 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on("Plant Floor", { + setup(frm) { + frm.trigger("setup_queries"); + }, + + setup_queries(frm) { + frm.set_query("warehouse", (doc) => { + if (!doc.company) { + frappe.throw(__("Please select Company first")); + } + + return { + filters: { + "is_group": 0, + "company": doc.company + } + } + }); + }, + + refresh(frm) { + frm.trigger('prepare_stock_dashboard') + frm.trigger('prepare_workstation_dashboard') + }, + + prepare_workstation_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, + }); + }, + + prepare_stock_dashboard(frm) { + if (!frm.doc.warehouse) { + return; + } + + let wrapper = $(frm.fields_dict["stock_summary"].wrapper); + wrapper.empty(); + + frappe.visual_stock = new VisualStock({ + wrapper: wrapper, + frm: frm, + }); + }, +}); + + +class VisualStock { + constructor(opts) { + Object.assign(this, opts); + this.make(); + } + + make() { + this.prepare_filters(); + this.prepare_stock_summary({ + start:0 + }); + } + + prepare_filters() { + this.wrapper.append(` +
+
+ +
+
+ `); + + this.item_filter = frappe.ui.form.make_control({ + df: { + fieldtype: "Link", + fieldname: "item_code", + placeholder: __("Item"), + options: "Item", + onchange: () => this.prepare_stock_summary({ + start:0, + item_code: this.item_filter.value + }) + }, + parent: this.wrapper.find('.filter-section'), + render_input: true, + }); + + this.item_filter.$wrapper.addClass('form-column col-sm-3'); + this.item_filter.$wrapper.find('.clearfix').hide(); + + this.item_group_filter = frappe.ui.form.make_control({ + df: { + fieldtype: "Link", + fieldname: "item_group", + placeholder: __("Item Group"), + options: "Item Group", + change: () => this.prepare_stock_summary({ + start:0, + item_group: this.item_group_filter.value + }) + }, + parent: this.wrapper.find('.filter-section'), + render_input: true, + }); + + this.item_group_filter.$wrapper.addClass('form-column col-sm-3'); + this.item_group_filter.$wrapper.find('.clearfix').hide(); + } + + prepare_stock_summary(args) { + let {start, item_code, item_group} = args; + + this.get_stock_summary(start, item_code, item_group).then(stock_summary => { + this.wrapper.find('.stock-summary-container').remove(); + this.wrapper.append(`
`); + this.stock_summary = stock_summary.message; + this.render_stock_summary(); + this.bind_events(); + }); + } + + async get_stock_summary(start, item_code, item_group) { + let stock_summary = await frappe.call({ + method: "erpnext.manufacturing.doctype.plant_floor.plant_floor.get_stock_summary", + args: { + warehouse: this.frm.doc.warehouse, + start: start, + item_code: item_code, + item_group: item_group + } + }); + + return stock_summary; + } + + render_stock_summary() { + let template = frappe.render_template("stock_summary_template", { + stock_summary: this.stock_summary + }); + + this.wrapper.find('.stock-summary-container').append(template); + } + + bind_events() { + this.wrapper.find('.btn-add').click((e) => { + this.item_code = decodeURI($(e.currentTarget).attr('data-item-code')); + + this.make_stock_entry([ + { + label: __("For Item"), + fieldname: "item_code", + fieldtype: "Data", + read_only: 1, + default: this.item_code + }, + { + label: __("Quantity"), + fieldname: "qty", + fieldtype: "Float", + reqd: 1 + } + ], __("Add Stock"), "Material Receipt") + }); + + this.wrapper.find('.btn-move').click((e) => { + this.item_code = decodeURI($(e.currentTarget).attr('data-item-code')); + + this.make_stock_entry([ + { + label: __("For Item"), + fieldname: "item_code", + fieldtype: "Data", + read_only: 1, + default: this.item_code + }, + { + label: __("Quantity"), + fieldname: "qty", + fieldtype: "Float", + reqd: 1 + }, + { + label: __("To Warehouse"), + fieldname: "to_warehouse", + fieldtype: "Link", + options: "Warehouse", + reqd: 1, + get_query: () => { + return { + filters: { + "is_group": 0, + "company": this.frm.doc.company + } + } + } + } + ], __("Move Stock"), "Material Transfer") + }); + } + + make_stock_entry(fields, title, stock_entry_type) { + frappe.prompt(fields, + (values) => { + this.values = values; + this.stock_entry_type = stock_entry_type; + this.update_values(); + + this.frm.call({ + method: "make_stock_entry", + doc: this.frm.doc, + args: { + kwargs: this.values, + }, + callback: (r) => { + if (!r.exc) { + var doc = frappe.model.sync(r.message); + frappe.set_route("Form", r.message.doctype, r.message.name); + } + } + }) + }, __(title), __("Create") + ); + } + + update_values() { + if (!this.values.qty) { + frappe.throw(__("Quantity is required")); + } + + let from_warehouse = ""; + let to_warehouse = ""; + + if (this.stock_entry_type == "Material Receipt") { + to_warehouse = this.frm.doc.warehouse; + } else { + from_warehouse = this.frm.doc.warehouse; + to_warehouse = this.values.to_warehouse; + } + + this.values = { + ...this.values, + ...{ + "company": this.frm.doc.company, + "item_code": this.item_code, + "from_warehouse": from_warehouse, + "to_warehouse": to_warehouse, + "purpose": this.stock_entry_type, + } + } + } +} \ No newline at end of file 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..be0052c47bf5 --- /dev/null +++ b/erpnext/manufacturing/doctype/plant_floor/plant_floor.json @@ -0,0 +1,97 @@ +{ + "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", + "stock_summary_tab", + "stock_summary", + "details_tab", + "column_break_mvbx", + "floor_name", + "company", + "warehouse" + ], + "fields": [ + { + "fieldname": "floor_name", + "fieldtype": "Data", + "label": "Floor Name", + "unique": 1 + }, + { + "depends_on": "eval:!doc.__islocal", + "fieldname": "workstations_tab", + "fieldtype": "Tab Break", + "label": "Workstations" + }, + { + "fieldname": "plant_dashboard", + "fieldtype": "HTML", + "label": "Plant Dashboard" + }, + { + "fieldname": "details_tab", + "fieldtype": "Tab Break", + "label": "Floor" + }, + { + "fieldname": "column_break_mvbx", + "fieldtype": "Column Break" + }, + { + "fieldname": "warehouse", + "fieldtype": "Link", + "label": "Warehouse", + "options": "Warehouse" + }, + { + "depends_on": "eval:!doc.__islocal && doc.warehouse", + "fieldname": "stock_summary_tab", + "fieldtype": "Tab Break", + "label": "Stock Summary" + }, + { + "fieldname": "stock_summary", + "fieldtype": "HTML", + "label": "Stock Summary" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2024-01-30 11:59:07.508535", + "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..d30b7d104990 --- /dev/null +++ b/erpnext/manufacturing/doctype/plant_floor/plant_floor.py @@ -0,0 +1,129 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe.model.document import Document +from frappe.query_builder import Order +from frappe.utils import get_link_to_form, nowdate, nowtime + + +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 + + company: DF.Link | None + floor_name: DF.Data | None + warehouse: DF.Link | None + # end: auto-generated types + + @frappe.whitelist() + def make_stock_entry(self, kwargs): + if isinstance(kwargs, str): + kwargs = frappe.parse_json(kwargs) + + if isinstance(kwargs, dict): + kwargs = frappe._dict(kwargs) + + stock_entry = frappe.new_doc("Stock Entry") + stock_entry.update( + { + "company": kwargs.company, + "from_warehouse": kwargs.from_warehouse, + "to_warehouse": kwargs.to_warehouse, + "purpose": kwargs.purpose, + "stock_entry_type": kwargs.purpose, + "posting_date": nowdate(), + "posting_time": nowtime(), + "items": self.get_item_details(kwargs), + } + ) + + stock_entry.set_missing_values() + + return stock_entry + + def get_item_details(self, kwargs) -> list[dict]: + item_details = frappe.db.get_value( + "Item", kwargs.item_code, ["item_name", "stock_uom", "item_group", "description"], as_dict=True + ) + item_details.update( + { + "qty": kwargs.qty, + "uom": item_details.stock_uom, + "item_code": kwargs.item_code, + "conversion_factor": 1, + "s_warehouse": kwargs.from_warehouse, + "t_warehouse": kwargs.to_warehouse, + } + ) + + return [item_details] + + +@frappe.whitelist() +def get_stock_summary(warehouse, start=0, item_code=None, item_group=None): + stock_details = get_stock_details( + warehouse, start=start, item_code=item_code, item_group=item_group + ) + + max_count = 0.0 + for d in stock_details: + d.actual_or_pending = ( + d.projected_qty + + d.reserved_qty + + d.reserved_qty_for_production + + d.reserved_qty_for_sub_contract + ) + d.pending_qty = 0 + d.total_reserved = ( + d.reserved_qty + d.reserved_qty_for_production + d.reserved_qty_for_sub_contract + ) + if d.actual_or_pending > d.actual_qty: + d.pending_qty = d.actual_or_pending - d.actual_qty + + d.max_count = max(d.actual_or_pending, d.actual_qty, d.total_reserved, max_count) + max_count = d.max_count + d.item_link = get_link_to_form("Item", d.item_code) + + return stock_details + + +def get_stock_details(warehouse, start=0, item_code=None, item_group=None): + item_table = frappe.qb.DocType("Item") + bin_table = frappe.qb.DocType("Bin") + + query = ( + frappe.qb.from_(bin_table) + .inner_join(item_table) + .on(bin_table.item_code == item_table.name) + .select( + bin_table.item_code, + bin_table.actual_qty, + bin_table.projected_qty, + bin_table.reserved_qty, + bin_table.reserved_qty_for_production, + bin_table.reserved_qty_for_sub_contract, + bin_table.reserved_qty_for_production_plan, + bin_table.reserved_stock, + item_table.item_name, + item_table.item_group, + item_table.image, + ) + .where(bin_table.warehouse == warehouse) + .limit(20) + .offset(start) + .orderby(bin_table.actual_qty, order=Order.desc) + ) + + if item_code: + query = query.where(bin_table.item_code == item_code) + + if item_group: + query = query.where(item_table.item_group == item_group) + + return query.run(as_dict=True) diff --git a/erpnext/manufacturing/doctype/plant_floor/stock_summary_template.html b/erpnext/manufacturing/doctype/plant_floor/stock_summary_template.html new file mode 100644 index 000000000000..8824c9811ba4 --- /dev/null +++ b/erpnext/manufacturing/doctype/plant_floor/stock_summary_template.html @@ -0,0 +1,61 @@ +{% $.each(stock_summary, (idx, row) => { %} +
+
+ {% if(row.image) { %} + + {% } else { %} +
{{frappe.get_abbr(row.item_code, 2)}}
+ {% } %} +
+
+ {% if (row.item_code === row.item_name) { %} + {{row.item_link}} + {% } else { %} + {{row.item_link}} +

+ {{row.item_name}} +

+ {% } %} + +
+
+ {{ frappe.format(row.actual_qty, { fieldtype: "Float"})}} +
+
+ {{ frappe.format(row.reserved_stock, { fieldtype: "Float"})}} +
+
+ + + {{ row.total_reserved }} + + + + + + + + {{ row.actual_qty }} {{ (row.pending_qty > 0) ? ("(" + row.pending_qty+ ")") : "" }} + + + + + {% if row.pending_qty > 0 %} + + + {% endif %} + + + +
+
+ +
+
+ +
+
+{% }); %} \ No newline at end of file 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..e3ad3fe3cce5 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,243 @@ 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.job_cards = r.message; + this.render_job_cards(); + } + } + }); + } + + render_job_cards() { + let template = frappe.render_template("workstation_job_card", { + data: this.job_cards + }); + + this.$wrapper.html(template); + this.prepare_timer(); + this.toggle_job_card(); + this.bind_events(); + } + + toggle_job_card() { + 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")) + }); + } + + bind_events() { + this.$wrapper.find(".make-material-request").on("click", (e) => { + let job_card = $(e.currentTarget).attr("job-card"); + this.make_material_request(job_card); + }); + + this.$wrapper.find(".btn-start").on("click", (e) => { + let job_card = $(e.currentTarget).attr("job-card"); + this.start_job(job_card); + }); + + 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); + }); + } + + start_job(job_card) { + let me = this; + 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', + } + ], 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(); + } + } + }); + }, __("Enter Value"), __("Start Job")); + } + + complete_job(job_card, qty_to_manufacture) { + let me = this; + let fields = [ + { + fieldtype: 'Float', + label: __('Completed Quantity'), + fieldname: 'qty', + reqd: 1, + default: flt(qty_to_manufacture || 0) + }, + { + fieldtype: 'Datetime', + label: __('End Time'), + fieldname: 'end_time', + default: frappe.datetime.now_datetime() + }, + ]; + + frappe.prompt(fields, data => { + if (data.qty <= 0) { + frappe.throw(__("Quantity should be greater than 0")); + } + + 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(); + } + } + }); + }, __("Enter Value"), __("Submit")); + } + + make_material_request(job_card) { + frappe.call({ + method: "erpnext.manufacturing.doctype.job_card.job_card.make_material_request", + args: { + source_name: job_card, + }, + callback: (r) => { + if (r.message) { + var doc = frappe.model.sync(r.message)[0]; + frappe.set_route("Form", doc.doctype, doc.name); + } + } + }); + } + + 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) { + this.updateStopwatch(data); + } else { + this.initialiseTimer(data); + } + } + }); + } + + update_job_card_details() { + let 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)", + } + + this.job_cards.forEach((data) => { + let job_card_selector = this.$wrapper.find(` + [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"); + } + }); + } + + initialiseTimer(data) { + setInterval(() => { + data._current_time += 1; + this.updateStopwatch(data); + }, 1000); + } + + updateStopwatch(data) { + let increment = data._current_time; + let hours = Math.floor(increment / 3600); + let minutes = Math.floor((increment - (hours * 3600)) / 60); + let seconds = cint(increment - (hours * 3600) - (minutes * 60)); + + let job_card_selector = `[data-job-card='${data.name}']` + let timer_selector = this.$wrapper.find(job_card_selector) + + $(timer_selector).find(".hours").text(hours < 10 ? ("0" + hours.toString()) : hours.toString()); + $(timer_selector).find(".minutes").text(minutes < 10 ? ("0" + minutes.toString()) : minutes.toString()); + $(timer_selector).find(".seconds").text(seconds < 10 ? ("0" + seconds.toString()) : seconds.toString()); + } + + get_current_time(data) { + let current_time = 0.0; + data.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 += this.get_seconds_diff(d.to_time, d.from_time); + } + } else { + current_time += this.get_seconds_diff(frappe.datetime.now_datetime(), d.from_time); + } + }); + + return current_time; + } + + get_seconds_diff(d1, d2) { + return moment(d1).diff(d2, "seconds"); + } +} \ 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 0a247fc431d0..90aa993d7e2c 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 = ( @@ -143,6 +164,141 @@ def validate_workstation_holiday(self, schedule_date, skip_holiday_list_check=Fa return schedule_date + @frappe.whitelist() + def start_job(self, job_card, from_time, employee): + doc = frappe.get_doc("Job Card", job_card) + doc.append("time_logs", {"from_time": from_time, "employee": employee}) + doc.save(ignore_permissions=True) + + return doc + + @frappe.whitelist() + def complete_job(self, job_card, qty, to_time): + doc = frappe.get_doc("Job Card", job_card) + for row in doc.time_logs: + if not row.to_time: + row.to_time = to_time + row.time_in_mins = time_diff_in_hours(row.to_time, row.from_time) / 60 + row.completed_qty = qty + + doc.save(ignore_permissions=True) + doc.submit() + + return doc + + +@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", + "transferred_qty", + "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) + time_logs = get_time_logs(job_cards) + + allow_excess_transfer = frappe.db.get_single_value( + "Manufacturing Settings", "job_card_excess_transfer" + ) + + 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, []) + 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 + + 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)", + } + + return color_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 + + +def get_time_logs(job_cards): + time_logs = {} + + data = frappe.get_all( + "Job Card Time Log", + fields=[ + "parent", + "name", + "employee", + "from_time", + "to_time", + "time_in_mins", + ], + filters={"parent": ["in", job_cards], "parentfield": "time_logs"}, + order_by="parent, idx", + ) + + for row in data: + time_logs.setdefault(row.parent, []).append(row) + + return time_logs + @frappe.whitelist() def get_default_holiday_list(): @@ -201,3 +357,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..97707855db0c --- /dev/null +++ b/erpnext/manufacturing/doctype/workstation/workstation_job_card.html @@ -0,0 +1,125 @@ + + +
+{% $.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..d2520d6b7eb6 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\":\"Ubj6zXcmIQ\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Plant Floor\",\"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\":\"OaiDqTT03Y\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"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": "2024-01-30 21:49:58.577218", "modified_by": "Administrator", "module": "Manufacturing", "name": "Manufacturing", @@ -336,6 +336,13 @@ "type": "URL", "url": "https://frappe.school/courses/manufacturing?utm_source=in_app" }, + { + "color": "Grey", + "doc_view": "List", + "label": "Plant Floor", + "link_to": "Plant Floor", + "type": "DocType" + }, { "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..8cd73adc5745 --- /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.wrapper.find('.plant-floor').css('border', 'none'); + 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() { + 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..1626b7c894d3 100644 --- a/erpnext/public/scss/erpnext.scss +++ b/erpnext/public/scss/erpnext.scss @@ -490,3 +490,53 @@ 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 { + 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: 9rem; +} + +.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