382 lines
30 KiB
Python
382 lines
30 KiB
Python
"""Демо-данные портала: склады и инструмент, заявки, инциденты, суточные задания, карта завода, карточки сотрудников."""
|
||
import random
|
||
import struct
|
||
import zlib
|
||
from datetime import datetime, time, timedelta
|
||
|
||
from sqlalchemy.orm import Session
|
||
|
||
from . import media, service, settings_store as ss
|
||
from .models import (DailyTask, Equipment, Incident, IncidentUnit, InventoryItem, InventoryMove, Position, ServiceUnit, Stock,
|
||
TaskEvent, ToolRequest, User, Warehouse)
|
||
|
||
# план завода: помещения в % от картинки (x, y, w, h, цвет заливки)
|
||
ROOMS = [
|
||
(3, 4, 44, 46, (226, 232, 240)), # Цех №1 (с компрессорной)
|
||
(50, 4, 47, 46, (224, 231, 255)), # Цех №2
|
||
(3, 55, 26, 41, (254, 226, 226)), # Котельная
|
||
(32, 55, 28, 41, (220, 252, 231)), # Насосная станция
|
||
(63, 55, 16, 41, (254, 249, 195)), # Подстанция
|
||
(82, 55, 15, 41, (237, 233, 254)), # Склады
|
||
]
|
||
# где стоит оборудование на карте (ключи — как в demo.seed)
|
||
SPOTS = {"lathe": (14, 20), "mill": (30, 20), "comp": (36, 40), "press": (62, 22), "fan": (84, 18),
|
||
"boiler": (16, 76), "pump1": (40, 72), "pump2": (52, 82), "trans": (71, 76),
|
||
# краны — на отдельной карте кранов (в демо она включена и использует тот же план)
|
||
"crane": (70, 30), "crane2": (55, 14), "crane3": (22, 35)}
|
||
CRANES = ("crane", "crane2", "crane3")
|
||
# за каким отделом закреплено оборудование (ключ отдела — как в seed)
|
||
EQ_DEPS = {"lathe": "mech", "mill": "mech", "press": "mech", "fan": "mech", "comp": "mech", "pump1": "mech", "pump2": "mech",
|
||
"boiler": "mech", "trans": "elec", "crane": "crane", "crane2": "crane", "crane3": "crane"}
|
||
ICONS = {"lathe": "lathe", "mill": "lathe", "comp": "compressor", "press": "press", "fan": "fan", "crane": "crane",
|
||
"boiler": "boiler", "pump1": "pump", "pump2": "pump", "trans": "transformer", "crane2": "crane", "crane3": "crane"}
|
||
|
||
|
||
def plan_png(w: int = 1200, h: int = 750) -> bytes:
|
||
"""Схема цехов без подписей (подписи — у иконок и зон складов). Чистый PNG без внешних библиотек."""
|
||
px = [[(246, 247, 249)] * w for _ in range(h)]
|
||
|
||
def rect(x0, y0, x1, y1, col):
|
||
for y in range(max(0, y0), min(h, y1)):
|
||
row = px[y]
|
||
for x in range(max(0, x0), min(w, x1)):
|
||
row[x] = col
|
||
rect(10, 10, w - 10, h - 10, (51, 65, 85))
|
||
rect(18, 18, w - 18, h - 18, (246, 247, 249))
|
||
for x, y, rw, rh, col in ROOMS:
|
||
X0, Y0, X1, Y1 = x * w // 100, y * h // 100, (x + rw) * w // 100, (y + rh) * h // 100
|
||
rect(X0, Y0, X1, Y1, (100, 116, 139))
|
||
rect(X0 + 6, Y0 + 6, X1 - 6, Y1 - 6, col)
|
||
rect((X0 + X1) // 2 - 40, Y1 - 6, (X0 + X1) // 2 + 40, Y1, col) # проём двери
|
||
for x in range(30, w - 30, 60): # разметка проезда
|
||
rect(x, h * 52 // 100, x + 30, h * 52 // 100 + 6, (148, 163, 184))
|
||
raw = bytearray()
|
||
for row in px:
|
||
raw.append(0)
|
||
for c in row:
|
||
raw += bytes(c)
|
||
|
||
def chunk(t, d):
|
||
return struct.pack(">I", len(d)) + t + d + struct.pack(">I", zlib.crc32(t + d) & 0xFFFFFFFF)
|
||
return (b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", struct.pack(">IIBBBBB", w, h, 8, 2, 0, 0, 0))
|
||
+ chunk(b"IDAT", zlib.compress(bytes(raw), 6)) + chunk(b"IEND", b""))
|
||
|
||
|
||
def seed(db: Session, *, users: dict, eq: dict, pos: dict, sch_store, pw: str, now: datetime, rnd: random.Random) -> dict:
|
||
today = now.date()
|
||
|
||
# --- администратор склада, навыки и телефоны ---
|
||
p_store = Position(name="Кладовщик", kind="technician", schedule_id=sch_store.id)
|
||
db.add(p_store)
|
||
db.flush()
|
||
users["lebedev"] = User(username="lebedev", full_name="Лебедев Павел", password_hash=pw, is_storekeeper=True,
|
||
position_id=p_store.id, phone="+7 900 555-01-07", skills="Учёт ТМЦ, выдача инструмента, работа с поставщиками")
|
||
db.add(users["lebedev"])
|
||
skills = {"ivanov": ("+7 900 555-01-01", "Слесарные работы, сварка (НАКС), гидравлика, центровка валов"),
|
||
"kozlov": ("+7 900 555-01-02", "Слесарные работы, станки с ЧПУ, пневматика"),
|
||
"petrov": ("+7 900 555-01-03", "Электромонтаж до и выше 1000 В, релейная защита, ПЛК Siemens"),
|
||
"sidorov": ("+7 900 555-01-04", "Насосы и компрессоры, вибродиагностика, такелаж"),
|
||
"morozova": ("+7 900 555-01-05", "Планирование ТО, анализ отказов, промышленная безопасность"),
|
||
"volkov": ("+7 900 555-01-06", "Регламенты ТО, учёт наработки, закупки запчастей"),
|
||
"orlov": ("+7 900 555-01-08", "Крановое электрооборудование, приборы безопасности кранов, допуск Ростехнадзора")}
|
||
for u, (ph, sk) in skills.items():
|
||
users[u].phone, users[u].skills = ph, sk
|
||
db.flush()
|
||
|
||
# --- отделы ---
|
||
from .models import Department
|
||
chief = Department(name="Служба главного инженера", head_user_id=users["morozova"].id, notes="Планирование и контроль ТО")
|
||
db.add(chief)
|
||
db.flush()
|
||
deps = {"mech": Department(name="Механическая служба", parent_id=chief.id, head_user_id=users["volkov"].id),
|
||
"elec": Department(name="Электрослужба", parent_id=chief.id, head_user_id=users["petrov"].id),
|
||
"store": Department(name="Склад инструмента", parent_id=chief.id, head_user_id=users["lebedev"].id),
|
||
"crane": Department(name="Крановое хозяйство", parent_id=chief.id, head_user_id=users["orlov"].id,
|
||
notes="Краны и грузоподъёмные механизмы; станки этому отделу не показываются")}
|
||
db.add_all(deps.values())
|
||
db.flush()
|
||
for u, d in (("morozova", chief), ("kuznetsova", chief), ("volkov", deps["mech"]), ("ivanov", deps["mech"]), ("kozlov", deps["mech"]),
|
||
("sidorov", deps["mech"]), ("petrov", deps["elec"]), ("lebedev", deps["store"]), ("orlov", deps["crane"])):
|
||
users[u].department_id = d.id
|
||
db.flush()
|
||
|
||
# --- иконки и карта завода ---
|
||
for k, e in eq.items():
|
||
e.icon = "builtin:" + ICONS.get(k, "gear")
|
||
if k in SPOTS:
|
||
e.map_x, e.map_y = SPOTS[k]
|
||
e.is_crane = k in CRANES
|
||
if k in EQ_DEPS:
|
||
e.department_id = deps[EQ_DEPS[k]].id
|
||
ss.set_(db, "map_image", media.save(db, "map", plan_png(), ".png"))
|
||
ss.set_(db, "crane_map", "1")
|
||
|
||
# --- склады и инструмент ---
|
||
wh = {"tool": Warehouse(name="Инструментальная кладовая", zone={"x": 82.5, "y": 56, "w": 14, "h": 19}, notes="Ручной и электроинструмент, измерительный"),
|
||
"cons": Warehouse(name="Склад расходников", zone={"x": 82.5, "y": 76, "w": 14, "h": 19}, notes="Смазки, СИЗ, крепёж"),
|
||
"shop": Warehouse(name="Кладовая цеха №2", zone={"x": 88, "y": 6, "w": 8, "h": 9}, notes="Оперативный запас у участка")}
|
||
db.add_all(wh.values())
|
||
db.flush()
|
||
catalog = [ # (ключ, название, категория, ед., инв. №, количество, склад)
|
||
("drill", "Дрель аккумуляторная Makita DDF485", "Электроинструмент", "шт", "ИН-001", 3, "tool"),
|
||
("grinder", "УШМ Bosch GWS 9-125", "Электроинструмент", "шт", "ИН-002", 2, "tool"),
|
||
("mm", "Мультиметр Fluke 117", "Измерительный", "шт", "ИН-010", 3, "tool"),
|
||
("clamp", "Токоизмерительные клещи UNI-T UT203", "Измерительный", "шт", "ИН-011", 1, "tool"),
|
||
("vibro", "Виброметр Vibrotest 60", "Измерительный", "шт", "ИН-012", 1, "tool"),
|
||
("keys", "Набор ключей комбинированных 8–32", "Ручной инструмент", "компл", "", 6, "tool"),
|
||
("torque", "Ключ динамометрический 40–200 Нм", "Ручной инструмент", "шт", "ИН-020", 2, "tool"),
|
||
("puller", "Съёмник подшипников 3-захватный", "Ручной инструмент", "шт", "ИН-021", 1, "tool"),
|
||
("gloves", "Перчатки х/б с ПВХ", "СИЗ", "пар", "", 200, "cons"),
|
||
("glasses", "Очки защитные", "СИЗ", "шт", "", 25, "cons"),
|
||
("litol", "Смазка Литол-24, 800 г", "Смазки", "шт", "", 30, "cons"),
|
||
("oil", "Масло компрессорное КС-19, 20 л", "Смазки", "кан", "", 6, "cons"),
|
||
("bolts", "Болт М12×40, оцинк.", "Крепёж", "шт", "", 400, "cons"),
|
||
("tape", "Изолента ПВХ 19 мм", "Расходники", "шт", "", 40, "shop"),
|
||
]
|
||
items = {}
|
||
t0 = now - timedelta(days=40)
|
||
for k, name, cat, unit, inv, qty, w in catalog:
|
||
it = InventoryItem(name=name, category=cat, unit=unit, inventory_no=inv, created_at=t0)
|
||
db.add(it)
|
||
db.flush()
|
||
items[k] = it
|
||
db.add(Stock(item_id=it.id, warehouse_id=wh[w].id, qty=qty, since=t0))
|
||
db.add(InventoryMove(item_id=it.id, qty=qty, kind="receive", to_warehouse_id=wh[w].id, by_user_id=users["lebedev"].id,
|
||
at=t0, note="Начальные остатки"))
|
||
db.flush()
|
||
|
||
def move(k, qty, *, src_wh=None, src_user=None, dst_wh=None, dst_user=None, days_ago=0, note=""):
|
||
it = items[k]
|
||
at = now - timedelta(days=days_ago, hours=rnd.randint(1, 6))
|
||
for w, u, sign in ((src_wh, src_user, -1), (dst_wh, dst_user, 1)):
|
||
if w is None and u is None:
|
||
continue
|
||
st = db.query(Stock).filter(Stock.item_id == it.id, Stock.warehouse_id == (wh[w].id if w else None),
|
||
Stock.user_id == (users[u].id if u else None)).first()
|
||
if not st:
|
||
st = Stock(item_id=it.id, warehouse_id=wh[w].id if w else None, user_id=users[u].id if u else None, qty=0)
|
||
db.add(st)
|
||
st.qty += sign * qty
|
||
st.since = at
|
||
kind = "writeoff" if dst_wh is None and dst_user is None else "issue" if src_wh and dst_user else "return" if src_user and dst_wh else "transfer"
|
||
db.add(InventoryMove(item_id=it.id, qty=qty, kind=kind, from_warehouse_id=wh[src_wh].id if src_wh else None,
|
||
from_user_id=users[src_user].id if src_user else None, to_warehouse_id=wh[dst_wh].id if dst_wh else None,
|
||
to_user_id=users[dst_user].id if dst_user else None, by_user_id=users["lebedev"].id, at=at, note=note))
|
||
db.flush()
|
||
|
||
move("drill", 1, src_wh="tool", dst_user="ivanov", days_ago=30)
|
||
move("keys", 1, src_wh="tool", dst_user="ivanov", days_ago=30)
|
||
move("keys", 1, src_wh="tool", dst_user="kozlov", days_ago=29)
|
||
move("torque", 1, src_wh="tool", dst_user="kozlov", days_ago=12, note="На время ТО пресса")
|
||
move("mm", 1, src_wh="tool", dst_user="petrov", days_ago=35)
|
||
move("clamp", 1, src_wh="tool", dst_user="petrov", days_ago=35)
|
||
move("vibro", 1, src_wh="tool", dst_user="sidorov", days_ago=20)
|
||
move("gloves", 10, src_wh="cons", dst_user="sidorov", days_ago=7)
|
||
move("glasses", 1, src_wh="cons", dst_user="ivanov", days_ago=7)
|
||
move("grinder", 1, src_wh="tool", dst_user="kozlov", days_ago=5)
|
||
move("grinder", 1, src_user="kozlov", dst_wh="tool", days_ago=3, note="Возврат после ремонта рамы")
|
||
move("litol", 6, src_wh="cons", dst_wh="shop", days_ago=10, note="Пополнение запаса у участка")
|
||
move("bolts", 25, src_wh="cons", days_ago=8, note="Израсходовано при ремонте кран-балки")
|
||
|
||
# --- заявки на инструмент ---
|
||
reqs = [("ivanov", "Съёмник подшипников гидравлический 10 т", "Механический съёмник не справляется с подшипниками насосов Н-1/Н-2",
|
||
"https://www.vseinstrumenti.ru/", 1, "new", "", None, 1),
|
||
("sidorov", "Тепловизор UNI-T UTi260B", "Контроль нагрева подшипников и контактов без остановки оборудования",
|
||
"https://www.chipdip.ru/", 1, "approved", "Согласовано, ждём счёт", "volkov", 4),
|
||
("petrov", "Набор отвёрток диэлектрических 1000 В", "Старый набор изношен, нужен для работ в щитах", "", 2, "ordered",
|
||
"Заказано у поставщика, срок 5 дней", "lebedev", 6),
|
||
("kozlov", "Перчатки антивибрационные", "Работа с УШМ и перфоратором", "", 4, "done", "Выданы со склада", "lebedev", 12),
|
||
("ivanov", "Шуруповёрт с ударным режимом", "Уже есть аккумуляторная дрель — по заявке отказ", "", 1, "rejected",
|
||
"Есть свободная дрель Makita в инструментальной", "lebedev", 15)]
|
||
for who, what, why, link, qty, st, ans, by, ago in reqs:
|
||
t = now - timedelta(days=ago, hours=2)
|
||
db.add(ToolRequest(author_id=users[who].id, what=what, why=why, link=link, qty=qty, status=st, answer=ans,
|
||
handled_by_id=users[by].id if by else None, created_at=t, updated_at=t + timedelta(hours=3)))
|
||
|
||
# --- инциденты ---
|
||
def units_of(k):
|
||
return db.query(ServiceUnit).filter(ServiceUnit.equipment_id == eq[k].id).order_by(ServiceUnit.id).all()
|
||
|
||
inc_open = Incident(equipment_id=eq["pump2"].id, author_id=users["sidorov"].id, occurred_at=now - timedelta(hours=3),
|
||
created_at=now - timedelta(hours=3), title="Течь по торцевому уплотнению",
|
||
description="При обходе обнаружена течь воды по валу, около 1 л/ч. Насос в работе, нагрузку снизили.")
|
||
for su in units_of("pump2")[:1]:
|
||
inc_open.units.append(IncidentUnit(unit_id=su.id, unit_name=su.name, replaced=False))
|
||
inc_open2 = Incident(equipment_id=eq["crane"].id, author_id=users["orlov"].id, occurred_at=now - timedelta(days=1, hours=2),
|
||
created_at=now - timedelta(days=1, hours=2), title="Не срабатывает концевой выключатель подъёма",
|
||
description="Крюк доходит до упора. Кран выведен из работы до ремонта, вывешен плакат.")
|
||
for su in units_of("crane")[:1]:
|
||
inc_open2.units.append(IncidentUnit(unit_id=su.id, unit_name=su.name, replaced=False))
|
||
comp_units = units_of("comp")
|
||
inc_closed = Incident(equipment_id=eq["comp"].id, author_id=users["sidorov"].id, occurred_at=now - timedelta(days=9),
|
||
created_at=now - timedelta(days=9), title="Аварийная остановка по температуре масла",
|
||
description="Компрессор остановился защитой, t масла 105 °C. Засорён масляный фильтр.",
|
||
replaced=True, replacement_note="Масляный фильтр и масло заменены", status="closed",
|
||
closed_at=now - timedelta(days=9) + timedelta(hours=5), closed_by_id=users["volkov"].id,
|
||
close_note="Запущен, температура в норме")
|
||
db.add_all([inc_open, inc_open2, inc_closed])
|
||
# сообщения о неисправности: замечено, но оборудование работает
|
||
for k, who, hours_ago, title, desc, closed in (
|
||
("lathe", "kozlov", 5, "Посторонний шум в шпиндельной бабке", "На оборотах выше 1000 слышен гул, станок работает. Нужна диагностика подшипников.", None),
|
||
("fan", "ivanov", 26, "Повышенная вибрация корпуса", "Вибрация заметна рукой, крепёж затянут. Работает, наблюдаем до планового ТО.", None),
|
||
("crane2", "orlov", 30, "Скрип колёс тележки при движении", "Скрип на правой стороне моста, кран в работе. Проверить смазку и реборды.", None),
|
||
("press", "volkov", 24 * 6, "Подтекает фитинг гидролинии", "Капельная течь на фитинге высокого давления.", "Фитинг подтянут, течи нет")):
|
||
if k not in eq:
|
||
continue
|
||
at = now - timedelta(hours=hours_ago)
|
||
db.add(Incident(kind="fault", equipment_id=eq[k].id, author_id=users[who].id, occurred_at=at, created_at=at, title=title, description=desc,
|
||
**({"status": "closed", "closed_at": at + timedelta(hours=20), "closed_by_id": users["volkov"].id, "close_note": closed} if closed else {})))
|
||
db.flush()
|
||
if comp_units:
|
||
su = comp_units[0]
|
||
inc_closed.units.append(IncidentUnit(unit_id=su.id, unit_name=su.name, replaced=True))
|
||
at = inc_closed.occurred_at + timedelta(hours=4)
|
||
if at > (su.last_service_at or at):
|
||
service.reset_unit(db, su, at=at, hours=max(su.last_service_hours, eq["comp"].meter_hours - 300), kind="incident",
|
||
user_id=users["sidorov"].id, note=f"Замена при отказе №{inc_closed.id}: {inc_closed.title}")
|
||
db.add(Incident(equipment_id=eq["boiler"].id, author_id=users["ivanov"].id, occurred_at=now - timedelta(days=16),
|
||
created_at=now - timedelta(days=16), title="Срыв пламени горелки", description="Повторный розжиг успешен, причина — загрязнение фотодатчика.",
|
||
status="closed", closed_at=now - timedelta(days=16, hours=-2), closed_by_id=users["morozova"].id, close_note="Датчик очищен"))
|
||
|
||
# --- суточные задания ---
|
||
def task(day_ago, title, who, by, status, *, k=None, unit_idx=None, desc="", flow=None):
|
||
day = today - timedelta(days=day_ago)
|
||
created = datetime.combine(day, time(8, 10)) + timedelta(minutes=rnd.randint(0, 40))
|
||
su = None
|
||
if k and unit_idx is not None:
|
||
us = units_of(k)
|
||
su = us[unit_idx] if len(us) > unit_idx else None
|
||
t = DailyTask(title=title, description=desc, day=day, assignee_id=users[who].id, created_by_id=users[by].id,
|
||
equipment_id=eq[k].id if k else None, unit_id=su.id if su else None, status="assigned",
|
||
created_at=created, updated_at=created)
|
||
db.add(t)
|
||
db.flush()
|
||
db.add(TaskEvent(task_id=t.id, user_id=users[by].id, at=created, from_status="", to_status="assigned"))
|
||
at, cur = created, "assigned"
|
||
for st, comment in (flow or []):
|
||
at = at + timedelta(minutes=rnd.randint(25, 140))
|
||
if at > now:
|
||
at = now - timedelta(minutes=5)
|
||
db.add(TaskEvent(task_id=t.id, user_id=users[who].id, at=at, from_status=cur, to_status=st, comment=comment))
|
||
cur = st
|
||
t.status, t.updated_at = cur, at
|
||
t.finished_at = at if cur in ("done", "cancelled") else None
|
||
return t
|
||
|
||
# сегодня
|
||
task(0, "Проверить натяжение ремней вентилятора", "kozlov", "volkov", "in_progress", k="fan", flow=[("in_progress", "")])
|
||
task(0, "Подтянуть сальник насоса Н-1", "sidorov", "volkov", "done", k="pump1", unit_idx=1,
|
||
flow=[("in_progress", ""), ("done", "Сальник подтянут, течи нет")])
|
||
task(0, "Замер сопротивления изоляции двигателя насоса Н-2", "petrov", "morozova", "assigned", k="pump2")
|
||
task(0, "Ремонт концевика кран-балки", "orlov", "morozova", "paused", k="crane",
|
||
desc="После ремонта — проверка на холостом ходу", flow=[("in_progress", ""), ("paused", "Ждём концевой выключатель со склада, заявка у кладовщика")])
|
||
task(0, "Уборка инструментальной кладовой", "lebedev", "volkov", "assigned")
|
||
task(0, "Замер вибрации насоса Н-2", "sidorov", "volkov", "assigned", k="pump2", unit_idx=0,
|
||
desc="После устранения течи: вибрация подшипников, результат — в комментарии")
|
||
task(0, "Осмотр подшипников дымососа", "ivanov", "ivanov", "assigned", k="boiler", unit_idx=1, desc="Поставил себе по итогам обхода")
|
||
# прошлые дни — для отчёта «Выполнение работ»
|
||
plan = [("Смазать направляющие станка", "kozlov", "lathe"), ("Очистить фильтр компрессора", "sidorov", "comp"),
|
||
("Проверить заземление ТП", "petrov", "trans"), ("Подтянуть крепёж пресса", "ivanov", "press"),
|
||
("Осмотр каната кран-балки", "orlov", "crane"), ("Продувка теплообменника", "sidorov", "boiler")]
|
||
for d in range(1, 15):
|
||
for title, who, k in rnd.sample(plan, rnd.randint(1, 3)):
|
||
r = rnd.random()
|
||
if r < 0.72:
|
||
flow = [("in_progress", ""), ("done", rnd.choice(["Выполнено", "Сделано, замечаний нет", "Выполнено, отклонений нет"]))]
|
||
elif r < 0.86:
|
||
flow = [("in_progress", ""), ("paused", rnd.choice(["Нет запчасти, жду склад", "Отвлекли на аварию"])),
|
||
("in_progress", ""), ("done", "Завершено на следующей смене")]
|
||
elif r < 0.94:
|
||
flow = [("cancelled", rnd.choice(["Оборудование в ремонте у подрядчика", "Перенесено на плановый останов"]))]
|
||
else:
|
||
flow = [("in_progress", ""), ("paused", "Требуется допуск, ждём наряд-допуск")]
|
||
task(d, title, who, rnd.choice(["volkov", "morozova"]), flow[-1][0], k=k, flow=flow)
|
||
db.flush()
|
||
_reliability(db, users=users, eq=eq, now=now)
|
||
_link_and_regular(db, users=users, eq=eq, now=now, rnd=rnd)
|
||
return {"warehouses": len(wh), "items": len(items)}
|
||
|
||
|
||
def _reliability(db: Session, *, users: dict, eq: dict, now: datetime):
|
||
"""История для «Анализа надёжности»: показания счётчиков за год и закрытые отказы с полной записью."""
|
||
from .models import MeterReading
|
||
rates = {"comp": 4.2, "pump1": 12.6, "pump2": 12.4, "boiler": 8.9, "lathe": 2.3, "mill": 6.5, "press": 7.0, "fan": 18.0,
|
||
"trans": 24.0, "crane": 0.8, "crane2": 3.5, "crane3": 2.5}
|
||
cur = {}
|
||
for k, rate in rates.items():
|
||
if k not in eq:
|
||
continue
|
||
e = eq[k]
|
||
if not e.meter_hours: # у этих машин счётчик заводим с нуля: «текущая» наработка — год работы
|
||
e.meter_hours, e.meter_updated_at = round(rate * 420, 1), now
|
||
db.add(MeterReading(equipment_id=e.id, hours=e.meter_hours, at=now, source="manual"))
|
||
cur[k] = e.meter_hours
|
||
for d in (365, 300, 240, 180, 120, 75, 45):
|
||
db.add(MeterReading(equipment_id=e.id, hours=max(0.0, round(cur[k] - rate * d, 1)), at=now - timedelta(days=d), source="manual"))
|
||
db.flush()
|
||
units = {k: db.query(ServiceUnit).filter(ServiceUnit.equipment_id == eq[k].id).order_by(ServiceUnit.id).all() for k in rates if k in eq}
|
||
# (машина, дней назад, простой ч, ожидание запчастей ч, причина, характер, что сделано, признаки, № узла-причины, что случилось, как устранено)
|
||
history = [
|
||
("pump1", 320, 6.0, 0, "wear", "gradual", "replace", "inspection", 0, "Течь по сальнику, остановлен", "Заменена сальниковая набивка"),
|
||
("pump1", 190, 3.5, 0, "operation", "sudden", "adjust", "none", None, "Сухой ход, сработала защита", "Восстановлен подпор, обучение смены"),
|
||
("pump1", 70, 26.0, 18.0, "wear", "gradual", "replace", "fault_report", 1, "Разрушение подшипника", "Подшипник заменён (ждали со склада)"),
|
||
("pump2", 250, 4.0, 0, "power", "sudden", "restart", "none", None, "Отключение по перегрузке", "Перезапуск, напряжение в норме"),
|
||
("pump2", 95, 12.0, 6.0, "wear", "gradual", "replace", "inspection", 0, "Повышенная вибрация, остановлен", "Заменена муфта"),
|
||
("comp", 280, 8.0, 0, "external", "gradual", "repair", "inspection", 0, "Перегрев: забит радиатор", "Радиатор промыт"),
|
||
("boiler", 200, 30.0, 20.0, "defect", "sudden", "replace", "none", 1, "Течь трубы экрана", "Вварена вставка, опрессовка"),
|
||
("fan", 150, 2.0, 0, "secondary", "sudden", "repair", "none", None, "Остановка при отказе пускателя", "Заменён пускатель"),
|
||
("fan", 40, 5.0, 0, "wear", "gradual", "replace", "fault_report", 0, "Обрыв ремня", "Комплект ремней заменён"),
|
||
("press", 110, 9.0, 4.0, "defect", "sudden", "repair", "none", None, "Не держит давление", "Перебран клапан после прошлого ремонта"),
|
||
("crane2", 160, 7.0, 0, "external", "sudden", "repair", "none", None, "Отказ концевика (влага)", "Концевик осушен, установлен кожух"),
|
||
("trans", 230, 1.5, 0, "power", "sudden", "restart", "none", None, "Срабатывание защиты по сети", "Включён после проверки"),
|
||
]
|
||
who = {"pump1": "sidorov", "pump2": "sidorov", "comp": "sidorov", "boiler": "ivanov", "fan": "ivanov", "press": "kozlov",
|
||
"crane2": "orlov", "trans": "petrov"}
|
||
for k, d, down, wait, cause, nature, action, prec, ui, title, note in history:
|
||
if k not in eq:
|
||
continue
|
||
at = now - timedelta(days=d, hours=3)
|
||
hrs = max(0.0, round(cur[k] - rates[k] * d, 1))
|
||
su = units.get(k, [])[ui] if ui is not None and ui < len(units.get(k, [])) else None
|
||
i = Incident(kind="incident", equipment_id=eq[k].id, author_id=users[who[k]].id, occurred_at=at, created_at=at, title=title,
|
||
hours_at_open=hrs, hours_at_failure=hrs, repair_started_at=at + timedelta(minutes=40),
|
||
restored_at=at + timedelta(hours=down), parts_wait_hours=wait or None, cause=cause, nature=nature, action=action,
|
||
precursor=prec, cause_unit_id=su.id if su else None, status="closed", closed_at=at + timedelta(hours=down, minutes=20),
|
||
closed_by_id=users["volkov"].id, close_note=note, replaced=action == "replace")
|
||
if su and action == "replace":
|
||
i.units.append(IncidentUnit(unit_id=su.id, unit_name=su.name, replaced=True))
|
||
db.add(i)
|
||
db.flush()
|
||
|
||
|
||
def _link_and_regular(db: Session, *, users: dict, eq: dict, now: datetime, rnd: random.Random):
|
||
"""Для отчёта «Выполнение работ»: наряды из ежедневного расписания — суточные осмотры; плюс история нарядов ТО."""
|
||
from . import logic
|
||
from .models import Checklist, Order, OrderTemplate
|
||
daily = {(t.equipment_id, t.checklist_id): t.id for t in db.query(OrderTemplate).filter(OrderTemplate.recurrence == "daily")}
|
||
for o in db.query(Order).filter(Order.template_id.is_(None)):
|
||
tid = daily.get((o.equipment_id, o.checklist_id))
|
||
if tid:
|
||
o.template_id = tid
|
||
reg = db.query(Checklist).filter(Checklist.kind == "regular").first()
|
||
if not reg:
|
||
return
|
||
duty = users["sidorov"]
|
||
for k, days, status in (("comp", 13, "completed"), ("pump1", 11, "completed"), ("boiler", 9, "completed"), ("comp", 6, "completed"),
|
||
("pump2", 4, "completed"), ("crane", 3, "cancelled"), ("lathe", 2, "completed"), ("boiler", 1, "in_progress")):
|
||
created = now - timedelta(days=days, hours=rnd.randint(1, 4))
|
||
o = logic.create_order(db, title=f"Регламентное ТО: {eq[k].name}", checklist_id=reg.id, equipment_id=eq[k].id,
|
||
location_id=eq[k].location_id, assignee_user_id=duty.id, due_at=created + timedelta(hours=48))
|
||
db.flush()
|
||
o.created_at, o.status = created, status
|
||
if status in ("completed", "in_progress"):
|
||
o.executor_id, o.started_at = duty.id, created + timedelta(minutes=40)
|
||
for n, it in enumerate(o.items):
|
||
if status == "in_progress" and n > 1:
|
||
break
|
||
it.value = str(round(eq[k].meter_hours - days * 8, 1)) if it.is_meter else "done"
|
||
it.answered_at = created + timedelta(minutes=50 + n * 5)
|
||
if status == "completed":
|
||
o.submitted_at = o.completed_at = created + timedelta(hours=2)
|
||
db.flush()
|