feat: visual plant floor

(cherry picked from commit 68c997aa067f342f6e432e9f9c84416a0a4cc1bf)
This commit is contained in:
Rohit Waghchaure 2023-10-06 17:45:26 +05:30 committed by Mergify
parent b126720826
commit 75bd1e6b65
19 changed files with 896 additions and 151 deletions

View File

@ -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,
});
},
});

View File

@ -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": []
}

View File

@ -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

View File

@ -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

View File

@ -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"))
});
}
}

View File

@ -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",

View File

@ -11,7 +11,11 @@ from frappe.utils import (
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 @@ class Workstation(Document):
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

View File

@ -0,0 +1,97 @@
<style>
.job-card-link {
min-height: 100px;
}
.section-head-job-card {
margin-bottom: 0px;
padding-bottom: 0px;
}
</style>
<div style = "max-height: 400px; overflow-y: auto;">
{% $.each(data, (idx, d) => { %}
<div class="row form-dashboard-section job-card-link form-links border-gray-200" data-name="{{d.name}}">
<div class="section-head section-head-job-card">
{{ d.operation }} - {{ d.production_item }}
<span class="ml-2 collapse-indicator-job mb-1" style="">
{{frappe.utils.icon("es-line-down", "sm", "mb-1")}}
</span>
</div>
<div class="row form-section" style="width:100%;margin-bottom:10px">
<div class="form-column col-sm-3">
<div class="frappe-control" title="{{__('Job Card')}}" style="text-decoration:underline">
{{ d.job_card_link }}
</div>
<div class="frappe-control" title="{{__('Work Order')}}" style="text-decoration:underline">
{{ d.work_order_link }}
</div>
</div>
<div class="form-column col-sm-3">
<div class="frappe-control" title="{{__('Expected Start Date')}}">
{{ frappe.format(d.expected_start_date, { fieldtype: 'Datetime' }) }}
</div>
</div>
<div class="form-column col-sm-2">
<div class="frappe-control" title="{{__('Status')}}" style="background:{{d.status_color}};text-align:center;border-radius:var(--border-radius-full)">
{{ d.status }}
</div>
</div>
<div class="form-column col-sm-2">
<div class="frappe-control" title="{{__('Qty to Manufacture')}}">
<div class="progress" title = "{{d.progress_title}}">
<div class="progress-bar progress-bar-success" style="width: {{d.progress_percent}}%">
</div>
</div>
</div>
<div class="frappe-control" style="text-align: center; font-size: 10px;">
{{ d.for_quantity }} / {{ d.total_completed_qty }}
</div>
</div>
</div>
<div class="section-body section-body-job-card form-section hide">
<hr>
{{ __("Raw Materials") }}
{% if(d.raw_materials) { %}
<table class="table table-bordered table-condensed">
<thead>
<tr>
<th style="width: 5%" class="table-sr">Sr</th>
<th style="width: 15%">{{ __("Item") }}</th>
<th style="width: 15%">{{ __("Warehouse") }}</th>
<th style="width: 10%">{{__("UOM")}}</th>
<th style="width: 15%">{{__("Item Group")}}</th>
<th style="width: 20%" >{{__("Required Qty")}}</th>
<th style="width: 20%" >{{__("Transferred Qty")}}</th>
</tr>
</thead>
<tbody>
{% $.each(d.raw_materials, (row_index, child_row) => { %}
<tr>
<td class="table-sr">{{ row_index+1 }}</td>
{% if(child_row.item_code === child_row.item_name) { %}
<td>{{ child_row.item_code }}</td>
{% } else { %}
<td>{{ child_row.item_code }}: {{child_row.item_name}}</td>
{% } %}
<td>{{ child_row.source_warehouse }}</td>
<td>{{ child_row.uom }}</td>
<td>{{ child_row.item_group }}</td>
<td>{{ child_row.required_qty }}</td>
<td>{{ child_row.transferred_qty }}</td>
</tr>
{% }); %}
</tbody>
{% } %}
</table>
</div>
</div>
{% }); %}
</div>

View File

@ -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];
}
};

View File

@ -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": []
}

View File

@ -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
);
}

View File

@ -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"
}

View File

@ -1,6 +1,6 @@
{
"charts": [],
"content": "[{\"id\":\"csBCiDglCE\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"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\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"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\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"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\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"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",

View File

@ -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";

View File

@ -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(`
<div class="plant-floor">
<div class="plant-floor-filter">
</div>
<div class="plant-floor-container col-sm-12">
</div>
</div>
`);
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;

View File

@ -0,0 +1,19 @@
{% $.each(workstations, (idx, row) => { %}
<div class="workstation-wrapper">
<div class="workstation-image">
<div class="flex items-center justify-center h-32 border-b-grey text-6xl text-grey-100">
<a class="workstation-image-link" href="{{row.workstation_link}}">
{% if(row.status_image) { %}
<img class="workstation-image-cls" src="{{row.status_image}}">
{% } else { %}
<div class="workstation-image-cls workstation-abbr">{{frappe.get_abbr(row.name, 2)}}</div>
{% } %}
</a>
</div>
</div>
<div class="workstation-card text-center">
<p style="background-color:{{row.background_color}};color:#fff">{{row.status}}</p>
<div>{{row.workstation_name}}</div>
</div>
</div>
{% }); %}

View File

@ -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;
}