fix: plant floor design changes

This commit is contained in:
Rohit Waghchaure
2024-09-25 14:25:42 +05:30
parent e4e96d2a44
commit 3c1cdeb3ec
13 changed files with 213 additions and 152 deletions

View File

@@ -90,69 +90,10 @@ frappe.ui.form.on("BOM Creator", {
},
{ fieldtype: "Section Break" },
{
label: __("Track Operations"),
fieldtype: "Check",
fieldname: "track_operations",
onchange: (r) => {
let track_operations = dialog.get_value("track_operations");
if (r.type === "input" && !track_operations) {
dialog.set_value("track_semi_finished_goods", 0);
}
},
},
{ fieldtype: "Column Break" },
{
label: __("Track Semi Finished Goods"),
fieldtype: "Check",
fieldname: "track_semi_finished_goods",
depends_on: "eval:doc.track_operations",
},
{
fieldtype: "Section Break",
label: __("Final Product Operation"),
depends_on: "eval:doc.track_semi_finished_goods",
},
{
label: __("Operation"),
label: __("Routing"),
fieldtype: "Link",
fieldname: "operation",
options: "Operation",
default: "Assembly",
mandatory_depends_on: "eval:doc.track_semi_finished_goods",
depends_on: "eval:doc.track_semi_finished_goods",
},
{
label: __("Operation Time (in mins)"),
fieldtype: "Float",
fieldname: "operation_time",
mandatory_depends_on: "eval:doc.track_semi_finished_goods",
depends_on: "eval:doc.track_semi_finished_goods",
},
{ fieldtype: "Column Break" },
{
label: __("Workstation Type"),
fieldtype: "Link",
fieldname: "workstation_type",
options: "Workstation",
depends_on: "eval:doc.track_semi_finished_goods",
},
{
label: __("Workstation"),
fieldtype: "Link",
fieldname: "workstation",
options: "Workstation",
depends_on: "eval:doc.track_semi_finished_goods",
get_query() {
let workstation_type = dialog.get_value("workstation_type");
if (workstation_type) {
return {
filters: {
workstation_type: dialog.get_value("workstation_type"),
},
};
}
},
fieldname: "routing",
options: "Routing",
},
],
primary_action_label: __("Create"),

View File

@@ -19,6 +19,8 @@
"qty",
"project",
"uom",
"section_break_xvld",
"routing",
"raw_materials_tab",
"currency_detail",
"rm_cost_as_per",
@@ -393,6 +395,17 @@
{
"fieldname": "column_break_buha",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_xvld",
"fieldtype": "Section Break",
"label": "Operations Routing"
},
{
"fieldname": "routing",
"fieldtype": "Link",
"label": "Routing",
"options": "Routing"
}
],
"hide_toolbar": 1,
@@ -404,7 +417,7 @@
"link_fieldname": "bom_creator"
}
],
"modified": "2024-09-20 09:05:52.945112",
"modified": "2024-09-26 17:07:32.111198",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Creator",

View File

@@ -28,6 +28,7 @@ BOM_ITEM_FIELDS = [
"stock_uom",
"conversion_factor",
"do_not_explode",
"operation",
]
@@ -64,6 +65,7 @@ class BOMCreator(Document):
raw_material_cost: DF.Currency
remarks: DF.TextEditor | None
rm_cost_as_per: DF.Literal["Valuation Rate", "Last Purchase Rate", "Price List"]
routing: DF.Link | None
set_rate_based_on_warehouse: DF.Check
skip_material_transfer: DF.Check
source_warehouse: DF.Link | None
@@ -409,6 +411,11 @@ class BOMCreator(Document):
},
)
elif row.item_code == self.item_code and self.routing:
bom.routing = self.routing
bom.with_operations = 1
bom.transfer_material_against = "Work Order"
for field in BOM_FIELDS:
if self.get(field):
bom.set(field, self.get(field))
@@ -595,6 +602,7 @@ def add_sub_assembly(**kwargs):
{
"item_code": row.item_code,
"qty": row.qty,
"operation": row.operation,
"fg_item": bom_item.item_code,
"uom": item_info.stock_uom,
"fg_reference_id": name,

View File

@@ -40,6 +40,7 @@ frappe.ui.form.on("Plant Floor", {
refresh(frm) {
frm.trigger("prepare_stock_dashboard");
frm.trigger("prepare_workstation_dashboard");
frm.trigger("update_realtime_status");
if (!frm.is_new()) {
frm.trigger("add_workstation");
@@ -58,6 +59,12 @@ frappe.ui.form.on("Plant Floor", {
});
},
update_realtime_status(frm) {
frappe.realtime.on("update_workstation_status", (data) => {
frappe.visual_plant_floor.update_status(data);
});
},
prepare_stock_dashboard(frm) {
if (!frm.doc.warehouse) {
return;

View File

@@ -72,7 +72,7 @@
"hide_toolbar": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-09-19 18:06:36.481625",
"modified": "2024-09-25 10:27:41.139634",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Plant Floor",

View File

@@ -190,11 +190,11 @@ class WorkstationDashboard {
setup_menu_actions() {
let me = this;
this.job_cards.forEach((data) => {
me.menu_actions = me.$wrapper.find(`.menu-actions[data-job-card='${data.name}']`);
$(me.menu_actions).find(".btn-start").hide();
$(me.menu_actions).find(".btn-resume").hide();
$(me.menu_actions).find(".btn-pause").hide();
$(me.menu_actions).find(".btn-complete").hide();
me.menu_btns = me.$wrapper.find(`.job-card-link[data-name='${data.name}']`);
$(me.menu_btns).find(".btn-resume").hide();
$(me.menu_btns).find(".btn-pause").hide();
$(me.menu_btns).find(".btn-complete .btn").attr("disabled", true);
if (
data.for_quantity + data.process_loss_qty > data.total_completed_qty &&
@@ -203,15 +203,18 @@ class WorkstationDashboard {
!data.finished_good)
) {
if (!data.time_logs?.length) {
$(me.menu_actions).find(".btn-start").show();
$(me.menu_btns).find(".btn-start").show();
} else if (data.is_paused) {
$(me.menu_actions).find(".btn-resume").show();
$(me.menu_btns).find(".btn-start").hide();
$(me.menu_btns).find(".btn-resume").show();
} else if (data.for_quantity - data.manufactured_qty > 0) {
$(me.menu_btns).find(".btn-start").hide();
if (!data.is_paused) {
$(me.menu_actions).find(".btn-pause").show();
$(me.menu_btns).find(".btn-pause").show();
}
$(me.menu_actions).find(".btn-complete").show();
$(me.menu_btns).find(".btn-complete").show();
$(me.menu_btns).find(".btn-complete .btn").attr("disabled", false);
}
}
});
@@ -243,26 +246,26 @@ class WorkstationDashboard {
});
this.$wrapper.find(".btn-start").on("click", (e) => {
let job_card = $(e.currentTarget).closest("ul").attr("data-job-card");
let job_card = $(e.currentTarget).closest("div").attr("data-job-card");
this.start_job(job_card);
});
this.$wrapper.find(".btn-pause").on("click", (e) => {
let job_card = $(e.currentTarget).closest("ul").attr("data-job-card");
let job_card = $(e.currentTarget).closest("div").attr("data-job-card");
me.update_job_card(job_card, "pause_job", {
end_time: frappe.datetime.now_datetime(),
});
});
this.$wrapper.find(".btn-resume").on("click", (e) => {
let job_card = $(e.currentTarget).closest("ul").attr("data-job-card");
let job_card = $(e.currentTarget).closest("div").attr("data-job-card");
me.update_job_card(job_card, "resume_job", {
start_time: frappe.datetime.now_datetime(),
});
});
this.$wrapper.find(".btn-complete").on("click", (e) => {
let job_card = $(e.currentTarget).closest("ul").attr("data-job-card");
let job_card = $(e.currentTarget).closest("div").attr("data-job-card");
let for_quantity = $(e.currentTarget).attr("data-qty");
me.complete_job(job_card, for_quantity);
});
@@ -315,6 +318,12 @@ class WorkstationDashboard {
let me = this;
return [
{
label: __("Start Time"),
fieldname: "start_time",
fieldtype: "Datetime",
default: frappe.datetime.now_datetime(),
},
{
label: __("Employee"),
fieldname: "employee",
@@ -337,12 +346,6 @@ class WorkstationDashboard {
}
},
},
{
label: __("Start Time"),
fieldname: "start_time",
fieldtype: "Datetime",
default: frappe.datetime.now_datetime(),
},
{ fieldtype: "Section Break" },
{
label: __("Employees"),

View File

@@ -15,6 +15,7 @@
"workstation_name",
"workstation_type",
"plant_floor",
"disabled",
"column_break_3",
"production_capacity",
"warehouse",
@@ -240,13 +241,20 @@
"fieldname": "section_break_mqqv",
"fieldtype": "Section Break",
"hide_border": 1
},
{
"default": "0",
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disabled"
}
],
"hide_toolbar": 1,
"icon": "icon-wrench",
"idx": 1,
"image_field": "on_status_image",
"links": [],
"modified": "2024-06-20 14:17:13.806609",
"modified": "2024-09-26 13:41:12.279344",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Workstation",

View File

@@ -49,6 +49,7 @@ class Workstation(Document):
)
description: DF.Text | None
disabled: DF.Check
holiday_list: DF.Link | None
hour_rate: DF.Currency
hour_rate_consumable: DF.Currency
@@ -71,6 +72,11 @@ class Workstation(Document):
self.set_data_based_on_workstation_type()
self.set_hour_rate()
self.set_total_working_hours()
self.disabled_workstation()
def disabled_workstation(self):
if self.disabled:
self.status = "Off"
def set_total_working_hours(self):
self.total_working_hours = 0.0
@@ -124,6 +130,28 @@ class Workstation(Document):
self.validate_overlap_for_operation_timings()
self.update_bom_operation()
if self.plant_floor:
self.publish_workstation_status()
def publish_workstation_status(self):
if not self._doc_before_save:
return
if self._doc_before_save.get("status") == self.status:
return
data = get_workstations(plant_floor=self.plant_floor, workstation_name=self.name)[0]
color_map = get_color_map()
data["old_color"] = color_map.get(self._doc_before_save.get("status"), "red")
frappe.publish_realtime(
"update_workstation_status",
data,
doctype="Plant Floor",
docname=self.plant_floor,
)
def validate_overlap_for_operation_timings(self):
"""Check if there is no overlap in setting Workstation Operating Hours"""
for d in self.get("working_hours"):
@@ -238,10 +266,13 @@ def get_job_cards(workstation, job_card=None):
user_employee = frappe.db.get_value("Employee", {"user_id": frappe.session.user}, "name")
for row in jc_data:
if row.status == "Open":
row.status = "Not Started"
item_code = row.finished_good or row.production_item
row.fg_uom = frappe.get_cached_value("Item", item_code, "stock_uom")
row.status_color = get_status_color(row.status)
row.status_colour = get_status_color(row.status)
row.job_card_link = f"""
<a class="ellipsis" data-doctype="Job Card" data-name="{row.name}" href="/app/job-card/{row.name}" title="" data-original-title="{row.name}">{row.name}</a>
"""
@@ -423,8 +454,8 @@ def get_workstations(**kwargs):
_workstation.on_status_image,
_workstation.off_status_image,
)
.orderby(_workstation.workstation_type, _workstation.name)
.where(_workstation.plant_floor == kwargs.plant_floor)
.orderby(_workstation.creation, _workstation.workstation_type, _workstation.name)
.where((_workstation.plant_floor == kwargs.plant_floor) & (_workstation.disabled == 0))
)
if kwargs.workstation:
@@ -436,9 +467,28 @@ def get_workstations(**kwargs):
if kwargs.workstation_status:
query = query.where(_workstation.status == kwargs.workstation_status)
if kwargs.workstation_name:
query = query.where(_workstation.name == kwargs.workstation_name)
data = query.run(as_dict=True)
color_map = {
color_map = get_color_map()
for d in data:
d.workstation_name = get_link_to_form("Workstation", d.name)
d.status_image = d.on_status_image
d.workstation_off = ""
d.color = color_map.get(d.status, "red")
d.workstation_link = get_url_to_form("Workstation", d.name)
if d.status != "Production":
d.status_image = d.off_status_image
d.workstation_off = "workstation-off"
return data
def get_color_map():
return {
"Production": "green",
"Off": "gray",
"Idle": "gray",
@@ -447,16 +497,6 @@ def get_workstations(**kwargs):
"Setup": "blue",
}
for d in data:
d.workstation_name = get_link_to_form("Workstation", d.name)
d.status_image = d.on_status_image
d.color = color_map.get(d.status, "red")
d.workstation_link = get_url_to_form("Workstation", d.name)
if d.status != "Production":
d.status_image = d.off_status_image
return data
@frappe.whitelist()
def update_job_card(job_card, method, **kwargs):

View File

@@ -17,7 +17,7 @@
<div class="row form-dashboard-section job-card-link form-links border-gray-200" data-name="{{d.name}}">
<div class="row form-section" style="width:100%;margin-top:10px">
<div class="form-column col-sm-2">
<div class="frappe-control bold" data-doctype="Job Card" data-name="{{d.name}}" title="{{__('Job Card ID')}}">
<div class="frappe-control bold indicator {{d.status_colour}}" data-doctype="Job Card" data-name="{{d.name}}" title="{{__(d.status)}}">
{{ d.job_card_link }}
</div>
</div>
@@ -52,17 +52,30 @@
{% } %} -->
</div>
<div class="form-column col-sm-2">
<div class="frappe-control indicator-pill no-indicator-dot whitespace-nowrap job-card-status {{d.status_color}}" title="{{__('Status')}}">
{% if(d.status === "Open") { %}
{{__("Not Started")}}
{% } else { %}
{{ __(d.status) }}
{% } %}
</div>
<div class="form-column col-sm-1 btn-start" data-job-card="{{d.name}}">
<button class="btn btn-default">
<span class="menu-item-label" data-label="Start">{{ __('Start') }}</span>
</button>
</div>
<div class="form-column col-sm-1 btn-resume" data-job-card="{{d.name}}">
<button class="btn btn-default ">
<span class="menu-item-label" data-label="Start">{{ __('Resume') }}</span>
</button>
</div>
<div class="form-column col-sm-1 btn-pause" data-job-card="{{d.name}}">
<button class="btn btn-default">
<span class="menu-item-label" data-label="Start">{{ __('Pause') }}</span>
</button>
</div>
<div class="form-column col-sm-1 btn-complete" data-job-card="{{d.name}}" data-qty="{{d.for_quantity}}">
<button class="btn btn-default">
<span class="menu-item-label" data-label="Start">{{ __('Complete') }}</span>
</button>
</div>
<div class="form-column col-sm-1">
<div class="menu-btn-group">
<div class="menu-btn-group" style="margin-left: 15px;">
<button type="button" class="btn btn-default icon-btn" data-toggle="dropdown" aria-expanded="false" aria-label="{{ __("Menu") }}">
<span>
<span class="menu-btn-group-label">
@@ -74,26 +87,6 @@
</span>
</button>
<ul class="dropdown-menu dropdown-menu-right menu-actions" role="menu" data-job-card="{{d.name}}">
<li>
<a class="grey-link dropdown-item btn-start" href="#" onclick="return false;">
<span class="menu-item-label" data-label="Start">{{ __('Start') }}</span>
</a>
</li>
<li>
<a class="grey-link dropdown-item btn-resume" href="#" onclick="return false;">
<span class="menu-item-label" data-label="Resume">{{ __('Resume') }}</span>
</a>
</li>
<li>
<a class="grey-link dropdown-item btn-pause" href="#" onclick="return false;">
<span class="menu-item-label" data-label="Pause">{{ __('Pause') }}</span>
</a>
</li>
<li>
<a class="grey-link dropdown-item btn-complete" href="#" onclick="return false;" data-qty="{{d.for_quantity}}">
<span class="menu-item-label" data-label="Complete">{{ __('Complete') }}</span>
</a>
</li>
<li>
<a class="grey-link dropdown-item btn-transfer-materials" href="#" onclick="return false;">
<span class="menu-item-label" data-label="Transfer Materials">{{ __('Transfer Materials') }}</span>

View File

@@ -419,6 +419,18 @@ class BOMConfigurator {
]
);
}
} else if (this.frm.doc.routing && is_root) {
fields.push(
...[
{ fieldtype: "Section Break" },
{
label: __("Operation"),
fieldname: "operation",
fieldtype: "Link",
options: "Operation",
},
]
);
}
fields.push(
@@ -474,6 +486,16 @@ class BOMConfigurator {
dialog.set_primary_action(__("Add"), () => {
let bom_item = dialog.get_values();
if (!bom_item.item_code) {
frappe.throw(__("Sub Assembly Item is mandatory"));
}
bom_item.items.forEach((d) => {
if (!d.item_code) {
frappe.throw(__("Item is mandatory in Raw Materials table."));
}
});
if (dialog.operation && !dialog.workstation_type && !dialog.workstation) {
frappe.throw(__("Either Workstation or Workstation Type is mandatory"));
}

View File

@@ -152,6 +152,30 @@ class VisualPlantFloor {
this.render_plant_visualization();
});
}
update_status(data) {
let workstation_card$ = this.wrapper.find(`.workstation-wrapper[data-workstation="${data.name}"]`);
workstation_card$.find(".workstation-image-container").empty();
if (data.status_image) {
workstation_card$
.find(".workstation-image-container")
.append(`<img class="workstation-image-cls" src="${data.status_image}" />`);
} else {
workstation_card$
.find(".workstation-image-container")
.append(
`<div class="workstation-image-cls workstation-abbr" style="margin:6px; height:82px">${frappe.get_abbr(
data.name,
2
)}</div>`
);
}
workstation_card$.find(".indicator-pill").removeClass(data.old_color);
workstation_card$.find(".indicator-pill").addClass(data.color);
workstation_card$.find(".workstation-status-title").text(data.status);
}
}
frappe.ui.VisualPlantFloor = VisualPlantFloor;

View File

@@ -1,30 +1,20 @@
{% $.each(workstations, (idx, row) => { %}
<div class="workstation-wrapper">
<div class="workstation-status text-right">
{% if(row.status == "Production") { %}
<div class="ring-container">
<div class="ringring"></div>
<div class="circle"></div>
</div>
{% } %}
<span class="indicator-pill no-indicator-dot whitespace-nowrap {{row.color}}" style="margin: 3px 4px 0px 0px;">
<span style="font-size:13px">{{row.status}}</span>
<div class="workstation-wrapper" data-workstation="{{row.name}}">
<div class="workstation-status text-left" style="">
<span class="indicator-pill no-indicator-dot whitespace-nowrap {{row.color}}" style="margin: 8px 0px 0px 8px;">
<span class="workstation-status-title" style="font-size:9px">{{row.status}}</span>
</span>
</div>
<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 class="workstation-image {{row.workstation_off}}" onclick="location.href='{{row.workstation_link}}'">
<div class="workstation-image-container flex items-center justify-center h-32 border-b-grey text-6xl text-grey-100">
{% if(row.status_image) { %}
<img class="workstation-image-cls" src="{{row.status_image}}">
{% } else { %}
<div class="workstation-image-cls workstation-abbr" style="margin:6px; height:82px">{{frappe.get_abbr(row.name, 2)}}</div>
{% } %}
</div>
</div>
<div class="workstation-card" style="display: grid;">
<span class="ellipsis" title="{{row.name}}">
{{row.workstation_name}}
<div style="font-size:9px; text-align:center;padding-bottom:8px">{{row.workstation_name}}</div>
</span>
</div>
</div>

View File

@@ -552,8 +552,16 @@ body[data-route="pos"] {
padding-bottom: 25px;
}
.workstation-image {
cursor: pointer;
}
.workstation-image-container {
height: 3.5rem;
}
.workstation-image-cls {
height: 9rem;
padding-top: 2px;
}
.plant-floor-filter {
@@ -565,7 +573,11 @@ body[data-route="pos"] {
.plant-floor-container {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: var(--margin-lg);
gap: 6px;
}
.workstation-off {
opacity: 0.6;
}
@media screen and (max-width: 620px) {