feat: make material request for job card from workstation dashboard
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index 23650b6..079350b 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -955,6 +955,14 @@
if update_status:
self.db_set("status", self.status)
+ if self.status in ["Completed", "Work In Progress"]:
+ status = {
+ "Completed": "Off",
+ "Work In Progress": "Production",
+ }.get(self.status)
+
+ self.update_status_in_workstation(status)
+
def set_wip_warehouse(self):
if not self.wip_warehouse:
self.wip_warehouse = frappe.db.get_single_value(
@@ -1035,6 +1043,12 @@
return False
+ def update_status_in_workstation(self, status):
+ if not self.workstation:
+ return
+
+ frappe.db.set_value("Workstation", self.workstation, "status", status)
+
@frappe.whitelist()
def make_time_log(args):
diff --git a/erpnext/manufacturing/doctype/plant_floor/plant_floor.js b/erpnext/manufacturing/doctype/plant_floor/plant_floor.js
index 4278937..67e5acd 100644
--- a/erpnext/manufacturing/doctype/plant_floor/plant_floor.js
+++ b/erpnext/manufacturing/doctype/plant_floor/plant_floor.js
@@ -2,11 +2,31 @@
// For license information, please see license.txt
frappe.ui.form.on("Plant Floor", {
- refresh(frm) {
- frm.trigger('prepare_dashboard')
+ setup(frm) {
+ frm.trigger("setup_queries");
},
- prepare_dashboard(frm) {
+ 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();
@@ -16,4 +36,221 @@
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,
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/plant_floor/plant_floor.json b/erpnext/manufacturing/doctype/plant_floor/plant_floor.json
index aa6eb1d..be0052c 100644
--- a/erpnext/manufacturing/doctype/plant_floor/plant_floor.json
+++ b/erpnext/manufacturing/doctype/plant_floor/plant_floor.json
@@ -10,11 +10,13 @@
"field_order": [
"workstations_tab",
"plant_dashboard",
+ "stock_summary_tab",
+ "stock_summary",
"details_tab",
"column_break_mvbx",
"floor_name",
- "section_break_cczv",
- "volumetric_weight"
+ "company",
+ "warehouse"
],
"fields": [
{
@@ -27,7 +29,7 @@
"depends_on": "eval:!doc.__islocal",
"fieldname": "workstations_tab",
"fieldtype": "Tab Break",
- "label": "Dashboard"
+ "label": "Workstations"
},
{
"fieldname": "plant_dashboard",
@@ -37,25 +39,39 @@
{
"fieldname": "details_tab",
"fieldtype": "Tab Break",
- "label": "Details"
+ "label": "Floor"
},
{
"fieldname": "column_break_mvbx",
"fieldtype": "Column Break"
},
{
- "fieldname": "section_break_cczv",
- "fieldtype": "Section Break"
+ "fieldname": "warehouse",
+ "fieldtype": "Link",
+ "label": "Warehouse",
+ "options": "Warehouse"
},
{
- "fieldname": "volumetric_weight",
- "fieldtype": "Float",
- "label": "Volumetric Weight"
+ "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": "2023-12-04 15:36:09.641203",
+ "modified": "2024-01-30 11:59:07.508535",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Plant Floor",
diff --git a/erpnext/manufacturing/doctype/plant_floor/plant_floor.py b/erpnext/manufacturing/doctype/plant_floor/plant_floor.py
index 729cc33..d30b7d1 100644
--- a/erpnext/manufacturing/doctype/plant_floor/plant_floor.py
+++ b/erpnext/manufacturing/doctype/plant_floor/plant_floor.py
@@ -1,8 +1,10 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
-# import frappe
+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):
@@ -14,8 +16,114 @@
if TYPE_CHECKING:
from frappe.types import DF
+ company: DF.Link | None
floor_name: DF.Data | None
- volumetric_weight: DF.Float
+ warehouse: DF.Link | None
# end: auto-generated types
- pass
+ @frappe.whitelist()
+ def make_stock_entry(self, kwargs):
+ if isinstance(kwargs, str):
+ kwargs = frappe.parse_json(kwargs)
+
+ if isinstance(kwargs, dict):
+ kwargs = frappe._dict(kwargs)
+
+ stock_entry = frappe.new_doc("Stock Entry")
+ stock_entry.update(
+ {
+ "company": kwargs.company,
+ "from_warehouse": kwargs.from_warehouse,
+ "to_warehouse": kwargs.to_warehouse,
+ "purpose": kwargs.purpose,
+ "stock_entry_type": kwargs.purpose,
+ "posting_date": nowdate(),
+ "posting_time": nowtime(),
+ "items": self.get_item_details(kwargs),
+ }
+ )
+
+ stock_entry.set_missing_values()
+
+ return stock_entry
+
+ def get_item_details(self, kwargs) -> list[dict]:
+ item_details = frappe.db.get_value(
+ "Item", kwargs.item_code, ["item_name", "stock_uom", "item_group", "description"], as_dict=True
+ )
+ item_details.update(
+ {
+ "qty": kwargs.qty,
+ "uom": item_details.stock_uom,
+ "item_code": kwargs.item_code,
+ "conversion_factor": 1,
+ "s_warehouse": kwargs.from_warehouse,
+ "t_warehouse": kwargs.to_warehouse,
+ }
+ )
+
+ return [item_details]
+
+
+@frappe.whitelist()
+def get_stock_summary(warehouse, start=0, item_code=None, item_group=None):
+ stock_details = get_stock_details(
+ warehouse, start=start, item_code=item_code, item_group=item_group
+ )
+
+ max_count = 0.0
+ for d in stock_details:
+ d.actual_or_pending = (
+ d.projected_qty
+ + d.reserved_qty
+ + d.reserved_qty_for_production
+ + d.reserved_qty_for_sub_contract
+ )
+ d.pending_qty = 0
+ d.total_reserved = (
+ d.reserved_qty + d.reserved_qty_for_production + d.reserved_qty_for_sub_contract
+ )
+ if d.actual_or_pending > d.actual_qty:
+ d.pending_qty = d.actual_or_pending - d.actual_qty
+
+ d.max_count = max(d.actual_or_pending, d.actual_qty, d.total_reserved, max_count)
+ max_count = d.max_count
+ d.item_link = get_link_to_form("Item", d.item_code)
+
+ return stock_details
+
+
+def get_stock_details(warehouse, start=0, item_code=None, item_group=None):
+ item_table = frappe.qb.DocType("Item")
+ bin_table = frappe.qb.DocType("Bin")
+
+ query = (
+ frappe.qb.from_(bin_table)
+ .inner_join(item_table)
+ .on(bin_table.item_code == item_table.name)
+ .select(
+ bin_table.item_code,
+ bin_table.actual_qty,
+ bin_table.projected_qty,
+ bin_table.reserved_qty,
+ bin_table.reserved_qty_for_production,
+ bin_table.reserved_qty_for_sub_contract,
+ bin_table.reserved_qty_for_production_plan,
+ bin_table.reserved_stock,
+ item_table.item_name,
+ item_table.item_group,
+ item_table.image,
+ )
+ .where(bin_table.warehouse == warehouse)
+ .limit(20)
+ .offset(start)
+ .orderby(bin_table.actual_qty, order=Order.desc)
+ )
+
+ if item_code:
+ query = query.where(bin_table.item_code == item_code)
+
+ if item_group:
+ query = query.where(item_table.item_group == item_group)
+
+ return query.run(as_dict=True)
diff --git a/erpnext/manufacturing/doctype/plant_floor/stock_summary_template.html b/erpnext/manufacturing/doctype/plant_floor/stock_summary_template.html
new file mode 100644
index 0000000..8824c98
--- /dev/null
+++ b/erpnext/manufacturing/doctype/plant_floor/stock_summary_template.html
@@ -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>
+{% }); %}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/workstation/workstation.js b/erpnext/manufacturing/doctype/workstation/workstation.js
index 4ffc506..e3ad3fe 100644
--- a/erpnext/manufacturing/doctype/workstation/workstation.js
+++ b/erpnext/manufacturing/doctype/workstation/workstation.js
@@ -94,18 +94,25 @@
},
callback: (r) => {
if (r.message) {
- this.render_job_cards(r.message);
+ this.job_cards = r.message;
+ this.render_job_cards();
}
}
});
}
- render_job_cards(job_cards) {
+ render_job_cards() {
let template = frappe.render_template("workstation_job_card", {
- data: job_cards
+ 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"))
@@ -114,4 +121,198 @@
$(e.currentTarget).html(frappe.utils.icon("es-line-up", "sm", "mb-1"))
});
}
+
+ bind_events() {
+ this.$wrapper.find(".make-material-request").on("click", (e) => {
+ let job_card = $(e.currentTarget).attr("job-card");
+ this.make_material_request(job_card);
+ });
+
+ this.$wrapper.find(".btn-start").on("click", (e) => {
+ let job_card = $(e.currentTarget).attr("job-card");
+ this.start_job(job_card);
+ });
+
+ this.$wrapper.find(".btn-complete").on("click", (e) => {
+ let job_card = $(e.currentTarget).attr("job-card");
+ let pending_qty = flt($(e.currentTarget).attr("pending-qty"));
+ this.complete_job(job_card, pending_qty);
+ });
+ }
+
+ start_job(job_card) {
+ let me = this;
+ frappe.prompt([
+ {
+ fieldtype: 'Datetime',
+ label: __('Start Time'),
+ fieldname: 'start_time',
+ reqd: 1,
+ default: frappe.datetime.now_datetime()
+ },
+ {
+ label: __('Operator'),
+ fieldname: 'employee',
+ fieldtype: 'Link',
+ options: 'Employee',
+ }
+ ], data => {
+ this.frm.call({
+ method: "start_job",
+ doc: this.frm.doc,
+ args: {
+ job_card: job_card,
+ from_time: data.start_time,
+ employee: data.employee,
+ },
+ callback(r) {
+ if (r.message) {
+ me.job_cards = [r.message];
+ me.prepare_timer()
+ me.update_job_card_details();
+ }
+ }
+ });
+ }, __("Enter Value"), __("Start Job"));
+ }
+
+ complete_job(job_card, qty_to_manufacture) {
+ let me = this;
+ let fields = [
+ {
+ fieldtype: 'Float',
+ label: __('Completed Quantity'),
+ fieldname: 'qty',
+ reqd: 1,
+ default: flt(qty_to_manufacture || 0)
+ },
+ {
+ fieldtype: 'Datetime',
+ label: __('End Time'),
+ fieldname: 'end_time',
+ default: frappe.datetime.now_datetime()
+ },
+ ];
+
+ frappe.prompt(fields, data => {
+ if (data.qty <= 0) {
+ frappe.throw(__("Quantity should be greater than 0"));
+ }
+
+ this.frm.call({
+ method: "complete_job",
+ doc: this.frm.doc,
+ args: {
+ job_card: job_card,
+ qty: data.qty,
+ to_time: data.end_time,
+ },
+ callback: function(r) {
+ if (r.message) {
+ me.job_cards = [r.message];
+ me.prepare_timer()
+ me.update_job_card_details();
+ }
+ }
+ });
+ }, __("Enter Value"), __("Submit"));
+ }
+
+ make_material_request(job_card) {
+ frappe.call({
+ method: "erpnext.manufacturing.doctype.job_card.job_card.make_material_request",
+ args: {
+ source_name: job_card,
+ },
+ callback: (r) => {
+ if (r.message) {
+ var doc = frappe.model.sync(r.message)[0];
+ frappe.set_route("Form", doc.doctype, doc.name);
+ }
+ }
+ });
+ }
+
+ prepare_timer() {
+ this.job_cards.forEach((data) => {
+ if (data.time_logs?.length) {
+ data._current_time = this.get_current_time(data);
+ if (data.time_logs[cint(data.time_logs.length) - 1].to_time) {
+ this.updateStopwatch(data);
+ } else {
+ this.initialiseTimer(data);
+ }
+ }
+ });
+ }
+
+ update_job_card_details() {
+ let color_map = {
+ "Pending": "var(--bg-blue)",
+ "In Process": "var(--bg-yellow)",
+ "Submitted": "var(--bg-blue)",
+ "Open": "var(--bg-gray)",
+ "Closed": "var(--bg-green)",
+ "Work In Progress": "var(--bg-orange)",
+ }
+
+ this.job_cards.forEach((data) => {
+ let job_card_selector = this.$wrapper.find(`
+ [data-name='${data.name}']`
+ );
+
+ $(job_card_selector).find(".job-card-status").text(data.status);
+ $(job_card_selector).find(".job-card-status").css("backgroundColor", color_map[data.status]);
+
+ if (data.status === "Work In Progress") {
+ $(job_card_selector).find(".btn-start").addClass("hide");
+ $(job_card_selector).find(".btn-complete").removeClass("hide");
+ } else if (data.status === "Completed") {
+ $(job_card_selector).find(".btn-start").addClass("hide");
+ $(job_card_selector).find(".btn-complete").addClass("hide");
+ }
+ });
+ }
+
+ initialiseTimer(data) {
+ setInterval(() => {
+ data._current_time += 1;
+ this.updateStopwatch(data);
+ }, 1000);
+ }
+
+ updateStopwatch(data) {
+ let increment = data._current_time;
+ let hours = Math.floor(increment / 3600);
+ let minutes = Math.floor((increment - (hours * 3600)) / 60);
+ let seconds = cint(increment - (hours * 3600) - (minutes * 60));
+
+ let job_card_selector = `[data-job-card='${data.name}']`
+ let timer_selector = this.$wrapper.find(job_card_selector)
+
+ $(timer_selector).find(".hours").text(hours < 10 ? ("0" + hours.toString()) : hours.toString());
+ $(timer_selector).find(".minutes").text(minutes < 10 ? ("0" + minutes.toString()) : minutes.toString());
+ $(timer_selector).find(".seconds").text(seconds < 10 ? ("0" + seconds.toString()) : seconds.toString());
+ }
+
+ get_current_time(data) {
+ let current_time = 0.0;
+ data.time_logs.forEach(d => {
+ if (d.to_time) {
+ if (d.time_in_mins) {
+ current_time += flt(d.time_in_mins, 2) * 60;
+ } else {
+ current_time += this.get_seconds_diff(d.to_time, d.from_time);
+ }
+ } else {
+ current_time += this.get_seconds_diff(frappe.datetime.now_datetime(), d.from_time);
+ }
+ });
+
+ return current_time;
+ }
+
+ get_seconds_diff(d1, d2) {
+ return moment(d1).diff(d2, "seconds");
+ }
}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/workstation/workstation.py b/erpnext/manufacturing/doctype/workstation/workstation.py
index 973c994..3d40a2d 100644
--- a/erpnext/manufacturing/doctype/workstation/workstation.py
+++ b/erpnext/manufacturing/doctype/workstation/workstation.py
@@ -164,6 +164,28 @@
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):
@@ -177,6 +199,7 @@
"operation",
"total_completed_qty",
"for_quantity",
+ "transferred_qty",
"status",
"expected_start_date",
"expected_end_date",
@@ -193,6 +216,11 @@
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 = (
@@ -204,12 +232,16 @@
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):
- colos_map = {
+ color_map = {
"Pending": "var(--bg-blue)",
"In Process": "var(--bg-yellow)",
"Submitted": "var(--bg-blue)",
@@ -218,7 +250,7 @@
"Work In Progress": "var(--bg-orange)",
}
- return colos_map.get(status, "var(--bg-blue)")
+ return color_map.get(status, "var(--bg-blue)")
def get_raw_materials(job_cards):
@@ -245,6 +277,29 @@
return raw_materials
+def get_time_logs(job_cards):
+ time_logs = {}
+
+ data = frappe.get_all(
+ "Job Card Time Log",
+ fields=[
+ "parent",
+ "name",
+ "employee",
+ "from_time",
+ "to_time",
+ "time_in_mins",
+ ],
+ filters={"parent": ["in", job_cards], "parentfield": "time_logs"},
+ order_by="parent, idx",
+ )
+
+ for row in data:
+ time_logs.setdefault(row.parent, []).append(row)
+
+ return time_logs
+
+
@frappe.whitelist()
def get_default_holiday_list():
return frappe.get_cached_value(
diff --git a/erpnext/manufacturing/doctype/workstation/workstation_job_card.html b/erpnext/manufacturing/doctype/workstation/workstation_job_card.html
index 3c0ef6d..9770785 100644
--- a/erpnext/manufacturing/doctype/workstation/workstation_job_card.html
+++ b/erpnext/manufacturing/doctype/workstation/workstation_job_card.html
@@ -27,13 +27,28 @@
{{ 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 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" title="{{__('Status')}}" style="background:{{d.status_color}};text-align:center;border-radius:var(--border-radius-full)">
+ <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>
@@ -48,11 +63,24 @@
{{ 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>
- {{ __("Raw Materials") }}
+ <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">
diff --git a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json
index e3b632d..d2520d6 100644
--- a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json
+++ b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json
@@ -1,6 +1,6 @@
{
"charts": [],
- "content": "[{\"id\":\"csBCiDglCE\",\"type\":\"header\",\"data\":{\"text\":\"<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 & 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",
"custom_blocks": [],
"docstatus": 0,
@@ -316,7 +316,7 @@
"type": "Link"
}
],
- "modified": "2023-11-30 15:21:14.577990",
+ "modified": "2024-01-30 21:49:58.577218",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Manufacturing",
@@ -341,7 +341,6 @@
"doc_view": "List",
"label": "Plant Floor",
"link_to": "Plant Floor",
- "stats_filter": "[]",
"type": "DocType"
},
{
@@ -354,13 +353,6 @@
{
"color": "Grey",
"doc_view": "List",
- "label": "Visual Plant Floor",
- "link_to": "visual-plant-floor",
- "type": "Page"
- },
- {
- "color": "Grey",
- "doc_view": "List",
"label": "BOM",
"link_to": "BOM",
"stats_filter": "{\"is_active\":[\"=\",1]}",
diff --git a/erpnext/public/js/plant_floor_visual/visual_plant.js b/erpnext/public/js/plant_floor_visual/visual_plant.js
index b1d120f..8cd73ad 100644
--- a/erpnext/public/js/plant_floor_visual/visual_plant.js
+++ b/erpnext/public/js/plant_floor_visual/visual_plant.js
@@ -26,6 +26,7 @@
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();
}
}
@@ -138,7 +139,6 @@
}
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
diff --git a/erpnext/public/scss/erpnext.scss b/erpnext/public/scss/erpnext.scss
index ef09854..1626b7c 100644
--- a/erpnext/public/scss/erpnext.scss
+++ b/erpnext/public/scss/erpnext.scss
@@ -510,7 +510,6 @@
}
.plant-floor-container {
- padding-top: 10px;
display: grid;
grid-template-columns: repeat(6,minmax(0,1fr));
gap: var(--margin-xl);
@@ -530,7 +529,7 @@
width: 100%;
font-size: 50px;
margin: var(--margin-sm);
- min-height: 11rem;
+ min-height: 9rem;
}
.workstation-abbr {