106 lines
3.4 KiB
Python
106 lines
3.4 KiB
Python
"""Filament-Bedarf aus wartender Queue vs. Spulenrest."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
|
||
_HEX_COLOR_RE = re.compile(r"^#?[0-9a-f]{6}([0-9a-f]{2})?$", re.I)
|
||
|
||
|
||
def _group_key(material_type, color):
|
||
return (
|
||
(material_type or "").strip().lower(),
|
||
(color or "").strip().lower(),
|
||
)
|
||
|
||
|
||
def _is_slicer_hex(color):
|
||
"""Orca/Anycubic speichert oft #26A69A — Lagerfarben heißen „Grau“/„Weiß“."""
|
||
return bool(_HEX_COLOR_RE.match((color or "").strip()))
|
||
|
||
|
||
def _stock_for_need(stock_map, mat, color):
|
||
"""Exakte Farbe zuerst; bei Slice-Hex oder leerer Farbe: Summe gleicher Sorte."""
|
||
key = _group_key(mat, color)
|
||
exact = stock_map.get(key, 0.0)
|
||
if exact > 0.5:
|
||
return exact
|
||
if not color or _is_slicer_hex(color):
|
||
mat_key = (mat or "").strip().lower()
|
||
return sum(
|
||
grams
|
||
for (m, _c), grams in stock_map.items()
|
||
if m == mat_key and grams > 0
|
||
)
|
||
return exact
|
||
|
||
|
||
def compute(conn):
|
||
"""Wartende Jobs × Dateigewicht vs. Spulenrest, gruppiert nach Sorte/Farbe."""
|
||
queue_rows = conn.execute(
|
||
"""
|
||
SELECT q.id, q.spool_id, q.repeat_total, q.repeat_done,
|
||
f.weight_g, f.filaments,
|
||
s.material_type AS spool_material, s.color AS spool_color
|
||
FROM print_queue q
|
||
JOIN files f ON f.id = q.file_id
|
||
LEFT JOIN spools s ON s.id = q.spool_id
|
||
WHERE q.status = 'waiting'
|
||
"""
|
||
).fetchall()
|
||
|
||
needed_map = {}
|
||
for row in queue_rows:
|
||
filaments = row.get("filaments") or []
|
||
first = filaments[0] if isinstance(filaments, list) and filaments else {}
|
||
if not isinstance(first, dict):
|
||
first = {}
|
||
mat = (row.get("spool_material") or first.get("type") or "").strip()
|
||
color = (row.get("spool_color") or first.get("color") or "").strip()
|
||
key = _group_key(mat, color)
|
||
try:
|
||
repeats = max(1, int(row.get("repeat_total") or 1) - int(row.get("repeat_done") or 0))
|
||
except (TypeError, ValueError):
|
||
repeats = 1
|
||
weight = float(row.get("weight_g") or 0) * repeats
|
||
if key not in needed_map:
|
||
needed_map[key] = {
|
||
"material_type": mat,
|
||
"color": color,
|
||
"weight_g": 0.0,
|
||
"jobs": 0,
|
||
}
|
||
needed_map[key]["weight_g"] += weight
|
||
needed_map[key]["jobs"] += 1
|
||
|
||
spool_rows = conn.execute(
|
||
"""
|
||
SELECT material_type, color, COALESCE(SUM(remaining_g), 0) AS remaining_g
|
||
FROM spools
|
||
WHERE archived_at IS NULL
|
||
GROUP BY material_type, color
|
||
"""
|
||
).fetchall()
|
||
|
||
stock_map = {}
|
||
for row in spool_rows:
|
||
key = _group_key(row.get("material_type"), row.get("color"))
|
||
stock_map[key] = stock_map.get(key, 0.0) + float(row.get("remaining_g") or 0)
|
||
|
||
needed = sorted(
|
||
needed_map.values(),
|
||
key=lambda x: (-x["weight_g"], x["material_type"], x["color"]),
|
||
)
|
||
shortfall = []
|
||
for item in needed:
|
||
have = _stock_for_need(stock_map, item["material_type"], item["color"])
|
||
gap = item["weight_g"] - have
|
||
if gap > 0.5:
|
||
shortfall.append({
|
||
**item,
|
||
"remaining_g": have,
|
||
"shortfall_g": gap,
|
||
})
|
||
shortfall.sort(key=lambda x: (-x["shortfall_g"], x["material_type"], x["color"]))
|
||
return {"needed": needed, "shortfall": shortfall}
|