Merge pull request #38362 from rohitwaghchaure/feat-visual-plant-floor
feat: visual plant floor
This commit is contained in:
commit
31f586f716
@ -955,6 +955,14 @@ class JobCard(Document):
|
|||||||
if update_status:
|
if update_status:
|
||||||
self.db_set("status", self.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):
|
def set_wip_warehouse(self):
|
||||||
if not self.wip_warehouse:
|
if not self.wip_warehouse:
|
||||||
self.wip_warehouse = frappe.db.get_single_value(
|
self.wip_warehouse = frappe.db.get_single_value(
|
||||||
@ -1035,6 +1043,12 @@ class JobCard(Document):
|
|||||||
|
|
||||||
return False
|
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()
|
@frappe.whitelist()
|
||||||
def make_time_log(args):
|
def make_time_log(args):
|
||||||
|
|||||||
256
erpnext/manufacturing/doctype/plant_floor/plant_floor.js
Normal file
256
erpnext/manufacturing/doctype/plant_floor/plant_floor.js
Normal file
@ -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(`
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-12 filter-section section-body">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
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(`<div class="col-sm-12 stock-summary-container" style="margin-bottom:20px"></div>`);
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
97
erpnext/manufacturing/doctype/plant_floor/plant_floor.json
Normal file
97
erpnext/manufacturing/doctype/plant_floor/plant_floor.json
Normal file
@ -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": []
|
||||||
|
}
|
||||||
129
erpnext/manufacturing/doctype/plant_floor/plant_floor.py
Normal file
129
erpnext/manufacturing/doctype/plant_floor/plant_floor.py
Normal file
@ -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)
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
{% $.each(stock_summary, (idx, row) => { %}
|
||||||
|
<div class="row" style="border-bottom:1px solid var(--border-color); padding:4px 5px; margin-top: 3px;margin-bottom: 3px;">
|
||||||
|
<div class="col-sm-1">
|
||||||
|
{% if(row.image) { %}
|
||||||
|
<img style="width:50px;height:50px;" src="{{row.image}}">
|
||||||
|
{% } else { %}
|
||||||
|
<div style="width:50px;height:50px;background-color:var(--control-bg);text-align:center;padding-top:15px">{{frappe.get_abbr(row.item_code, 2)}}</div>
|
||||||
|
{% } %}
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-3">
|
||||||
|
{% if (row.item_code === row.item_name) { %}
|
||||||
|
{{row.item_link}}
|
||||||
|
{% } else { %}
|
||||||
|
{{row.item_link}}
|
||||||
|
<p>
|
||||||
|
{{row.item_name}}
|
||||||
|
</p>
|
||||||
|
{% } %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-1" title="{{ __('Actual Qty') }}">
|
||||||
|
{{ frappe.format(row.actual_qty, { fieldtype: "Float"})}}
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-1" title="{{ __('Reserved Stock') }}">
|
||||||
|
{{ frappe.format(row.reserved_stock, { fieldtype: "Float"})}}
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-4 small">
|
||||||
|
<span class="inline-graph">
|
||||||
|
<span class="inline-graph-half" title="{{ __("Reserved Qty") }}">
|
||||||
|
<span class="inline-graph-count">{{ row.total_reserved }}</span>
|
||||||
|
<span class="inline-graph-bar">
|
||||||
|
<span class="inline-graph-bar-inner"
|
||||||
|
style="width: {{ cint(Math.abs(row.total_reserved)/row.max_count * 100) || 5 }}%">
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span class="inline-graph-half" title="{{ __("Actual Qty {0} / Waiting Qty {1}", [row.actual_qty, row.pending_qty]) }}">
|
||||||
|
<span class="inline-graph-count">
|
||||||
|
{{ row.actual_qty }} {{ (row.pending_qty > 0) ? ("(" + row.pending_qty+ ")") : "" }}
|
||||||
|
</span>
|
||||||
|
<span class="inline-graph-bar">
|
||||||
|
<span class="inline-graph-bar-inner dark"
|
||||||
|
style="width: {{ cint(row.actual_qty/row.max_count * 100) }}%">
|
||||||
|
</span>
|
||||||
|
{% if row.pending_qty > 0 %}
|
||||||
|
<span class="inline-graph-bar-inner" title="{{ __("Projected Qty") }}"
|
||||||
|
style="width: {{ cint(row.pending_qty/row.max_count * 100) }}%">
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-1">
|
||||||
|
<button style="margin-left: 7px;" class="btn btn-default btn-xs btn-add" data-item-code="{{ escape(row.item_code) }}">Add</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-1">
|
||||||
|
<button style="margin-left: 7px;" class="btn btn-default btn-xs btn-move" data-item-code="{{ escape(row.item_code) }}">Move</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% }); %}
|
||||||
@ -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
|
||||||
@ -2,6 +2,28 @@
|
|||||||
// License: GNU General Public License v3. See license.txt
|
// License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
frappe.ui.form.on("Workstation", {
|
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) {
|
onload(frm) {
|
||||||
if(frm.is_new())
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,10 +8,24 @@
|
|||||||
"document_type": "Setup",
|
"document_type": "Setup",
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
|
"dashboard_tab",
|
||||||
|
"workstation_dashboard",
|
||||||
|
"details_tab",
|
||||||
"workstation_name",
|
"workstation_name",
|
||||||
"production_capacity",
|
|
||||||
"column_break_3",
|
|
||||||
"workstation_type",
|
"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",
|
"over_heads",
|
||||||
"hour_rate_electricity",
|
"hour_rate_electricity",
|
||||||
"hour_rate_consumable",
|
"hour_rate_consumable",
|
||||||
@ -24,7 +38,9 @@
|
|||||||
"description",
|
"description",
|
||||||
"working_hours_section",
|
"working_hours_section",
|
||||||
"holiday_list",
|
"holiday_list",
|
||||||
"working_hours"
|
"working_hours",
|
||||||
|
"total_working_hours",
|
||||||
|
"connections_tab"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@ -120,9 +136,10 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "1",
|
"default": "1",
|
||||||
|
"description": "Run parallel job cards in a workstation",
|
||||||
"fieldname": "production_capacity",
|
"fieldname": "production_capacity",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
"label": "Production Capacity",
|
"label": "Job Capacity",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -145,12 +162,97 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "section_break_11",
|
"fieldname": "section_break_11",
|
||||||
"fieldtype": "Section Break"
|
"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",
|
"icon": "icon-wrench",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
|
"image_field": "on_status_image",
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-11-04 17:39:01.549346",
|
"modified": "2023-11-30 12:43:35.808845",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Workstation",
|
"name": "Workstation",
|
||||||
|
|||||||
@ -11,7 +11,11 @@ from frappe.utils import (
|
|||||||
comma_and,
|
comma_and,
|
||||||
flt,
|
flt,
|
||||||
formatdate,
|
formatdate,
|
||||||
|
get_link_to_form,
|
||||||
|
get_time,
|
||||||
|
get_url_to_form,
|
||||||
getdate,
|
getdate,
|
||||||
|
time_diff_in_hours,
|
||||||
time_diff_in_seconds,
|
time_diff_in_seconds,
|
||||||
to_timedelta,
|
to_timedelta,
|
||||||
)
|
)
|
||||||
@ -60,6 +64,23 @@ class Workstation(Document):
|
|||||||
def before_save(self):
|
def before_save(self):
|
||||||
self.set_data_based_on_workstation_type()
|
self.set_data_based_on_workstation_type()
|
||||||
self.set_hour_rate()
|
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):
|
def set_hour_rate(self):
|
||||||
self.hour_rate = (
|
self.hour_rate = (
|
||||||
@ -143,6 +164,141 @@ class Workstation(Document):
|
|||||||
|
|
||||||
return schedule_date
|
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()
|
@frappe.whitelist()
|
||||||
def get_default_holiday_list():
|
def get_default_holiday_list():
|
||||||
@ -201,3 +357,52 @@ def check_workstation_for_holiday(workstation, from_datetime, to_datetime):
|
|||||||
+ "\n".join(applicable_holidays),
|
+ "\n".join(applicable_holidays),
|
||||||
WorkstationHolidayError,
|
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
|
||||||
|
|||||||
@ -0,0 +1,125 @@
|
|||||||
|
<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-2">
|
||||||
|
<div class="frappe-control timer" title="{{__('Timer')}}" style="text-align:center;font-size:14px;" data-job-card = {{escape(d.name)}}>
|
||||||
|
<span class="hours">00</span>
|
||||||
|
<span class="colon">:</span>
|
||||||
|
<span class="minutes">00</span>
|
||||||
|
<span class="colon">:</span>
|
||||||
|
<span class="seconds">00</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if(d.status === "Open") { %}
|
||||||
|
<div class="frappe-control" title="{{__('Expected Start Date')}}" style="text-align:center;font-size:11px;padding-top: 4px;">
|
||||||
|
{{ frappe.format(d.expected_start_date, { fieldtype: 'Datetime' }) }}
|
||||||
|
</div>
|
||||||
|
{% } else { %}
|
||||||
|
<div class="frappe-control" title="{{__('Expected End Date')}}" style="text-align:center;font-size:11px;padding-top: 4px;">
|
||||||
|
{{ frappe.format(d.expected_end_date, { fieldtype: 'Datetime' }) }}
|
||||||
|
</div>
|
||||||
|
{% } %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="form-column col-sm-2">
|
||||||
|
<div class="frappe-control job-card-status" 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 class="form-column col-sm-2 text-center">
|
||||||
|
<button style="width: 85px;" class="btn btn-default btn-start {% if(d.status !== "Open") { %} hide {% } %}" job-card="{{d.name}}"> {{__("Start")}} </button>
|
||||||
|
<button style="width: 85px;" class="btn btn-default btn-complete {% if(d.status === "Open") { %} hide {% } %}" job-card="{{d.name}}" pending-qty="{{d.for_quantity - d.transferred_qty}}"> {{__("Complete")}} </button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-body section-body-job-card form-section hide">
|
||||||
|
<hr>
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-column col-sm-2">
|
||||||
|
{{ __("Raw Materials") }}
|
||||||
|
</div>
|
||||||
|
{% if(d.make_material_request) { %}
|
||||||
|
<div class="form-column col-sm-10 text-right">
|
||||||
|
<button class="btn btn-default btn-xs make-material-request" job-card="{{d.name}}">{{ __("Material Request") }}</button>
|
||||||
|
</div>
|
||||||
|
{% } %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% 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>
|
||||||
@ -1,5 +1,16 @@
|
|||||||
|
|
||||||
frappe.listview_settings['Workstation'] = {
|
frappe.listview_settings['Workstation'] = {
|
||||||
// add_fields: ["status"],
|
add_fields: ["status"],
|
||||||
// filters:[["status","=", "Open"]]
|
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];
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,150 +1,58 @@
|
|||||||
{
|
{
|
||||||
"allow_copy": 0,
|
"actions": [],
|
||||||
"allow_import": 0,
|
|
||||||
"allow_rename": 0,
|
|
||||||
"beta": 0,
|
|
||||||
"creation": "2014-12-24 14:46:40.678236",
|
"creation": "2014-12-24 14:46:40.678236",
|
||||||
"custom": 0,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"document_type": "",
|
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"start_time",
|
||||||
|
"hours",
|
||||||
|
"column_break_2",
|
||||||
|
"end_time",
|
||||||
|
"enabled"
|
||||||
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "start_time",
|
"fieldname": "start_time",
|
||||||
"fieldtype": "Time",
|
"fieldtype": "Time",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Start Time",
|
"label": "Start Time",
|
||||||
"length": 0,
|
"reqd": 1
|
||||||
"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
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "column_break_2",
|
"fieldname": "column_break_2",
|
||||||
"fieldtype": "Column Break",
|
"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
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "end_time",
|
"fieldname": "end_time",
|
||||||
"fieldtype": "Time",
|
"fieldtype": "Time",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "End Time",
|
"label": "End Time",
|
||||||
"length": 0,
|
"reqd": 1
|
||||||
"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
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"default": "1",
|
"default": "1",
|
||||||
"fieldname": "enabled",
|
"fieldname": "enabled",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
"label": "Enabled"
|
||||||
"label": "Enabled",
|
},
|
||||||
"length": 0,
|
{
|
||||||
"no_copy": 0,
|
"fieldname": "hours",
|
||||||
"permlevel": 0,
|
"fieldtype": "Float",
|
||||||
"precision": "",
|
"label": "Hours",
|
||||||
"print_hide": 0,
|
"read_only": 1
|
||||||
"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
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"hide_heading": 0,
|
|
||||||
"hide_toolbar": 0,
|
|
||||||
"idx": 0,
|
|
||||||
"image_view": 0,
|
|
||||||
"in_create": 0,
|
|
||||||
|
|
||||||
"is_submittable": 0,
|
|
||||||
"issingle": 0,
|
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"max_attachments": 0,
|
"links": [],
|
||||||
"modified": "2016-12-13 05:02:36.754145",
|
"modified": "2023-10-25 14:48:29.697498",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Workstation Working Hour",
|
"name": "Workstation Working Hour",
|
||||||
"name_case": "",
|
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
"quick_entry": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"read_only_onload": 0,
|
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"track_seen": 0
|
"states": []
|
||||||
}
|
}
|
||||||
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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"
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"charts": [],
|
"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 & 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\":\"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\":\"<span class=\\\"h4\\\"><b>Reports & 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",
|
"creation": "2020-03-02 17:11:37.032604",
|
||||||
"custom_blocks": [],
|
"custom_blocks": [],
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
@ -316,7 +316,7 @@
|
|||||||
"type": "Link"
|
"type": "Link"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2023-08-08 22:28:39.633891",
|
"modified": "2024-01-30 21:49:58.577218",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Manufacturing",
|
"name": "Manufacturing",
|
||||||
@ -336,6 +336,13 @@
|
|||||||
"type": "URL",
|
"type": "URL",
|
||||||
"url": "https://frappe.school/courses/manufacturing?utm_source=in_app"
|
"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",
|
"color": "Grey",
|
||||||
"doc_view": "List",
|
"doc_view": "List",
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import "./sms_manager";
|
|||||||
import "./utils/party";
|
import "./utils/party";
|
||||||
import "./controllers/stock_controller";
|
import "./controllers/stock_controller";
|
||||||
import "./payment/payments";
|
import "./payment/payments";
|
||||||
|
import "./templates/visual_plant_floor_template.html";
|
||||||
|
import "./plant_floor_visual/visual_plant";
|
||||||
import "./controllers/taxes_and_totals";
|
import "./controllers/taxes_and_totals";
|
||||||
import "./controllers/transaction";
|
import "./controllers/transaction";
|
||||||
import "./templates/item_selector.html";
|
import "./templates/item_selector.html";
|
||||||
|
|||||||
157
erpnext/public/js/plant_floor_visual/visual_plant.js
Normal file
157
erpnext/public/js/plant_floor_visual/visual_plant.js
Normal 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.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;
|
||||||
19
erpnext/public/js/templates/visual_plant_floor_template.html
Normal file
19
erpnext/public/js/templates/visual_plant_floor_template.html
Normal 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>
|
||||||
|
{% }); %}
|
||||||
@ -490,3 +490,53 @@ body[data-route="pos"] {
|
|||||||
.exercise-col {
|
.exercise-col {
|
||||||
padding: 10px;
|
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;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user