6 Commits

Author SHA1 Message Date
Christian Anetzberger
68b7f21e13 Basic function now working in v16 2026-01-20 19:45:31 +00:00
b20411fce1 Fixed caching issues 2023-09-08 07:44:22 +00:00
b14eeff0b3 Fixed calculation Error 2023-09-08 07:16:33 +00:00
a487804c7c Removed logger 2023-09-08 05:14:21 +00:00
ea83e3e1ac Redone status generation and caching improvements 2023-09-07 06:29:01 +00:00
3cdcb239c3 Rework 2023-09-06 13:45:04 +00:00
17 changed files with 601 additions and 204 deletions

View File

@@ -1,3 +1,2 @@
__version__ = '0.0.1' __version__ = '14.38.0'

View File

@@ -15,7 +15,13 @@ required_apps = ["erpnext"]
# include js, css files in header of desk.html # include js, css files in header of desk.html
# app_include_css = "/assets/manufacturing_overview/css/manufacturing_overview.css" # app_include_css = "/assets/manufacturing_overview/css/manufacturing_overview.css"
app_include_js = "/assets/manufacturing_overview/js/manufacturing_overview.min.js" app_include_js = [
"drawer_bootstrap.bundle.js",
]
app_include_css = [
"manufacturing_overview_drawer.bundle.css",
]
# include js, css files in header of web template # include js, css files in header of web template
# web_include_css = "/assets/manufacturing_overview/css/manufacturing_overview.css" # web_include_css = "/assets/manufacturing_overview/css/manufacturing_overview.css"
@@ -45,7 +51,7 @@ app_include_js = "/assets/manufacturing_overview/js/manufacturing_overview.min.j
# website user home page (by Role) # website user home page (by Role)
# role_home_page = { # role_home_page = {
# "Role": "home_page" # "Role": "home_page"
# } # }
# Generators # Generators
@@ -101,7 +107,7 @@ app_include_js = "/assets/manufacturing_overview/js/manufacturing_overview.min.j
# "on_update": "method", # "on_update": "method",
# "on_cancel": "method", # "on_cancel": "method",
# "on_trash": "method" # "on_trash": "method"
# } # }
# } # }
# Scheduled Tasks # Scheduled Tasks

View File

@@ -1,96 +1,174 @@
import frappe import frappe
from functools import reduce
from datetime import date from datetime import date
from typing import Dict, List
def clearCache(doc, event): def get_due_in_days(d):
frappe.cache().delete_value("production_overview") return (d - date.today()).days
def getDueInDays(d):
delta = d - date.today()
return delta.days
def getWorkOrders(soname): def format_date(d):
return frappe.get_all("Work Order", filters={ # keep your output format
'sales_order': soname}, fields=['name']) return frappe.utils.formatdate(d, "dd.MM.yyyy")
def getAmountInWarehouses(item_code): def calculate_status(item):
bins = frappe.get_all("Bin", fields=["actual_qty"], filters=[ # item.qty is still the original ordered qty here (before subtracting delivered)
["actual_qty", ">", 0], ["item_code", "=", item_code]]) if item.delivered_qty > 0 and item.delivered_qty < item.qty:
return reduce(lambda x, y: x + y, return "Partially Delivered"
map(lambda b: b["actual_qty"], bins), 0) elif item.delivered_qty >= item.qty:
return "Fully Delivered"
elif item.currentWarehouseQty >= item.qty:
def shortenCustomerName(customer): return "In Warehouse"
shortName = frappe.get_value( elif item.work_order_qty > 0:
'Customer', customer, 'short_name') return "To Produce"
if shortName is not None:
return shortName
else: else:
return customer return "No Work Order"
def generateProductionOverviewCache():
salesOrderItems = frappe.db.sql( def fetch_sales_order_items() -> List[dict]:
# NOTE: your WHERE uses "status = 'To Deliver and Bill'".
# In your SELECT you also include `tabSales Order`.status as parentStatus,
# but the WHERE currently filters on "status" (ambiguous). We'll be explicit.
return frappe.db.sql(
""" """
SELECT SELECT
`tabSales Order Item`.name, soi.name,
`tabSales Order Item`.item_name, soi.item_name,
`tabSales Order Item`.item_group, soi.item_group,
`tabSales Order Item`.qty, soi.qty,
`tabSales Order Item`.actual_qty, soi.actual_qty,
`tabSales Order Item`.delivered_qty, soi.delivered_qty,
`tabSales Order Item`.delivery_date, soi.delivery_date,
`tabSales Order Item`.item_code, soi.item_code,
`tabSales Order Item`.work_order_qty, soi.work_order_qty,
`tabSales Order Item`.parent, soi.parent,
`tabSales Order`.status as parentStatus soi.production_plan_qty,
FROM so.status AS parentStatus,
`tabSales Order Item` so.customer AS customer_name
INNER JOIN `tabSales Order` FROM `tabSales Order Item` soi
ON INNER JOIN `tabSales Order` so ON soi.parent = so.name
`tabSales Order Item`.parent = `tabSales Order`.name
WHERE WHERE
delivered_qty < qty and soi.delivered_qty < soi.qty
status = 'To Deliver and Bill' and AND so.status = 'To Deliver and Bill'
item_group = "Produkte" AND soi.item_group = 'Produkte'
ORDER BY ORDER BY
delivery_date, item_code, name soi.delivery_date, soi.item_code, soi.name
""", as_dict=1) """,
as_dict=True,
)
for soItem in salesOrderItems:
soItem.due_in = getDueInDays(soItem.delivery_date)
soItem.customer = shortenCustomerName(frappe.get_value( def fetch_customer_short_names(customers: List[str]) -> Dict[str, str]:
'Sales Order', soItem.parent, 'customer')) if not customers:
# soItem.wos = frappe.get_all('Work Order', filters=[ return {}
# ['sales_order', '=', soItem.parent], ['production_item', '=', soItem.item_code], ['status', '!=', "Cancelled"]])
soItem.delivery_date = frappe.utils.formatdate(soItem.delivery_date, 'dd.MM.yyyy') rows = frappe.get_all(
soItem.warehouseamount = getAmountInWarehouses(soItem.item_code) "Customer",
filters={"name": ["in", customers]},
fields=["name", "short_name"],
)
out = {}
for r in rows:
# fall back to full name if short_name missing/empty
out[r["name"]] = r.get("short_name") or r["name"]
return out
if soItem.delivered_qty > 0 and soItem.delivered_qty < soItem.qty:
soItem.status = 'Partially Delivered' def fetch_total_warehouse_qty(item_codes: List[str]) -> Dict[str, float]:
elif soItem.delivered_qty >= soItem.qty: if not item_codes:
soItem.status = 'Fully Delivered' return {}
elif soItem.warehouseamount >= soItem.qty:
soItem.status = 'In Warehouse' # Sum actual_qty across all bins with qty > 0 for those item codes
elif soItem.work_order_qty > 0: rows = frappe.db.sql(
soItem.status = 'To Produce' """
SELECT
item_code,
SUM(actual_qty) AS total_qty
FROM `tabBin`
WHERE
actual_qty > 0
AND item_code IN %(item_codes)s
GROUP BY item_code
""",
{"item_codes": tuple(set(item_codes))},
as_dict=True,
)
return {r["item_code"]: float(r["total_qty"] or 0) for r in rows}
def generateProductionOverviewCacheData():
salesOrderItems = fetch_sales_order_items()
if not salesOrderItems:
return []
# collect uniques
item_codes = [r["item_code"] for r in salesOrderItems if r.get("item_code")]
customers = [r["customer_name"] for r in salesOrderItems if r.get("customer_name")]
total_wh_qty = fetch_total_warehouse_qty(item_codes)
customer_short = fetch_customer_short_names(customers)
# Running "allocation" per item_code (your currentWarehouseQty logic, but O(n))
# In your old code:
# - first occurrence returns fetchedWarehouseQty
# - then subtract qty from stored qty for subsequent occurrences
remaining_after_current: Dict[str, float] = {}
for r in salesOrderItems:
item_code = r.get("item_code")
if not item_code:
r["totalWarehouseQty"] = 0
r["currentWarehouseQty"] = 0
else: else:
soItem.status = 'No Work Order' fetched = float(total_wh_qty.get(item_code, 0))
soItem.qty = soItem.qty - soItem.delivered_qty if item_code in remaining_after_current:
# previously stored "qty" meant: remaining AFTER previous rows consumption
current_qty = remaining_after_current[item_code]
else:
# first time: show full fetched qty
current_qty = fetched
soItem.link = '/app/sales-order/' + soItem.parent # Update remaining for next occurrence: subtract ordered qty (old behavior)
remaining_after_current[item_code] = current_qty - float(r.get("qty") or 0)
frappe.cache().set_value("production_overview", salesOrderItems) r["totalWarehouseQty"] = fetched
r["currentWarehouseQty"] = current_qty
# due_in uses original date (date object)
r["due_in"] = get_due_in_days(r["delivery_date"])
# customer short name
cust = r.get("customer_name") or ""
r["customer"] = customer_short.get(cust, cust)
# format delivery date string
r["delivery_date"] = format_date(r["delivery_date"])
# compute status BEFORE qty is reduced by delivered (same as your original)
r["status"] = calculate_status(frappe._dict(r))
# now update qty to remaining qty (same as your original)
r["qty"] = (r.get("qty") or 0) - (r.get("delivered_qty") or 0)
# link
r["link"] = "/app/sales-order/" + r["parent"]
# keep output compatibility: you previously returned dicts with these keys
# (delivered_qty, customer_name etc are extra; harmless)
return salesOrderItems
@frappe.whitelist() @frappe.whitelist()
def getSalesorderOverviewList(): def getSalesorderOverviewList():
salesOrderItems = frappe.cache().get_value( cache_key = "production_overview"
"production_overview", generateProductionOverviewCache()) salesOrderItems = frappe.cache().get_value(cache_key, expires=True)
if salesOrderItems is None:
salesOrderItems = generateProductionOverviewCacheData()
frappe.cache().set_value(cache_key, salesOrderItems, expires_in_sec=60)
return salesOrderItems return salesOrderItems

View File

@@ -0,0 +1,59 @@
import frappe
from erpnext.stock.get_item_details import get_default_bom
# frappe.utils.logger.set_log_level("DEBUG")
# logger = frappe.logger("manufacturing_overview",
# allow_site=True, file_count=50)
# logger.debug(f"{current_value} + {value} = {updated_value}")
@frappe.whitelist()
def makepp(sales_order):
"""
Check if Production Plan already exists and break when it does.
"""
so = frappe.get_doc("Sales Order", sales_order)
checkpp = frappe.get_all('Production Plan Item', filters={'sales_order': so.name}, fields=[
'name', 'parent'])
if checkpp:
return {'status': 400,
'docname': [checkpp[0].parent]}
"""
Create Production Plan
"""
sales_orders = [{
"sales_order": so.name,
"customer": so.customer,
"sales_order_date": so.transaction_date,
"grand_total": so.grand_total,
}]
po_items = []
for item in so.items:
i = {
"item_code": item.item_code,
"bom_no": get_default_bom(item.item_code),
"planned_qty": item.qty,
"planned_start_date": frappe.utils.nowdate(),
"stock_uom": "Stk",
"sales_order": so.name,
"sales_order_item": item.name
}
po_items.append(i)
pp = frappe.get_doc({
'doctype': 'Production Plan',
'get_items_from': 'Sales Order',
'sales_orders': sales_orders,
'po_items': po_items,
'for_warehouse': "Lagerräume - HP"
})
pp.insert()
return {'status': 201,
'docname': [pp.name]}

View File

@@ -1,5 +0,0 @@
{
"manufacturing_overview/js/manufacturing_overview.min.js": [
"public/js/manufacturing_overview_page.js"
]
}

View File

@@ -0,0 +1,66 @@
#mo-drawer {
position: fixed;
top: 0;
right: 0;
height: 100vh;
width: 390px;
/* collapsed: show thin bar */
transform: translateX(calc(100% - 12px));
transition: transform 160ms ease;
z-index: 1050;
}
#mo-drawer:hover,
#mo-drawer.mo-open {
transform: translateX(0);
}
#mo-drawer .mo-hover-bar {
position: absolute;
top: 0;
left: 0;
width: 12px;
height: 100%;
cursor: pointer;
background: var(--primary, #2563eb);
opacity: 0.35;
}
#mo-drawer:hover .mo-hover-bar {
opacity: 0.55;
}
#mo-drawer .mo-panel {
position: absolute;
top: 0;
left: 12px;
width: calc(100% - 12px);
height: 100%;
background: var(--card-bg, #fff);
border-left: 1px solid var(--border-color, #e5e7eb);
box-shadow: -10px 0 28px rgba(0, 0, 0, 0.16);
display: flex;
flex-direction: column;
overflow: hidden;
}
#mo-drawer .mo-content {
padding: 10px;
height: 100%;
overflow: auto;
}
/* Optional: hide on mobile */
@media (max-width: 768px) {
#mo-drawer {
display: none;
}
}

View File

@@ -0,0 +1,50 @@
frappe.provide("manufacturing_overview.drawer");
manufacturing_overview.drawer._bundle_loaded = false;
manufacturing_overview.drawer.ensure_drawer = function () {
if (document.getElementById("mo-drawer")) return;
const el = document.createElement("div");
el.id = "mo-drawer";
el.innerHTML = `
<div class="mo-hover-bar" title="Manufacturing Overview"></div>
<div class="mo-panel">
<div class="mo-content">
<div id="mo-vue-root">
<div style="opacity:.7;padding:8px 2px;">Hover to load…</div>
</div>
</div>
</div>
`;
document.body.appendChild(el);
const loadBundleOnce = async () => {
if (manufacturing_overview.drawer._bundle_loaded) return;
manufacturing_overview.drawer._bundle_loaded = true;
// Lazy-load the heavy Vue bundle
await frappe.require("manufacturing_overview.bundle.js"); // bundle name, not a path
if (manufacturing_overview.drawer.mount_vue) {
manufacturing_overview.drawer.mount_vue("#mo-vue-root");
} else {
console.error(
"[manufacturing_overview] manufacturing_overview.bundle.js loaded, but mount function is missing."
);
}
};
// Load on first hover
el.addEventListener("mouseenter", loadBundleOnce, { once: true });
// Click toggles pinned open/close (also loads bundle)
el.querySelector(".mo-hover-bar").addEventListener("click", async () => {
el.classList.toggle("mo-open");
await loadBundleOnce();
});
};
// Ensure injection after desk bootstraps
frappe.after_ajax(() => manufacturing_overview.drawer.ensure_drawer());

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
{"version":3,"file":"manufacturing_overview.min.js","sources":["../../../../apps/manufacturing_overview/manufacturing_overview/public/js/manufacturing_overview_row.vue?rollup-plugin-vue=script.js","../../../../apps/manufacturing_overview/manufacturing_overview/public/js/manufacturing_overview_desk.vue?rollup-plugin-vue=script.js","../../../../apps/manufacturing_overview/manufacturing_overview/public/js/manufacturing_overview_page.js"],"sourcesContent":["//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n\nexport default {\n name: \"ManufacturingOverviewRow\",\n props: [\n \"qty\",\n \"item_name\",\n \"item_code\",\n \"customer\",\n \"delivery_date\",\n \"status\",\n \"link\",\n \"reference\",\n \"due_in\",\n ],\n methods: {\n pushRoute(link) {\n frappe.router.push_state(link);\n },\n },\n};\n","//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n\nimport ManufacturingOverviewRow from \"./manufacturing_overview_row.vue\";\n\nexport default {\n components: {\n ManufacturingOverviewRow,\n },\n data() {\n return {\n salesorderData: [\n {\n customer: \"\",\n delivery_date: \"\",\n link: \"\",\n name: \"\",\n item_name: \"Loading...\",\n item_code: \"\",\n qty: \"\",\n sales_order: \"\",\n status: \"Unknown\",\n due_in: 0,\n },\n ],\n timer: \"\",\n origin: window.location.origin,\n userPermissions: {},\n };\n },\n created() {\n this.fetchEventsList();\n this.timer = setInterval(this.fetchEventsList, 30000);\n },\n methods: {\n fetchEventsList() {\n let self = this;\n frappe.call({\n method:\n \"manufacturing_overview.manufacturing_overview.api.getSalesorderOverviewList\",\n async: true,\n args: {},\n callback: function (r) {\n if (r.message) {\n self.salesorderData = r.message;\n }\n },\n });\n },\n cancelAutoUpdate() {\n clearInterval(this.timer);\n },\n beforeDestroy() {\n clearInterval(this.timer);\n },\n },\n};\n","import ManufacturingOverviewDesk from \"./manufacturing_overview_desk.vue\";\n\n$(document).ready(function () {\n $(\".layout-main-section-wrapper\").after('<div class=\"col-12 col-lg-4 layout-main-section-wrapper\" id=\"manufacturing-overview-body\"></div>');\n var pod = new Vue({\n el: \"#manufacturing-overview-body\",\n render(h) {\n return h(ManufacturingOverviewDesk, {});\n }\n });\n});"],"names":["name","props","methods","pushRoute","link","frappe","router","push_state","components","data","salesorderData","customer","delivery_date","item_name","item_code","qty","sales_order","status","due_in","timer","origin","window","location","userPermissions","created","this","fetchEventsList","setInterval","let","self","call","method","async","args","callback","r","message","cancelAutoUpdate","clearInterval","beforeDestroy","$","document","ready","after","Vue","el","render","h","ManufacturingOverviewDesk"],"mappings":"+BA4De,CACbA,KAAM,2BACNC,MAAO,CACL,MACA,YACA,YACA,WACA,gBACA,SACA,OACA,YACA,UAEFC,QAAS,CACPC,mBAAUC,GACRC,OAAOC,OAAOC,WAAWH,0mDCzChB,CACbI,WAAY,o8DAGZC,gBACE,MAAO,CACLC,eAAgB,CACd,CACEC,SAAU,GACVC,cAAe,GACfR,KAAM,GACNJ,KAAM,GACNa,UAAW,aACXC,UAAW,GACXC,IAAK,GACLC,YAAa,GACbC,OAAQ,UACRC,OAAQ,IAGZC,MAAO,GACPC,OAAQC,OAAOC,SAASF,OACxBG,gBAAiB,KAGrBC,mBACEC,KAAKC,kBACLD,KAAKN,MAAQQ,YAAYF,KAAKC,gBAAiB,MAEjDxB,QAAS,CACPwB,2BACEE,IAAIC,EAAOJ,KACXpB,OAAOyB,KAAK,CACVC,OACE,8EACFC,OAAO,EACPC,KAAM,GACNC,SAAU,SAAUC,GACdA,EAAEC,UACJP,EAAKnB,eAAiByB,EAAEC,aAKhCC,4BACEC,cAAcb,KAAKN,QAErBoB,yBACED,cAAcb,KAAKN,8vFChFzBqB,EAAEC,UAAUC,MAAM,WACdF,EAAE,gCAAgCG,MAAM,oGAC9B,IAAIC,IAAI,CACdC,GAAI,+BACJC,gBAAOC,GACH,OAAOA,EAAEC,EAA2B"}

View File

@@ -1,90 +0,0 @@
<template>
<div class="col-12 col-lg-4 layout-main-section-wrapper">
<div class="layout-main-section">
<div class="widget links-widget-box" style="height: auto">
<div class="widget-head">
<div>
<div class="widget-subtitle"></div>
</div>
<div class="widget-control"></div>
</div>
<div class="widget-body">
<manufacturing-overview-row
v-for="so in salesorderData"
:key="so.name"
v-bind:qty="so.qty"
v-bind:item_name="so.item_name"
v-bind:item_code="so.item_code"
v-bind:customer="so.customer"
v-bind:delivery_date="so.delivery_date"
v-bind:status="so.status"
v-bind:link="so.link"
v-bind:reference="so.parent"
v-bind:due_in="so.due_in"
>
</manufacturing-overview-row>
</div>
</div>
</div>
</div>
</template>
<script>
import ManufacturingOverviewRow from "./manufacturing_overview_row.vue";
export default {
components: {
ManufacturingOverviewRow,
},
data() {
return {
salesorderData: [
{
customer: "",
delivery_date: "",
link: "",
name: "",
item_name: "Loading...",
item_code: "",
qty: "",
sales_order: "",
status: "Unknown",
due_in: 0,
},
],
timer: "",
origin: window.location.origin,
userPermissions: {},
};
},
created() {
this.fetchEventsList();
this.timer = setInterval(this.fetchEventsList, 30000);
},
methods: {
fetchEventsList() {
let self = this;
frappe.call({
method:
"manufacturing_overview.manufacturing_overview.api.getSalesorderOverviewList",
async: true,
args: {},
callback: function (r) {
if (r.message) {
self.salesorderData = r.message;
}
},
});
},
cancelAutoUpdate() {
clearInterval(this.timer);
},
beforeDestroy() {
clearInterval(this.timer);
},
},
};
</script>
<style>
</style>

View File

@@ -0,0 +1,52 @@
frappe.provide("manufacturing_overview.drawer");
manufacturing_overview.drawer._bundle_loaded = false;
manufacturing_overview.drawer.ensure_drawer = function () {
if (document.getElementById("mo-drawer")) return;
const el = document.createElement("div");
el.id = "mo-drawer";
el.innerHTML = `
<div class="mo-hover-bar" title="Manufacturing Overview"></div>
<div class="mo-panel">
<div class="mo-content">
<div id="mo-vue-root">
<div style="opacity:.7;padding:8px 2px;">Hover to load…</div>
</div>
</div>
</div>
`;
document.body.appendChild(el);
const loadBundleOnce = async () => {
if (manufacturing_overview.drawer._bundle_loaded) return;
manufacturing_overview.drawer._bundle_loaded = true;
// This must exist at: /assets/manufacturing_overview/js/manufacturing_overview.bundle.js
await frappe.require("manufacturing_overview.bundle.js");
if (manufacturing_overview.drawer.mount_vue) {
manufacturing_overview.drawer.mount_vue("#mo-vue-root");
} else {
console.error(
"[manufacturing_overview] Bundle loaded but mount function missing."
);
}
};
// Load bundle on first hover (once)
el.addEventListener("mouseenter", loadBundleOnce, { once: true });
// Allow pin open/close via click on the bar
el.querySelector(".mo-hover-bar").addEventListener("click", async () => {
el.classList.toggle("mo-open");
await loadBundleOnce();
});
};
// Ensure injection after desk bootstrap
frappe.after_ajax(() => {
manufacturing_overview.drawer.ensure_drawer();
});

View File

@@ -0,0 +1,14 @@
import { createApp } from "vue";
import ManufacturingOverviewDesk from "./manufacturing_overview_desk.vue";
manufacturing_overview.drawer.mount_vue = function (selector) {
const mountEl = document.querySelector(selector);
if (!mountEl) return;
// prevent remount
if (mountEl.__mo_app__) return;
const app = createApp(ManufacturingOverviewDesk, {});
mountEl.__mo_app__ = app;
app.mount(mountEl);
};

View File

@@ -0,0 +1,84 @@
<template>
<div class="widget links-widget-box" style="height: auto">
<div class="widget-head">
<div class="widget-label">
<div class="widget-title">
<span class="ellipsis" title="Manufacturing Overview">
Manufacturing Overview
</span>
</div>
<div class="widget-subtitle"></div>
</div>
<div class="widget-control"></div>
</div>
<div class="widget-body">
<manufacturing-overview-row
v-for="so in salesorderData"
:key="so.name"
:qty="so.qty"
:item_name="so.item_name"
:item_code="so.item_code"
:customer="so.customer"
:delivery_date="so.delivery_date"
:status="so.status"
:link="so.link"
:reference="so.parent"
:due_in="so.due_in"
/>
</div>
</div>
</template>
<script>
import ManufacturingOverviewRow from "./manufacturing_overview_row.vue";
export default {
components: { ManufacturingOverviewRow },
data() {
return {
salesorderData: [
{
customer: "",
delivery_date: "",
link: "",
name: "",
item_name: "Loading...",
item_code: "",
qty: "",
sales_order: "",
status: "Unknown",
due_in: 0,
},
],
timer: null,
};
},
created() {
this.fetchEventsList();
this.timer = setInterval(this.fetchEventsList, 10000);
},
beforeUnmount() {
clearInterval(this.timer);
},
methods: {
fetchEventsList() {
frappe.call({
method:
"manufacturing_overview.manufacturing_overview.api.getSalesorderOverviewList",
async: true,
args: {},
callback: (r) => {
if (r.message) this.salesorderData = r.message;
},
});
},
},
};
</script>
<style scoped>
.widget-body {
padding-top: 6px;
}
</style>

View File

@@ -1,28 +1,41 @@
<template> <template>
<a @click="pushRoute(link)" class="row link-item text-wrap ellipsis onbpoard-spotlight" type="Link"> <a @click="pushRoute(link)" class="row link-item text-wrap ellipsis" role="link">
<div class="col col-xs-8"> <div class="col col-xs-8">
<span class="indicator-pill no-margin" v-bind:class="{ <span
red: status === 'No Work Order', class="indicator-pill no-margin"
blue: status === 'Partially Delivered', :class="{
gray: status === 'Fully Delivered', red: status === 'No Work Order',
green: status === 'In Warehouse', blue: status === 'Partially Delivered',
yellow: status === 'To Produce', gray: status === 'Fully Delivered',
grey: status === 'Unknown', green: status === 'In Warehouse',
}"></span> yellow: status === 'To Produce',
grey: status === 'Unknown',
}"
></span>
<span class="widget-subtitle">{{ qty }}</span> - <span class="widget-subtitle">{{ qty }}</span> -
<span class="widget-title">{{ item_name }}</span> <span class="widget-title">{{ item_name }}</span>
<div> <div>
<small v-if="customer && item_code" class="color-secondary">{{ customer }} - <small v-if="customer && item_code" class="color-secondary">
<a @click="pushRoute('/app/item/' + item_code)" type="Link">{{ {{ customer }} -
item_code <a
}}</a></small> @click.stop.prevent="pushRoute('/desk/item/' + item_code)"
<small v-else-if="customer" class="color-secondary">{{ role="link"
customer >
}}</small> {{ item_code }}
<small v-else-if="item_code" class="color-secondary">{{ </a>
item_code </small>
}}</small>
<small v-else-if="customer" class="color-secondary">
{{ customer }}
</small>
<small v-else-if="item_code" class="color-secondary">
{{ item_code }}
</small>
</div> </div>
<div> <div>
<small class="color-secondary">{{ reference }}</small> <small class="color-secondary">{{ reference }}</small>
</div> </div>
@@ -31,9 +44,11 @@
<div v-if="due_in < 0" class="text-muted ellipsis color-secondary col col-xs-4 text-right"> <div v-if="due_in < 0" class="text-muted ellipsis color-secondary col col-xs-4 text-right">
<b style="color: red">{{ delivery_date }}</b> <b style="color: red">{{ delivery_date }}</b>
</div> </div>
<div v-else-if="due_in === 0" class="text-muted ellipsis color-secondary col col-xs-4 text-right"> <div v-else-if="due_in === 0" class="text-muted ellipsis color-secondary col col-xs-4 text-right">
<b style="color: black">{{ delivery_date }}</b> <b style="color: black">{{ delivery_date }}</b>
</div> </div>
<div v-else class="text-muted ellipsis color-secondary col col-xs-4 text-right"> <div v-else class="text-muted ellipsis color-secondary col col-xs-4 text-right">
{{ delivery_date }} {{ delivery_date }}
</div> </div>
@@ -62,6 +77,4 @@ export default {
}; };
</script> </script>
<style> <style scoped></style>
</style>

View File

@@ -1,11 +0,0 @@
import ManufacturingOverviewDesk from "./manufacturing_overview_desk.vue";
$(document).ready(function () {
$(".layout-main-section-wrapper").after('<div class="col-12 col-lg-4 layout-main-section-wrapper" id="manufacturing-overview-body"></div>');
var pod = new Vue({
el: "#manufacturing-overview-body",
render(h) {
return h(ManufacturingOverviewDesk, {});
}
});
});

View File

@@ -0,0 +1,19 @@
import { createApp } from "vue";
import ManufacturingOverviewDesk from "./js/manufacturing_overview_drawer/manufacturing_overview_desk.vue";
frappe.provide("manufacturing_overview.drawer");
/**
* Called by drawer_bootstrap.bundle.js after lazy-loading this bundle
*/
manufacturing_overview.drawer.mount_vue = function (selector = "#mo-vue-root") {
const el = document.querySelector(selector);
if (!el) return;
// Prevent double mounting if user hovers multiple times
if (el.__mo_vue_app__) return;
const app = createApp(ManufacturingOverviewDesk);
el.__mo_vue_app__ = app;
app.mount(el);
};

View File

@@ -0,0 +1,66 @@
#mo-drawer {
position: fixed;
top: 0;
right: 0;
height: 100vh;
width: 390px;
/* collapsed: show thin bar */
transform: translateX(calc(100% - 12px));
transition: transform 160ms ease;
z-index: 1050;
}
#mo-drawer:hover,
#mo-drawer.mo-open {
transform: translateX(0);
}
#mo-drawer .mo-hover-bar {
position: absolute;
top: 0;
left: 0;
width: 12px;
height: 100%;
cursor: pointer;
background: var(--primary, #2563eb);
opacity: 0.35;
}
#mo-drawer:hover .mo-hover-bar {
opacity: 0.55;
}
#mo-drawer .mo-panel {
position: absolute;
top: 0;
left: 12px;
width: calc(100% - 12px);
height: 100%;
background: var(--card-bg, #fff);
border-left: 1px solid var(--border-color, #e5e7eb);
box-shadow: -10px 0 28px rgba(0, 0, 0, 0.16);
display: flex;
flex-direction: column;
overflow: hidden;
}
#mo-drawer .mo-content {
padding: 10px;
height: 100%;
overflow: auto;
}
/* Optional: hide on mobile */
@media (max-width: 768px) {
#mo-drawer {
display: none;
}
}