From 36ebc4f3487e8208b64ae9168e8d82506a17b73d Mon Sep 17 00:00:00 2001 From: Andrew Pimenov Date: Sat, 13 Jun 2026 19:23:26 +0300 Subject: [PATCH] update license author and add architecture docs --- ARCHITECTURE.md | 115 ++++++++++++++++++++++++++++++++++++++++++++ ARCHITECTURE.ru.md | 117 +++++++++++++++++++++++++++++++++++++++++++++ LICENSE | 2 +- 3 files changed, 233 insertions(+), 1 deletion(-) create mode 100644 ARCHITECTURE.md create mode 100644 ARCHITECTURE.ru.md diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..13f959d --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,115 @@ +# Architecture + +## Overview + +Warehouse Master is a desktop application for printing numbered PDF labels and tracking laptop reassembly via Google Sheets. Two independent tabs: Labels (PDF generation) and Reassembly (Google Sheets sync). + +## File structure + +| File | Purpose | +|------|---------| +| **main.py** | Entry point, `LabelApp` main window, tab lifecycle, i18n dispatch | +| **printing.py** | Utilities: file I/O, resource paths, printer discovery, app config folder | +| **styles.py** | Stylesheet builder: `build_stylesheet(dark: bool)` → returns CSS for both themes | +| **workers.py** | Background threads: `PDFWorker` (reportlab), `ReassemblyWorker` (gspread sync) | +| **widgets.py** | Reusable UI: `NoScrollComboBox`, `RangeRowWidget`, `PreferencesDialog` | +| **strings.py** | i18n: `STRINGS` dict `{"en": {...}, "ru": {...}}` with all UI strings | +| **sheet_manager.py** | Google Sheets API wrapper: config-driven column indices, category calculation, batch updates | +| **sheet_config.example.json** | Template config: worksheets, column mappings, translators, statuses | +| **WarehouseMaster.spec** | PyInstaller spec: bundles `Inter.ttf`, builds single-file exe | +| **requirements.txt** | Dependencies: PyQt5, reportlab, gspread, google-auth | + +## Module responsibilities + +### main.py +- Loads/saves QSettings (theme, language, window state) +- Renders two tabs: labels and reassembly +- Wires up event handlers +- Calls `_retranslate()` on locale change +- Temp file cleanup on close + +### printing.py +- `resource_path()` — finds bundled files (Inter.ttf, icon.ico) +- `app_data_dir()` — `%APPDATA%/WarehouseMaster/` for token.json, local configs +- `config_path()` — returns sheet_config.json (local or appdata) +- `register_fonts()` — loads Inter.ttf into reportlab +- `get_installed_printers()` — enumerates Windows printers (win32api) +- `open_file()` — cross-platform file open (Windows/macOS/Linux) +- `print_file()` — print to specific printer or system default + +### styles.py +- `build_stylesheet(dark: bool)` — single function, returns full QPalette CSS +- Colors hardcoded (dark: #212529 bg, #f8f9fa text; light: inverse) +- Called on theme toggle → `setStyleSheet()` + +### workers.py +- `PDFWorker(QThread)` — generates PDF in background, emits `finished(filepath)` or `error_occurred(str)` +- `ReassemblyWorker(QThread)` — calls `SheetManager.process_reassembly()`, emits `finished_signal(comment, tech_op)` or `error_signal(str)` +- Both prevent UI freeze + +### widgets.py +- `NoScrollComboBox` — suppresses scroll-wheel zoom +- `RangeRowWidget` — start/end inputs + +/- buttons; callback-driven +- `PreferencesDialog` — language + theme + sheet URL + save folder, saves to QSettings + +### strings.py +- Two dicts: `STRINGS["en"]`, `STRINGS["ru"]` +- Keys: `"window_title"`, `"tab_labels"`, message templates with `{n}` placeholders +- No locale switching logic — main.py reads `settings.value("locale")` and indexes + +### sheet_manager.py +- `SheetManager(sheet_url, config_path)` — loads config.json, authorizes gspread +- `process_reassembly(laptop_id, parts_str, ssd_id, akb_id)` — atomically updates: marks parts shipped, writes grades to laptop row, recalculates category (D/C/B/A/NEW) +- `_calculate_category(laptop_data)` — rule-based scoring (D if any part ≤rank 1, NEW if all ≥A and cycles < 150, etc.) +- Config-driven: all column indices, range clears, status values from sheet_config.json + +## Hot paths + +### PDF generation +1. User clicks "Generate PDF" +2. `_start_generation()` validates ranges (start ≤ end), spawns `PDFWorker` +3. `PDFWorker.run()` → `reportlab.canvas` draws labels in grid layout +4. On success: `_on_pdf_done()` → open file or print +5. UI unblocked (worker thread) + +### Reassembly sync +1. User fills order/parts, clicks "Apply changes" +2. `_run_reassembly()` spawns `ReassemblyWorker` +3. `ReassemblyWorker.run()` → `SheetManager.process_reassembly()` +4. Manager: fetches IDs from all worksheets, processes each part (SSD/battery/part), calculates new category +5. Batch updates all sheets, returns comment + tech lines +6. `_on_reassembly_done()` displays results +7. UI unblocked + +## Adding things + +### New label format (e.g., 6×6 grid) +1. Add to `PAGE_SIZES` and `DEFAULT_FONT_SIZES` in workers.py +2. Add radio button in `_init_gen_tab()` +3. Add branch in `PDFWorker.run()` for grid layout + +### New reassembly field +1. Add config key to sheet_config.example.json (e.g., `"warranty_months": 52`) +2. Read in `SheetManager.__init__()` via self.cfg +3. Add processing in `_process_part()` or new `_process_()` method + +### New language +1. Add `STRINGS["new_lang"]` dict in strings.py +2. Add to language combo in PreferencesDialog (widgets.py) +3. Map combo index → locale code (e.g., index 2 → "de") + +### New Google Sheets worksheet +1. Add to `sheet_config.json` → `worksheets` +2. Add column mappings if needed +3. Add fetch/batch in `SheetManager` methods +4. Wire up in `process_reassembly()` + +## Gotchas + +- **QSettings casing:** Linux filenames are case-sensitive; use lowercase keys (`"sheet_url"` not `"SheetURL"`) +- **Token refresh:** If gspread credentials expire mid-session, worker will fail; user must restart and re-auth +- **Column alignment:** reportlab canvas Y-axis is bottom-up (0 = bottom). Grid layout offsets Y correctly with `ph - (row + 1) * cell_h` +- **Config missing:** If sheet_config.json missing, SheetManager raises on init; handle in UI with try-except, show user-friendly message +- **Cyrillic grades:** Before rank check, normalize Cyrillic А/В/С/Д → Latin A/B/C/D via `str.maketrans()` +- **Printer not found:** If saved printer deleted, `findText()` returns -1; guard before `setCurrentIndex()` +- **Temp files:** If app crashes, temp PDFs not deleted; consider OS-level temp cleanup (tmp dirs auto-purge) diff --git a/ARCHITECTURE.ru.md b/ARCHITECTURE.ru.md new file mode 100644 index 0000000..3888c32 --- /dev/null +++ b/ARCHITECTURE.ru.md @@ -0,0 +1,117 @@ +[English](ARCHITECTURE.md) + +# Архитектура + +## Обзор + +Warehouse Master — десктопное приложение для печати нумерованных этикеток в PDF и синхронизации пересборки ноутбуков через Google Sheets. Два независимых таба: Labels (генерация PDF) и Reassembly (синхронизация с таблицей). + +## Структура файлов + +| Файл | Назначение | +|------|-----------| +| **main.py** | Entry point, главное окно `LabelApp`, жизненный цикл табов, dispatch i18n | +| **printing.py** | Утилиты: файловый ввод-вывод, пути к ресурсам, обнаружение принтеров, папка конфига | +| **styles.py** | Генератор стилей: `build_stylesheet(dark: bool)` → возвращает CSS для обеих тем | +| **workers.py** | Фоновые потоки: `PDFWorker` (reportlab), `ReassemblyWorker` (синхронизация gspread) | +| **widgets.py** | Переиспользуемые UI компоненты: `NoScrollComboBox`, `RangeRowWidget`, `PreferencesDialog` | +| **strings.py** | i18n: словарь `STRINGS` `{"en": {...}, "ru": {...}}` со всеми строками UI | +| **sheet_manager.py** | Обёртка Google Sheets API: конфиг-управляемые индексы колонок, вычисление категории, батч-обновления | +| **sheet_config.example.json** | Шаблон конфига: листы, маппинги колонок, переводчики типов, статусы | +| **WarehouseMaster.spec** | Спек PyInstaller: бандлит Inter.ttf, собирает single-file exe | +| **requirements.txt** | Зависимости: PyQt5, reportlab, gspread, google-auth | + +## Ответственность модулей + +### main.py +- Загружает/сохраняет QSettings (тема, язык, состояние окна) +- Рендерит два таба: labels и reassembly +- Подключает обработчики событий +- Вызывает `_retranslate()` при смене языка +- Очистка временных файлов при закрытии + +### printing.py +- `resource_path()` — ищет бандлированные файлы (Inter.ttf, icon.ico) +- `app_data_dir()` — `%APPDATA%/WarehouseMaster/` для token.json, локальных конфигов +- `config_path()` — возвращает sheet_config.json (локальный или из appdata) +- `register_fonts()` — загружает Inter.ttf в reportlab +- `get_installed_printers()` — перечисляет принтеры Windows (win32api) +- `open_file()` — кросс-платформенный открыватель файлов (Windows/macOS/Linux) +- `print_file()` — печать на конкретный принтер или системный по умолчанию + +### styles.py +- `build_stylesheet(dark: bool)` — одна функция, возвращает полный CSS палитры +- Цвета захардкожены (dark: #212529 фон, #f8f9fa текст; light: инверсия) +- Вызывается при переключении темы → `setStyleSheet()` + +### workers.py +- `PDFWorker(QThread)` — генерирует PDF в фоне, эмитит `finished(filepath)` или `error_occurred(str)` +- `ReassemblyWorker(QThread)` — вызывает `SheetManager.process_reassembly()`, эмитит `finished_signal(comment, tech_op)` или `error_signal(str)` +- Оба предотвращают блокировку UI + +### widgets.py +- `NoScrollComboBox` — подавляет скроллинг на колесике мыши +- `RangeRowWidget` — поля start/end + кнопки +/-; управляется callback-ами +- `PreferencesDialog` — язык + тема + URL таблицы + папка сохранения, сохраняет в QSettings + +### strings.py +- Два словаря: `STRINGS["en"]`, `STRINGS["ru"]` +- Ключи: `"window_title"`, `"tab_labels"`, шаблоны сообщений с `{n}` плейсхолдерами +- Никакой логики переключения локали — main.py читает `settings.value("locale")` и индексирует + +### sheet_manager.py +- `SheetManager(sheet_url, config_path)` — загружает config.json, авторизует gspread +- `process_reassembly(laptop_id, parts_str, ssd_id, akb_id)` — атомарно обновляет: отмечает запчасти отгруженными, пишет грейды в строку ноутбука, пересчитывает категорию (D/C/B/A/NEW) +- `_calculate_category(laptop_data)` — правило-based скоринг (D если любая деталь ≤rank 1, NEW если все ≥A и cycles < 150 и т.д.) +- Управляется конфигом: все индексы колонок, range clears, значения статусов из sheet_config.json + +## Горячие пути + +### Генерация PDF +1. Пользователь нажимает «Generate PDF» +2. `_start_generation()` валидирует диапазоны (start ≤ end), спавнит `PDFWorker` +3. `PDFWorker.run()` → `reportlab.canvas` рисует этикетки в сеточном макете +4. При успехе: `_on_pdf_done()` → открыть файл или печать +5. UI разблокирован (рабочий поток) + +### Синхронизация пересборки +1. Пользователь заполняет заказ/запчасти, нажимает «Apply changes» +2. `_run_reassembly()` спавнит `ReassemblyWorker` +3. `ReassemblyWorker.run()` → `SheetManager.process_reassembly()` +4. Manager: фетчит ID из всех листов, обрабатывает каждую запчасть (SSD/АКБ/запчасть), вычисляет новую категорию +5. Батч-обновляет все листы, возвращает comment + tech строки +6. `_on_reassembly_done()` отображает результаты +7. UI разблокирован + +## Добавление функциональности + +### Новый формат этикетки (например 6×6 сетка) +1. Добавить в `PAGE_SIZES` и `DEFAULT_FONT_SIZES` в workers.py +2. Добавить radio button в `_init_gen_tab()` +3. Добавить ветку в `PDFWorker.run()` для макета сетки + +### Новое поле пересборки +1. Добавить ключ конфига в sheet_config.example.json (например `"warranty_months": 52`) +2. Прочитать в `SheetManager.__init__()` через self.cfg +3. Добавить обработку в `_process_part()` или новый `_process_()` метод + +### Новый язык +1. Добавить `STRINGS["new_lang"]` словарь в strings.py +2. Добавить в языковую комбобокс в PreferencesDialog (widgets.py) +3. Маппировать индекс комбо → код локали (например index 2 → "de") + +### Новый лист Google Sheets +1. Добавить в `sheet_config.json` → `worksheets` +2. Добавить маппинги колонок если нужно +3. Добавить fetch/batch в методы `SheetManager` +4. Подключить в `process_reassembly()` + +## Подводные камни + +- **QSettings кейс:** На Linux имена файлов case-sensitive; использовать lowercase ключи (`"sheet_url"` не `"SheetURL"`) +- **Обновление токена:** Если учетные данные gspread истекают во время сессии, рабочий поток упадёт; пользователь должен перезапустить и переавторизоваться +- **Выравнивание колонок:** В reportlab canvas Y-ось направлена вверх (0 = внизу). Сеточный макет правильно смещает Y через `ph - (row + 1) * cell_h` +- **Конфиг отсутствует:** Если sheet_config.json не найден, SheetManager выбросит на инит; обработать в UI с try-except, показать понятное сообщение +- **Кириллические грейды:** Перед проверкой ранга нормализовать кириллицу А/В/С/Д → латинь A/B/C/D через `str.maketrans()` +- **Принтер не найден:** Если сохранённый принтер удалён, `findText()` вернёт -1; guard перед `setCurrentIndex()` +- **Временные файлы:** Если приложение упадёт, временные PDF не удалятся; рассмотреть OS-уровневую очистку tmp директорий (автоматическая purge) diff --git a/LICENSE b/LICENSE index 99dc809..a429eb6 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 Andrew Pimenov +Copyright (c) 2026 Andrew Pimenov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal