Compare commits
9 Commits
version-16
...
version-15
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78e1831314 | ||
| b1935a3520 | |||
| cd98dffbab | |||
| 43d9b0dc0f | |||
| 2670bb83c7 | |||
| cccf121910 | |||
| f8b9e28258 | |||
| 5161c00efc | |||
| 6f1a9d1219 |
@@ -15,13 +15,7 @@ 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 = [
|
app_include_js = "manufacturing_overview.bundle.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"
|
||||||
@@ -110,6 +104,12 @@ app_include_css = [
|
|||||||
# }
|
# }
|
||||||
# }
|
# }
|
||||||
|
|
||||||
|
# doc_events = {
|
||||||
|
# "Subcontracting Receipt": {
|
||||||
|
# "on_submit": "manufacturing_overview.manufacturing_overview.productionplan.update_productionplan_status",
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
|
||||||
# Scheduled Tasks
|
# Scheduled Tasks
|
||||||
# ---------------
|
# ---------------
|
||||||
|
|
||||||
|
|||||||
@@ -1,174 +1,194 @@
|
|||||||
import frappe
|
import frappe
|
||||||
|
from functools import reduce
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from typing import Dict, List
|
|
||||||
|
# frappe.utils.logger.set_log_level("DEBUG")
|
||||||
|
# logger = frappe.logger("manufacturing_overview", allow_site=True)
|
||||||
|
|
||||||
|
#logger.debug(f"String")
|
||||||
|
|
||||||
|
|
||||||
def get_due_in_days(d):
|
def getDueInDays(d):
|
||||||
return (d - date.today()).days
|
delta = d - date.today()
|
||||||
|
return delta.days
|
||||||
|
|
||||||
|
|
||||||
def format_date(d):
|
def getWorkOrder(soItemName):
|
||||||
# keep your output format
|
return frappe.get_all("Work Order", filters={"sales_order_item": soItemName})
|
||||||
return frappe.utils.formatdate(d, "dd.MM.yyyy")
|
|
||||||
|
def getPurchaseOrder(soItemName):
|
||||||
|
|
||||||
|
return frappe.get_all("Purchase Order Item", filters={"sales_order_item": soItemName}, fields=['parent'])
|
||||||
|
|
||||||
|
|
||||||
def calculate_status(item):
|
def getAmountInWarehouses(item_code):
|
||||||
# item.qty is still the original ordered qty here (before subtracting delivered)
|
bins = frappe.get_all("Bin", fields=["actual_qty"], filters=[
|
||||||
if item.delivered_qty > 0 and item.delivered_qty < item.qty:
|
["actual_qty", ">", 0], ["item_code", "=", item_code]])
|
||||||
return "Partially Delivered"
|
return reduce(lambda x, y: x + y,
|
||||||
elif item.delivered_qty >= item.qty:
|
map(lambda b: b["actual_qty"], bins), 0)
|
||||||
return "Fully Delivered"
|
|
||||||
elif item.currentWarehouseQty >= item.qty:
|
|
||||||
return "In Warehouse"
|
def shortenCustomerName(customer):
|
||||||
elif item.work_order_qty > 0:
|
shortName = frappe.get_value(
|
||||||
return "To Produce"
|
'Customer', customer, 'short_name')
|
||||||
|
if shortName is not None:
|
||||||
|
return shortName
|
||||||
else:
|
else:
|
||||||
return "No Work Order"
|
return customer
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
soi.name,
|
|
||||||
soi.item_name,
|
|
||||||
soi.item_group,
|
|
||||||
soi.qty,
|
|
||||||
soi.actual_qty,
|
|
||||||
soi.delivered_qty,
|
|
||||||
soi.delivery_date,
|
|
||||||
soi.item_code,
|
|
||||||
soi.work_order_qty,
|
|
||||||
soi.parent,
|
|
||||||
soi.production_plan_qty,
|
|
||||||
so.status AS parentStatus,
|
|
||||||
so.customer AS customer_name
|
|
||||||
FROM `tabSales Order Item` soi
|
|
||||||
INNER JOIN `tabSales Order` so ON soi.parent = so.name
|
|
||||||
WHERE
|
|
||||||
soi.delivered_qty < soi.qty
|
|
||||||
AND so.status = 'To Deliver and Bill'
|
|
||||||
AND soi.item_group = 'Produkte'
|
|
||||||
ORDER BY
|
|
||||||
soi.delivery_date, soi.item_code, soi.name
|
|
||||||
""",
|
|
||||||
as_dict=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_customer_short_names(customers: List[str]) -> Dict[str, str]:
|
|
||||||
if not customers:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
rows = frappe.get_all(
|
|
||||||
"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
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_total_warehouse_qty(item_codes: List[str]) -> Dict[str, float]:
|
|
||||||
if not item_codes:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# Sum actual_qty across all bins with qty > 0 for those item codes
|
|
||||||
rows = frappe.db.sql(
|
|
||||||
"""
|
|
||||||
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():
|
def generateProductionOverviewCacheData():
|
||||||
salesOrderItems = fetch_sales_order_items()
|
salesOrderItems = frappe.db.sql(
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
`tabSales Order Item`.name,
|
||||||
|
`tabSales Order Item`.item_name,
|
||||||
|
`tabSales Order Item`.item_group,
|
||||||
|
`tabSales Order Item`.qty,
|
||||||
|
`tabSales Order Item`.actual_qty,
|
||||||
|
`tabSales Order Item`.delivered_qty,
|
||||||
|
`tabSales Order Item`.delivery_date AS date,
|
||||||
|
`tabSales Order Item`.item_code,
|
||||||
|
`tabSales Order Item`.work_order_qty,
|
||||||
|
`tabSales Order Item`.parent,
|
||||||
|
`tabSales Order Item`.production_plan_qty,
|
||||||
|
`tabSales Order`.status as parentStatus
|
||||||
|
FROM
|
||||||
|
`tabSales Order Item`
|
||||||
|
INNER JOIN `tabSales Order`
|
||||||
|
ON
|
||||||
|
`tabSales Order Item`.parent = `tabSales Order`.name
|
||||||
|
WHERE
|
||||||
|
delivered_qty < qty and
|
||||||
|
status = 'To Deliver and Bill' and
|
||||||
|
item_group = "Produkte"
|
||||||
|
ORDER BY
|
||||||
|
date, item_code, name
|
||||||
|
""", as_dict=1)
|
||||||
|
|
||||||
if not salesOrderItems:
|
|
||||||
return []
|
|
||||||
|
|
||||||
# collect uniques
|
currentWarehouseQtyList = []
|
||||||
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)
|
for soItem in salesOrderItems:
|
||||||
customer_short = fetch_customer_short_names(customers)
|
soItem.currentWarehouseQty = calculateCurrentWarehouseQty(
|
||||||
|
soItem.item_code, soItem.qty, currentWarehouseQtyList)
|
||||||
|
|
||||||
# Running "allocation" per item_code (your currentWarehouseQty logic, but O(n))
|
soItem.totalWarehouseQty = getAmountInWarehouses(soItem.item_code)
|
||||||
# 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:
|
soItem.due_in = getDueInDays(soItem.date)
|
||||||
item_code = r.get("item_code")
|
|
||||||
if not item_code:
|
soItem.customer = shortenCustomerName(frappe.get_value(
|
||||||
r["totalWarehouseQty"] = 0
|
'Sales Order', soItem.parent, 'customer'))
|
||||||
r["currentWarehouseQty"] = 0
|
|
||||||
|
soItem.direct_po = getPurchaseOrder(soItem.name)
|
||||||
|
if len(soItem.direct_po) >= 1:
|
||||||
|
soItem.direct_po = soItem.direct_po[0]['parent']
|
||||||
else:
|
else:
|
||||||
fetched = float(total_wh_qty.get(item_code, 0))
|
soItem.direct_po = None
|
||||||
|
soItem.direct_wo = getWorkOrder(soItem.name)
|
||||||
|
if len(soItem.direct_wo) >= 1:
|
||||||
|
soItem.direct_wo = soItem.direct_wo[0]['name']
|
||||||
|
else:
|
||||||
|
soItem.direct_wo = None
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
# Update remaining for next occurrence: subtract ordered qty (old behavior)
|
|
||||||
remaining_after_current[item_code] = current_qty - float(r.get("qty") or 0)
|
|
||||||
|
|
||||||
r["totalWarehouseQty"] = fetched
|
soItem = formatDate(soItem)
|
||||||
r["currentWarehouseQty"] = current_qty
|
soItem = calculateStatus(soItem)
|
||||||
|
soItem.qty = soItem.qty - soItem.delivered_qty
|
||||||
|
soItem.link = '/app/sales-order/' + soItem.parent
|
||||||
|
|
||||||
# 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
|
return salesOrderItems
|
||||||
|
|
||||||
|
|
||||||
|
def generatePurchaseOrderOverviewCacheData():
|
||||||
|
purchaseOrderItems = frappe.db.sql(
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
`tabPurchase Order Item`.name,
|
||||||
|
`tabPurchase Order Item`.item_name,
|
||||||
|
`tabPurchase Order Item`.item_group,
|
||||||
|
`tabPurchase Order Item`.qty,
|
||||||
|
`tabPurchase Order Item`.schedule_date AS date,
|
||||||
|
`tabPurchase Order Item`.item_code,
|
||||||
|
`tabPurchase Order Item`.parent,
|
||||||
|
`tabPurchase Order Item`.fg_item,
|
||||||
|
`tabPurchase Order`.status as parentStatus
|
||||||
|
FROM
|
||||||
|
`tabPurchase Order Item`
|
||||||
|
INNER JOIN `tabPurchase Order`
|
||||||
|
ON
|
||||||
|
`tabPurchase Order Item`.parent = `tabPurchase Order`.name
|
||||||
|
WHERE
|
||||||
|
status = 'To Receive and Bill'
|
||||||
|
ORDER BY
|
||||||
|
date, item_code, name
|
||||||
|
""", as_dict=1)
|
||||||
|
|
||||||
|
for poItem in purchaseOrderItems:
|
||||||
|
poItem = formatDate(poItem)
|
||||||
|
poItem.customer = frappe.get_value("Purchase Order", poItem.parent, 'supplier_name')
|
||||||
|
|
||||||
|
poItem.fg_item_name = frappe.get_value("Item", poItem.fg_item, 'item_name')
|
||||||
|
poItem.direct_po = poItem.parent
|
||||||
|
|
||||||
|
poItem.link = '/app/purchase-order/' + poItem.parent
|
||||||
|
|
||||||
|
poItem.status = 'Unknown'
|
||||||
|
|
||||||
|
return purchaseOrderItems
|
||||||
|
|
||||||
|
|
||||||
|
def calculateCurrentWarehouseQty(item_code, qty, currentWarehouseQtyList):
|
||||||
|
for warehouseQty in currentWarehouseQtyList:
|
||||||
|
|
||||||
|
if warehouseQty['item_code'] == item_code:
|
||||||
|
returnQty = warehouseQty['qty']
|
||||||
|
warehouseQty['qty'] = warehouseQty['qty'] - qty
|
||||||
|
return returnQty
|
||||||
|
|
||||||
|
fetchedWarehouseQty = getAmountInWarehouses(item_code)
|
||||||
|
currentWarehouseQtyList.append(
|
||||||
|
{'item_code': item_code, 'qty': fetchedWarehouseQty - qty})
|
||||||
|
return fetchedWarehouseQty
|
||||||
|
|
||||||
|
|
||||||
|
def formatDate(item):
|
||||||
|
item.date = frappe.utils.formatdate(
|
||||||
|
item.date, 'dd.MM.yyyy')
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
def calculateStatus(item):
|
||||||
|
if item.delivered_qty > 0 and item.delivered_qty < item.qty:
|
||||||
|
item.status = 'Partially Delivered'
|
||||||
|
elif item.delivered_qty >= item.qty:
|
||||||
|
item.status = 'Fully Delivered'
|
||||||
|
elif item.currentWarehouseQty >= item.qty:
|
||||||
|
item.status = 'In Warehouse'
|
||||||
|
elif item.work_order_qty > 0:
|
||||||
|
item.status = 'To Produce'
|
||||||
|
else:
|
||||||
|
item.status = 'No Work Order'
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def getSalesorderOverviewList():
|
def getSalesorderOverviewList():
|
||||||
cache_key = "production_overview"
|
salesOrderItems = frappe.cache().get_value('production_overview', expires=True)
|
||||||
salesOrderItems = frappe.cache().get_value(cache_key, expires=True)
|
|
||||||
|
|
||||||
if salesOrderItems is None:
|
if salesOrderItems is None:
|
||||||
salesOrderItems = generateProductionOverviewCacheData()
|
salesOrderItems = generateProductionOverviewCacheData()
|
||||||
frappe.cache().set_value(cache_key, salesOrderItems, expires_in_sec=60)
|
frappe.cache().set_value('production_overview', salesOrderItems,expires_in_sec=600)
|
||||||
|
|
||||||
return salesOrderItems
|
return salesOrderItems
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def getPurchaseOrderOverviewList():
|
||||||
|
purchaseOrderItems = frappe.cache().get_value('purchaseOrder_items', expires=True)
|
||||||
|
|
||||||
|
if purchaseOrderItems is None:
|
||||||
|
purchaseOrderItems = generatePurchaseOrderOverviewCacheData()
|
||||||
|
frappe.cache().set_value('purchaseOrder_items', purchaseOrderItems, expires_in_sec=600)
|
||||||
|
|
||||||
|
return purchaseOrderItems
|
||||||
@@ -1,59 +1,109 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from erpnext.stock.get_item_details import get_default_bom
|
from erpnext.stock.get_item_details import get_default_bom
|
||||||
|
from frappe.utils import (
|
||||||
|
nowdate
|
||||||
|
)
|
||||||
|
|
||||||
# frappe.utils.logger.set_log_level("DEBUG")
|
frappe.utils.logger.set_log_level("DEBUG")
|
||||||
# logger = frappe.logger("manufacturing_overview",
|
logger = frappe.logger("manufacturing_overview",
|
||||||
# allow_site=True, file_count=50)
|
allow_site=True, file_count=50)
|
||||||
|
# logger.debug(f"{wo_id}")
|
||||||
|
|
||||||
# logger.debug(f"{current_value} + {value} = {updated_value}")
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def makepp(sales_order):
|
def makepp(sales_order):
|
||||||
"""
|
"""
|
||||||
Check if Production Plan already exists and break when it does.
|
Check if Production Plan already exists and break when it does.
|
||||||
"""
|
"""
|
||||||
so = frappe.get_doc("Sales Order", sales_order)
|
so = frappe.get_doc("Sales Order", sales_order)
|
||||||
|
|
||||||
checkpp = frappe.get_all('Production Plan Item', filters={'sales_order': so.name}, fields=[
|
checkpp = frappe.get_all('Production Plan Item', filters={'sales_order': so.name}, fields=[
|
||||||
'name', 'parent'])
|
'name', 'parent'])
|
||||||
|
|
||||||
if checkpp:
|
if checkpp:
|
||||||
return {'status': 400,
|
return {'status': 400,
|
||||||
'docname': [checkpp[0].parent]}
|
'docname': [checkpp[0].parent]}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Create Production Plan
|
Create Production Plan
|
||||||
"""
|
"""
|
||||||
sales_orders = [{
|
sales_orders = [{
|
||||||
"sales_order": so.name,
|
"sales_order": so.name,
|
||||||
"customer": so.customer,
|
"customer": so.customer,
|
||||||
"sales_order_date": so.transaction_date,
|
"sales_order_date": so.transaction_date,
|
||||||
"grand_total": so.grand_total,
|
"grand_total": so.grand_total,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
po_items = []
|
po_items = []
|
||||||
for item in so.items:
|
for item in so.items:
|
||||||
i = {
|
i = {
|
||||||
"item_code": item.item_code,
|
"item_code": item.item_code,
|
||||||
"bom_no": get_default_bom(item.item_code),
|
"bom_no": get_default_bom(item.item_code),
|
||||||
"planned_qty": item.qty,
|
"planned_qty": item.qty,
|
||||||
"planned_start_date": frappe.utils.nowdate(),
|
"planned_start_date": frappe.utils.nowdate(),
|
||||||
"stock_uom": "Stk",
|
"stock_uom": "Stk",
|
||||||
"sales_order": so.name,
|
"sales_order": so.name,
|
||||||
"sales_order_item": item.name
|
"sales_order_item": item.name
|
||||||
}
|
}
|
||||||
|
|
||||||
po_items.append(i)
|
po_items.append(i)
|
||||||
|
|
||||||
pp = frappe.get_doc({
|
pp = frappe.get_doc({
|
||||||
'doctype': 'Production Plan',
|
'doctype': 'Production Plan',
|
||||||
'get_items_from': 'Sales Order',
|
'get_items_from': 'Sales Order',
|
||||||
'sales_orders': sales_orders,
|
'sales_orders': sales_orders,
|
||||||
'po_items': po_items,
|
'po_items': po_items,
|
||||||
'for_warehouse': "Lagerräume - HP"
|
'for_warehouse': "Lagerräume - HP"
|
||||||
})
|
})
|
||||||
pp.insert()
|
pp.insert()
|
||||||
|
|
||||||
return {'status': 201,
|
return {'status': 201,
|
||||||
'docname': [pp.name]}
|
'docname': [pp.name]}
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def fixfinisheditemwopo(production_plan):
|
||||||
|
pp = frappe.get_doc("Production Plan", production_plan)
|
||||||
|
wo_id = frappe.get_all('Work Order', filters={'production_plan_item': pp.po_items[0].name})
|
||||||
|
wo_id = wo_id[0]["name"]
|
||||||
|
|
||||||
|
frappe.delete_doc("Work Order", wo_id)
|
||||||
|
|
||||||
|
|
||||||
|
po = frappe.new_doc("Purchase Order")
|
||||||
|
po.supplier = pp.po_items[0].custom_supplier
|
||||||
|
po.schedule_date = nowdate()
|
||||||
|
po.is_subcontracted = 1
|
||||||
|
|
||||||
|
for row in pp.po_items:
|
||||||
|
po_data = {
|
||||||
|
"fg_item": row.item_code,
|
||||||
|
"warehouse": row.warehouse,
|
||||||
|
"bom": row.bom_no,
|
||||||
|
"production_plan": pp.name,
|
||||||
|
"fg_item_qty": row.planned_qty,
|
||||||
|
"supplier_warehouse": "Externe Arbeiten - HP"
|
||||||
|
}
|
||||||
|
|
||||||
|
for field in [
|
||||||
|
"schedule_date",
|
||||||
|
"qty",
|
||||||
|
"description",
|
||||||
|
"production_plan_item",
|
||||||
|
]:
|
||||||
|
po_data[field] = row.get(field)
|
||||||
|
|
||||||
|
po.append("items", po_data)
|
||||||
|
|
||||||
|
po.set_service_items_for_finished_goods()
|
||||||
|
po.set_missing_values()
|
||||||
|
# po.flags.ignore_mandatory = True
|
||||||
|
# po.flags.ignore_validate = True
|
||||||
|
po.insert()
|
||||||
|
|
||||||
|
# pp.db_set("total_produced_qty", pp.total_planned_qty)
|
||||||
|
# pp.db_set("status", "Completed")
|
||||||
|
|
||||||
|
return {'status': 201,
|
||||||
|
'docname': po.name}
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
#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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
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());
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import './manufacturing_overview_desk.vue';
|
||||||
|
import './manufacturing_overview_page.js';
|
||||||
|
import './manufacturing_overview_row.vue';
|
||||||
|
import './sales_order_addition.js';
|
||||||
|
import './production_plan_addition.js';
|
||||||
189
manufacturing_overview/public/js/manufacturing_overview_desk.vue
Normal file
189
manufacturing_overview/public/js/manufacturing_overview_desk.vue
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
<template>
|
||||||
|
<div class="form-tabs-list">
|
||||||
|
<ul class="nav form-tabs" id="form-tabs" role="tablist">
|
||||||
|
<li class="nav-item show">
|
||||||
|
<a
|
||||||
|
class="nav-link"
|
||||||
|
:class="{ active: currentTab === 0 }"
|
||||||
|
role="tab"
|
||||||
|
@click="changeTab(0)"
|
||||||
|
>
|
||||||
|
Production Overview
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item show">
|
||||||
|
<a
|
||||||
|
class="nav-link"
|
||||||
|
:class="{ active: currentTab === 1 }"
|
||||||
|
role="tab"
|
||||||
|
@click="changeTab(1)"
|
||||||
|
>
|
||||||
|
Purchase Overview
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div class="layout-main-section">
|
||||||
|
<div
|
||||||
|
class="widget quick-list-widget-box"
|
||||||
|
style="height: auto"
|
||||||
|
v-if="currentTab === 0"
|
||||||
|
>
|
||||||
|
<div class="widget-head">
|
||||||
|
<div class="widget-label">
|
||||||
|
<div class="widget-title">
|
||||||
|
<span class="ellipsis" title="Sales Order"
|
||||||
|
>Delivery Overview</span
|
||||||
|
>
|
||||||
|
</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:date="so.date"
|
||||||
|
v-bind:status="so.status"
|
||||||
|
v-bind:link="so.link"
|
||||||
|
v-bind:reference="so.parent"
|
||||||
|
v-bind:due_in="so.due_in"
|
||||||
|
v-bind:direct_wo="so.direct_wo"
|
||||||
|
v-bind:direct_po="so.direct_po"
|
||||||
|
v-bind:rowtype="so"
|
||||||
|
>
|
||||||
|
</manufacturing-overview-row>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="widget quick-list-widget-box"
|
||||||
|
style="height: auto"
|
||||||
|
v-if="currentTab === 1"
|
||||||
|
>
|
||||||
|
<div class="widget-head">
|
||||||
|
<div class="widget-label">
|
||||||
|
<div class="widget-title">
|
||||||
|
<span class="ellipsis" title="Sales Order"
|
||||||
|
>Purchase Overview</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="widget-control"></div>
|
||||||
|
</div>
|
||||||
|
<div class="widget-body">
|
||||||
|
<manufacturing-overview-row
|
||||||
|
v-for="sc in purchaseOrderData"
|
||||||
|
:key="sc.name"
|
||||||
|
v-bind:qty="sc.qty"
|
||||||
|
v-bind:item_name="sc.fg_item_name"
|
||||||
|
v-bind:item_code="sc.fg_item"
|
||||||
|
v-bind:fg_item="sc.fg_item"
|
||||||
|
v-bind:customer="sc.customer"
|
||||||
|
v-bind:date="sc.date"
|
||||||
|
v-bind:status="sc.status"
|
||||||
|
v-bind:direct_po="sc.direct_po"
|
||||||
|
v-bind:link="sc.link"
|
||||||
|
v-bind:rowtype="po"
|
||||||
|
>
|
||||||
|
</manufacturing-overview-row>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ManufacturingOverviewRow from "./manufacturing_overview_row.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
ManufacturingOverviewRow,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
currentTab: 0,
|
||||||
|
salesorderData: [
|
||||||
|
{
|
||||||
|
customer: "",
|
||||||
|
date: "",
|
||||||
|
link: "",
|
||||||
|
name: "",
|
||||||
|
item_name: "Loading...",
|
||||||
|
item_code: "",
|
||||||
|
qty: "",
|
||||||
|
sales_order: "",
|
||||||
|
status: "Unknown",
|
||||||
|
due_in: 0,
|
||||||
|
direct_wo: "",
|
||||||
|
direct_po: "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
purchaseOrderData: [
|
||||||
|
{
|
||||||
|
name: "",
|
||||||
|
customer: "",
|
||||||
|
link: "",
|
||||||
|
item_name: "Loading...",
|
||||||
|
item_code: "",
|
||||||
|
qty: "",
|
||||||
|
date: "",
|
||||||
|
fg_item: "",
|
||||||
|
status: "Unknown",
|
||||||
|
direct_po: "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
timer: "",
|
||||||
|
origin: window.location.origin,
|
||||||
|
userPermissions: {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.fetchEventsList();
|
||||||
|
this.timer = setInterval(this.fetchEventsList, 30000);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
changeTab(id) {
|
||||||
|
let self = this;
|
||||||
|
self.currentTab = id;
|
||||||
|
},
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
frappe.call({
|
||||||
|
method:
|
||||||
|
"manufacturing_overview.manufacturing_overview.api.getPurchaseOrderOverviewList",
|
||||||
|
async: true,
|
||||||
|
args: {},
|
||||||
|
callback: function (r) {
|
||||||
|
if (r.message) {
|
||||||
|
self.purchaseOrderData = r.message;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
cancelAutoUpdate() {
|
||||||
|
clearInterval(this.timer);
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
clearInterval(this.timer);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
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);
|
|
||||||
};
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
<template>
|
|
||||||
<a @click="pushRoute(link)" class="row link-item text-wrap ellipsis" role="link">
|
|
||||||
<div class="col col-xs-8">
|
|
||||||
<span
|
|
||||||
class="indicator-pill no-margin"
|
|
||||||
:class="{
|
|
||||||
red: status === 'No Work Order',
|
|
||||||
blue: status === 'Partially Delivered',
|
|
||||||
gray: status === 'Fully Delivered',
|
|
||||||
green: status === 'In Warehouse',
|
|
||||||
yellow: status === 'To Produce',
|
|
||||||
grey: status === 'Unknown',
|
|
||||||
}"
|
|
||||||
></span>
|
|
||||||
|
|
||||||
<span class="widget-subtitle">{{ qty }}</span> -
|
|
||||||
<span class="widget-title">{{ item_name }}</span>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<small v-if="customer && item_code" class="color-secondary">
|
|
||||||
{{ customer }} -
|
|
||||||
<a
|
|
||||||
@click.stop.prevent="pushRoute('/desk/item/' + item_code)"
|
|
||||||
role="link"
|
|
||||||
>
|
|
||||||
{{ item_code }}
|
|
||||||
</a>
|
|
||||||
</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>
|
|
||||||
<small class="color-secondary">{{ reference }}</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="text-muted ellipsis color-secondary col col-xs-4 text-right">
|
|
||||||
{{ delivery_date }}
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: "ManufacturingOverviewRow",
|
|
||||||
props: [
|
|
||||||
"qty",
|
|
||||||
"item_name",
|
|
||||||
"item_code",
|
|
||||||
"customer",
|
|
||||||
"delivery_date",
|
|
||||||
"status",
|
|
||||||
"link",
|
|
||||||
"reference",
|
|
||||||
"due_in",
|
|
||||||
],
|
|
||||||
methods: {
|
|
||||||
pushRoute(link) {
|
|
||||||
frappe.router.push_state(link);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { createApp } from "vue";
|
||||||
|
|
||||||
|
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>');
|
||||||
|
|
||||||
|
const moapp = createApp(ManufacturingOverviewDesk)
|
||||||
|
|
||||||
|
moapp.mount("#manufacturing-overview-body");
|
||||||
|
});
|
||||||
|
|
||||||
152
manufacturing_overview/public/js/manufacturing_overview_row.vue
Normal file
152
manufacturing_overview/public/js/manufacturing_overview_row.vue
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
@click="pushRoute(link)"
|
||||||
|
@mouseenter="hover = true"
|
||||||
|
@mouseleave="(hover = false), (detailsOpen = false)"
|
||||||
|
class="quick-list-item align-items-start"
|
||||||
|
>
|
||||||
|
<div class="col-1">
|
||||||
|
<span
|
||||||
|
class="indicator-pill no-margin"
|
||||||
|
v-bind:class="{
|
||||||
|
red: status === 'No Work Order',
|
||||||
|
blue: status === 'Partially Delivered',
|
||||||
|
gray: status === 'Fully Delivered',
|
||||||
|
green: status === 'In Warehouse',
|
||||||
|
yellow: status === 'To Produce',
|
||||||
|
grey: status === 'Unknown',
|
||||||
|
}"
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
<div class="col col-xs-6 col-lg-8 overflow-hidden">
|
||||||
|
<span class="ellipsis title"
|
||||||
|
><b>{{ qty }}</b> - {{ item_name }}</span
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<small v-if="fg_item" class="color-secondary">
|
||||||
|
<a
|
||||||
|
class="underline-hover"
|
||||||
|
@click="pushRoute('/app/item/' + fg_item)"
|
||||||
|
v-on:click.stop
|
||||||
|
>{{ fg_item }}</a
|
||||||
|
>
|
||||||
|
</small>
|
||||||
|
<small v-else-if="customer && item_code" class="color-secondary"
|
||||||
|
>{{ customer }} -
|
||||||
|
<a
|
||||||
|
class="underline-hover"
|
||||||
|
@click="pushRoute('/app/item/' + item_code)"
|
||||||
|
v-on:click.stop
|
||||||
|
>{{ item_code }}</a
|
||||||
|
>
|
||||||
|
</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>
|
||||||
|
<small>
|
||||||
|
<span>
|
||||||
|
<a
|
||||||
|
class="underline-hover"
|
||||||
|
@click="pushRoute('/app/sales-order/' + reference)"
|
||||||
|
v-on:click.stop
|
||||||
|
>{{ reference }}</a
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
<span v-if="direct_wo">
|
||||||
|
-
|
||||||
|
<a
|
||||||
|
class="underline-hover"
|
||||||
|
@click="pushRoute('/app/work-order/' + direct_wo)"
|
||||||
|
v-on:click.stop
|
||||||
|
>{{ direct_wo }}</a
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
<span v-if="direct_po && direct_wo">-</span>
|
||||||
|
<span v-if="direct_po">
|
||||||
|
<a
|
||||||
|
class="underline-hover"
|
||||||
|
@click="pushRoute('/app/purchase-order/' + direct_po)"
|
||||||
|
v-on:click.stop
|
||||||
|
>{{ direct_po }}</a
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- v-if="hover" -->
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="text-muted ellipsis color-secondary col col-xs-5 col-lg-3 text-right"
|
||||||
|
>
|
||||||
|
<div class="d-flex justify-content-end flex-column align-items-end">
|
||||||
|
<div class="mb-auto">
|
||||||
|
<b v-if="due_in < 0" style="color: red">{{ date }}</b>
|
||||||
|
<b v-else-if="due_in === 0" style="color: black">{{ date }}</b>
|
||||||
|
<span v-else>{{ date }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- <div @click="getItemDetails(item_code)" v-if="!detailsOpen" v-on:click.stop>
|
||||||
|
Details
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- <div v-if="detailsOpen" :style="{ height: '500px' }"></div> -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "ManufacturingOverviewRow",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
hover: false,
|
||||||
|
detailsOpen: false,
|
||||||
|
currentDetails: ["1", "2"],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
props: [
|
||||||
|
"qty",
|
||||||
|
"item_name",
|
||||||
|
"item_code",
|
||||||
|
"customer",
|
||||||
|
"customershort",
|
||||||
|
"date",
|
||||||
|
"status",
|
||||||
|
"link",
|
||||||
|
"reference",
|
||||||
|
"due_in",
|
||||||
|
"direct_wo",
|
||||||
|
"direct_po",
|
||||||
|
"fg_item",
|
||||||
|
],
|
||||||
|
methods: {
|
||||||
|
pushRoute(link) {
|
||||||
|
frappe.router.push_state(link);
|
||||||
|
},
|
||||||
|
// getItemDetails(item) {
|
||||||
|
// this.detailsOpen = !this.detailsOpen;
|
||||||
|
// frappe.call({
|
||||||
|
// method:
|
||||||
|
// "manufacturing_overview.manufacturing_overview.api.get_bom_tree",
|
||||||
|
// async: true,
|
||||||
|
// args: { item_code: item },
|
||||||
|
// callback: function (r) {
|
||||||
|
// console.log(r.message);
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.underline-hover:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
28
manufacturing_overview/public/js/production_plan_addition.js
Normal file
28
manufacturing_overview/public/js/production_plan_addition.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
frappe.ui.form.on("Production Plan", {
|
||||||
|
refresh: function (frm) {
|
||||||
|
frm.add_custom_button(__('Fix WO/PO'), function () {
|
||||||
|
frappe.call({
|
||||||
|
method: 'manufacturing_overview.manufacturing_overview.productionplan.fixfinisheditemwopo',
|
||||||
|
args: {
|
||||||
|
production_plan: frm.docname
|
||||||
|
},
|
||||||
|
callback: function (r) {
|
||||||
|
if (r.message.status === 201) {
|
||||||
|
frappe.msgprint({
|
||||||
|
title: __('Created'),
|
||||||
|
indicator: 'green',
|
||||||
|
message: __('Purchase Order {0} created.',
|
||||||
|
['<a href="/app/purchase-order/' + r.message.docname + '">' + r.message.docname + '</a>'])
|
||||||
|
});
|
||||||
|
} else if (r.message.status === 400) {
|
||||||
|
frappe.msgprint({
|
||||||
|
title: __('Already exists'),
|
||||||
|
indicator: 'yellow',
|
||||||
|
message: "Oooopsie"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
29
manufacturing_overview/public/js/sales_order_addition.js
Normal file
29
manufacturing_overview/public/js/sales_order_addition.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
frappe.ui.form.on("Sales Order", {
|
||||||
|
refresh: function (frm) {
|
||||||
|
frm.add_custom_button(__('Production Plan'), function () {
|
||||||
|
frappe.call({
|
||||||
|
method: 'manufacturing_overview.manufacturing_overview.productionplan.makepp',
|
||||||
|
args: {
|
||||||
|
sales_order: frm.docname
|
||||||
|
},
|
||||||
|
callback: function (r) {
|
||||||
|
if (r.message.status === 201) {
|
||||||
|
frappe.msgprint({
|
||||||
|
title: __('Created'),
|
||||||
|
indicator: 'green',
|
||||||
|
message: __('Production Plan {0} created.',
|
||||||
|
['<a href="/app/production-plan/' + r.message.docname + '">' + r.message.docname + '</a>'])
|
||||||
|
});
|
||||||
|
} else if (r.message.status === 400) {
|
||||||
|
frappe.msgprint({
|
||||||
|
title: __('Already exists'),
|
||||||
|
indicator: 'yellow',
|
||||||
|
message: __('Production Plan {0} already exists.',
|
||||||
|
['<a href="/app/production-plan/' + r.message.docname + '">' + r.message.docname + '</a>'])
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
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);
|
|
||||||
};
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
#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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user