Initial Plast Track MVP
This commit is contained in:
118
backend/app/services/dashboard.py
Normal file
118
backend/app/services/dashboard.py
Normal file
@@ -0,0 +1,118 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, time, timedelta, timezone
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..models import Cycle, Downtime, Machine, ProductionOrder, Scrap
|
||||
from .events import get_open_downtime, get_running_order
|
||||
from .oee import compute_machine_oee
|
||||
|
||||
UTC = timezone.utc
|
||||
|
||||
|
||||
def _today_bounds() -> tuple[datetime, datetime]:
|
||||
start = datetime.combine(datetime.now(UTC).date(), time.min, tzinfo=UTC)
|
||||
return start, start + timedelta(days=1)
|
||||
|
||||
|
||||
def serialize_machine_config(machine: Machine) -> dict:
|
||||
return {
|
||||
"name": machine.name,
|
||||
"brand": machine.brand,
|
||||
"model": machine.model,
|
||||
"is_active": machine.is_active,
|
||||
"cycle_signal_edge": machine.cycle_signal_edge,
|
||||
"debounce_ms": machine.debounce_ms,
|
||||
"min_cycle_time_sec": machine.min_cycle_time_sec,
|
||||
"max_cycle_time_sec": machine.max_cycle_time_sec,
|
||||
"stop_detection_delay_sec": machine.stop_detection_delay_sec,
|
||||
}
|
||||
|
||||
|
||||
def serialize_machine_card(session: Session, machine: Machine) -> dict:
|
||||
running_order = get_running_order(session, machine.id)
|
||||
open_downtime = get_open_downtime(session, machine.id)
|
||||
today_start, today_end = _today_bounds()
|
||||
|
||||
recent_cycles = session.execute(
|
||||
select(Cycle.cycle_time_sec).where(Cycle.machine_id == machine.id).order_by(Cycle.timestamp.desc()).limit(20)
|
||||
).scalars().all()
|
||||
avg_cycle = sum(recent_cycles) / len(recent_cycles) if recent_cycles else 0
|
||||
|
||||
order_produced_qty = 0
|
||||
order_scrap_qty = 0
|
||||
progress = 0
|
||||
if running_order is not None:
|
||||
order_produced_qty = session.execute(
|
||||
select(func.coalesce(func.sum(Cycle.produced_qty), 0)).where(
|
||||
Cycle.production_order_id == running_order.id,
|
||||
Cycle.is_valid.is_(True),
|
||||
)
|
||||
).scalar_one()
|
||||
order_scrap_qty = session.execute(
|
||||
select(func.coalesce(func.sum(Scrap.quantity), 0)).where(Scrap.production_order_id == running_order.id)
|
||||
).scalar_one()
|
||||
progress = min(order_produced_qty / running_order.planned_qty, 1) if running_order.planned_qty else 0
|
||||
|
||||
daily_oee = compute_machine_oee(session, machine.id, today_start, today_end)
|
||||
latest_downtime = session.execute(
|
||||
select(Downtime).where(Downtime.machine_id == machine.id).order_by(Downtime.start_time.desc()).limit(1)
|
||||
).scalar_one_or_none()
|
||||
|
||||
return {
|
||||
"id": machine.id,
|
||||
"code": machine.code,
|
||||
"name": machine.name,
|
||||
"brand": machine.brand,
|
||||
"model": machine.model,
|
||||
"status": machine.status,
|
||||
"status_since": machine.status_since.isoformat() if machine.status_since else None,
|
||||
"powered_on": machine.powered_on,
|
||||
"config": serialize_machine_config(machine),
|
||||
"active_order": None
|
||||
if running_order is None
|
||||
else {
|
||||
"id": running_order.id,
|
||||
"order_number": running_order.order_number,
|
||||
"article_ref": running_order.article_ref,
|
||||
"article_name": running_order.article_name,
|
||||
"mold_ref": running_order.mold_ref,
|
||||
"material_ref": running_order.material_ref,
|
||||
"planned_qty": running_order.planned_qty,
|
||||
"cavities": running_order.cavities,
|
||||
"theoretical_cycle_time_sec": running_order.theoretical_cycle_time_sec,
|
||||
"status": running_order.status,
|
||||
},
|
||||
"cycle_real_avg_sec": round(avg_cycle, 2),
|
||||
"produced_qty": int(order_produced_qty),
|
||||
"scrap_qty": int(order_scrap_qty),
|
||||
"order_progress": round(progress, 4),
|
||||
"today_oee": daily_oee,
|
||||
"open_downtime": None
|
||||
if open_downtime is None
|
||||
else {
|
||||
"id": open_downtime.id,
|
||||
"reason_code": open_downtime.reason_code,
|
||||
"start_time": open_downtime.start_time.isoformat(),
|
||||
"comment": open_downtime.comment,
|
||||
},
|
||||
"last_downtime": None
|
||||
if latest_downtime is None
|
||||
else {
|
||||
"id": latest_downtime.id,
|
||||
"reason_code": latest_downtime.reason_code,
|
||||
"start_time": latest_downtime.start_time.isoformat(),
|
||||
"end_time": latest_downtime.end_time.isoformat() if latest_downtime.end_time else None,
|
||||
"duration_sec": latest_downtime.duration_sec,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def build_dashboard_snapshot(session: Session) -> dict:
|
||||
machines = session.execute(select(Machine).where(Machine.is_active.is_(True)).order_by(Machine.code)).scalars().all()
|
||||
return {
|
||||
"timestamp": datetime.now(UTC).isoformat(),
|
||||
"machines": [serialize_machine_card(session, machine) for machine in machines],
|
||||
}
|
||||
Reference in New Issue
Block a user