Загрузка данных


import json
import os
from dataclasses import dataclass, asdict
from typing import List, Optional, Dict, Any

DATA_FILE = "projects.json"
DENSITY_STEEL = 7850.0  # кг/м³


@dataclass
class Item:
    id: str
    name: str
    dims_mm: tuple[float, float, float]  # ДхШхВ в мм
    qty: int
    metal_thickness_mm: float
    board_section_mm: tuple[float, float]  # ширина, высота в мм
    board_length_m: float
    screws_per_item: int
    bolts_per_item: int
    eye_bolts_per_item: int

    def volume_m3(self) -> float:
        l, w, h = self.dims_mm
        return (l * w * h) / 1_000_000_000.0

    def surface_area_m2(self) -> float:
        l, w, h = self.dims_mm
        # сумма площадей всех граней
        return 2 * ((l * w + l * h + w * h) / 1_000_000.0)

    def metal_mass_kg(self) -> float:
        area_m2 = self.surface_area_m2()
        thickness_m = self.metal_thickness_mm / 1000.0
        return area_m2 * thickness_m * DENSITY_STEEL

    def board_volume_m3_per_item(self) -> float:
        bw, bh = self.board_section_mm
        section_m2 = (bw / 1000.0) * (bh / 1000.0)
        return section_m2 * self.board_length_m

    def total_values(self) -> Dict[str, Any]:
        vol_m3 = self.volume_m3()
        total_vol = vol_m3 * self.qty
        area_m2 = self.surface_area_m2()
        total_area = area_m2 * self.qty
        metal_kg_per = self.metal_mass_kg()
        total_metal = metal_kg_per * self.qty
        board_vol_per = self.board_volume_m3_per_item()
        total_board = board_vol_per * self.qty

        return {
            "volume_1_item_m3": round(vol_m3, 4),
            "total_volume_m3": round(total_vol, 3),
            "area_1_item_m2": round(area_m2, 2),
            "total_area_m2": round(total_area, 1),
            "metal_1_form_kg": round(metal_kg_per, 1),
            "total_metal_kg": round(total_metal, 1),
            "board_1_item_m3": round(board_vol_per, 3),
            "total_board_m3": round(total_board, 3),
            "screws_total": self.screws_per_item * self.qty,
            "bolts_total": self.bolts_per_item * self.qty,
            "eye_bolts_total": self.eye_bolts_per_item * self.qty,
        }


@dataclass
class Order:
    id: str
    number: str
    items: List[Item]


@dataclass
class Project:
    name: str
    orders: List[Order]

    @classmethod
    def load_all(cls) -> List["Project"]:
        if not os.path.exists(DATA_FILE):
            return []
        try:
            with open(DATA_FILE, "r", encoding="utf-8") as f:
                data = json.load(f)
            projects = [
                Project(
                    name=p["name"],
                    orders=[
                        Order(
                            id=o["id"],
                            number=o["number"],
                            items=[
                                Item(
                                    id=i["id"],
                                    name=i["name"],
                                    dims_mm=tuple(i["dims_mm"]),
                                    qty=i["qty"],
                                    metal_thickness_mm=i["metal_thickness_mm"],
                                    board_section_mm=tuple(i["board_section_mm"]),
                                    board_length_m=i["board_length_m"],
                                    screws_per_item=i["screws_per_item"],
                                    bolts_per_item=i["bolts_per_item"],
                                    eye_bolts_per_item=i["eye_bolts_per_item"],
                                )
                                for i in o["items"]
                            ],
                        )
                        for o in p["orders"]
                    ],
                )
                for p in data
            ]
            return projects
        except Exception:
            print("Ошибка чтения файла данных. Начнём с пустого списка.")
            return []

    @staticmethod
    def save_all(projects: List["Project"]) -> None:
        data = [
            {
                "name": p.name,
                "orders": [
                    {
                        "id": o.id,
                        "number": o.number,
                        "items": [asdict(i) for i in o.items],
                    }
                    for o in p.orders
                ],
            }
            for p in projects
        ]
        with open(DATA_FILE, "w", encoding="utf-8") as f:
            json.dump(data, f, ensure_ascii=False, indent=2)


def generate_id(prefix: str) -> str:
    import uuid
    return f"{prefix}_{uuid.uuid4().hex[:6]}"


def input_int(prompt: str, min_val: Optional[int] = None) -> int:
    while True:
        try:
            val = int(input(prompt))
            if min_val is not None and val < min_val:
                print(f"Значение должно быть не меньше {min_val}.")
                continue
            return val
        except ValueError:
            print("Пожалуйста, введите целое число.")


def input_float(prompt: str, min_val: Optional[float] = None) -> float:
    while True:
        try:
            val = float(input(prompt))
            if min_val is not None and val < min_val:
                print(f"Значение должно быть не меньше {min_val}.")
                continue
            return val
        except ValueError:
            print("Пожалуйста, введите число.")


def parse_dims() -> tuple[float, float, float]:
    while True:
        s = input("Габариты (ДхШхВ, мм, через пробел, например: 1000 500 200): ").strip()
        parts = s.replace(",", " ").split()
        if len(parts) != 3:
            print("Нужно ровно три числа.")
            continue
        try:
            dims = tuple(float(x) for x in parts)
            if any(d <= 0 for d in dims):
                print("Все размеры должны быть больше 0.")
                continue
            return dims
        except ValueError:
            print("Введите корректные числа.")


def parse_board_section() -> tuple[float, float]:
    while True:
        s = input("Сечение доски (ширина×высота, мм, например: 150×50 или 150x50): ").strip().replace("×", "x")
        parts = s.split("x")
        if len(parts) != 2:
            print("Формат: ширина×высота (например: 150×50).")
            continue
        try:
            w, h = float(parts[0]), float(parts[1])
            if w <= 0 or h <= 0:
                print("Ширина и высота должны быть больше 0.")
                continue
            return w, h
        except ValueError:
            print("Введите корректные числа.")


def add_project(projects: List[Project]) -> None:
    name = input("Название проекта: ").strip()
    if not name:
        print("Название не может быть пустым.")
        return
    projects.append(Project(name=name, orders=[]))
    Project.save_all(projects)
    print("Проект добавлен.")


def list_projects(projects: List[Project]) -> Optional[Project]:
    if not projects:
        print("Нет проектов.")
        return None
    print("\nПроекты:")
    for i, p in enumerate(projects, start=1):
        print(f"{i}. {p.name}")
    while True:
        try:
            idx = int(input("Выберите номер проекта (или 0 для отмены): "))
            if idx == 0:
                return None
            if 1 <= idx <= len(projects):
                return projects[idx - 1]
            print("Неверный номер.")
        except ValueError:
            print("Введите число.")


def add_order(project: Project) -> None:
    number = input("Номер заказа (например: ORD-101): ").strip()
    if not number:
        print("Номер заказа не может быть пустым.")
        return
    if any(o.number == number for o in project.orders):
        print("Такой заказ уже есть в проекте.")
        return
    project.orders.append(Order(id=generate_id("ord"), number=number, items=[]))
    Project.save_all([p for p in all_projects if p.name != project.name] + [project])
    print("Заказ добавлен.")


def add_item(project: Project, order: Order) -> None:
    name = input("Наименование изделия: ").strip()
    if not name:
        print("Наименование не может быть пустым.")
        return

    dims = parse_dims()
    qty = input_int("Количество изделий, шт: ", min_val=1)
    metal_thick = input_float("Толщина металла формы, мм: ", min_val=0.1)
    board_sec = parse_board_section()
    board_len = input_float("Длина доски, м: ", min_val=0.5)
    screws = input_int("Саморезы на 1 изделие, шт: ", min_val=0)
    bolts = input_int("Болты+гайки на 1 изделие (парами), шт: ", min_val=0)
    eye_bolts = input_int("Рым-болты на 1 изделие, шт: ", min_val=0)

    item = Item(
        id=generate_id("itm"),
        name=name,
        dims_mm=dims,
        qty=qty,
        metal_thickness_mm=metal_thick,
        board_section_mm=board_sec,
        board_length_m=board_len,
        screws_per_item=screws,
        bolts_per_item=bolts,
        eye_bolts_per_item=eye_bolts,
    )
    order.items.append(item)
    Project.save_all(all_projects)
    print("Изделие добавлено.")


def show_item_details(project: Project, order: Order, item: Item) -> None:
    vals = item.total_values()
    print(f"\nДетали: {item.name} ({order.number}, проект {project.name})")
    print(f"Объём 1 изделия: {vals['volume_1_item_m3']} м³")
    print(f"Суммарный объём: {vals['total_volume_m3']} м³")
    print(f"Площадь 1 изделия: {vals['area_1_item_m2']} м²")
    print(f"Суммарная площадь: {vals['total_area_m2']} м²")
    print(f"Металл на 1 форму: {vals['metal_1_form_kg']} кг")
    print(f"Суммарно металл: {vals['total_metal_kg']} кг")
    print(f"Доска на 1 изделие: {vals['board_1_item_m3']} м³")
    print(f"Суммарно доски: {vals['total_board_m3']} м³")
    print(f"Саморезы (суммарно): {vals['screws_total']} шт")
    print(f"Болты+гайки (суммарно): {vals['bolts_total']} шт")
    print(f"Рым-болты (суммарно): {vals['eye_bolts_total']} шт")


def select_order(project: Project) -> Optional[Order]:
    if not project.orders:
        print("В проекте нет заказов.")
        return None
    print("\nЗаказы:")
    for i, o in enumerate(project.orders, start=1):
        print(f"{i}. {o.number} (изделий: {len(o.items)})")
    while True:
        try:
            idx = int(input("Выберите номер заказа (или 0 для отмены): "))
            if idx == 0:
                return None
            if 1 <= idx <= len(project.orders):
                return project.orders[idx - 1]
            print("Неверный номер.")
        except ValueError:
            print("Введите число.")


def select_item(order: Order) -> Optional[Item]:
    if not order.items:
        print("В заказе нет изделий.")
        return None
    print("\nИзделия:")
    for i, it in enumerate(order.items, start=1):
        print(f"{i}. {it.name}")
    while True:
        try:
            idx = int(input("Выберите номер изделия (или 0 для отмены): "))
            if idx == 0:
                return None
            if 1 <= idx <= len(order.items):
                return order.items[idx - 1]
            print("Неверный номер.")
        except ValueError:
            print("Введите число.")


# --- Главный цикл ---
all_projects: List[Project] = Project.load_all()

while True:
    print("\nМеню:")
    print("1. Добавить проект")
    print("2. Выбрать проект")
    print("3. Показать все проекты")
    print("0. Выход")
    choice = input("Выберите действие: ").strip()

    if choice == "1":
        add_project(all_projects)
    elif choice == "2":
        project = list_projects(all_projects)
        if project is None:
            continue
        while True:
            print(f"\nПроект: {project.name}")
            print("1. Добавить заказ")
            print("2. Добавить изделие")
            print("3. Посмотреть детали изделия")
            print("4. Вернуться к выбору проекта")
            sub = input("Действие: ").strip()
            if sub == "1":
                add_order(project)
            elif sub == "2":
                order = select_order(project)
                if order:
                    add_item(project, order)
            elif sub == "3":
                order = select_order(project)
                if not order:
                    continue
                item = select_item(order)
                if item:
                    show_item_details(project, order, item)
            elif sub == "4":
                break
            else:
                print("Неверная команда.")
    elif choice == "3":
        for i, p in enumerate(all_projects, start=1):
            print(f"{i}. {p.name} (заказов: {len(p.orders)})")
    elif choice == "0":
        print("До свидания!")
        break
    else:
        print("Неверная команда.")