mirror of
https://github.com/Horizontal42/warehouse-master.git
synced 2026-11-10 16:02:07 +03:00
Compare commits
7 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8ef829311 | ||
|
|
39a6757173 | ||
|
|
06e7f1f202 | ||
|
|
36ebc4f348 | ||
|
|
e7718d6333 | ||
|
|
c08a5a10d0 | ||
|
|
4c42318226 |
6 changed files with 235 additions and 78 deletions
67
.github/workflows/build.yml
vendored
67
.github/workflows/build.yml
vendored
|
|
@ -1,67 +0,0 @@
|
|||
name: build
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag to upload assets to'
|
||||
required: true
|
||||
default: 'v1.0.0'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: windows-latest
|
||||
artifact: WarehouseMaster.exe
|
||||
- os: macos-latest
|
||||
artifact: WarehouseMaster-macos
|
||||
- os: ubuntu-latest
|
||||
artifact: WarehouseMaster-linux
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install system deps (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y libxcb-xinerama0 libgl1 libegl1
|
||||
|
||||
- name: Install Python deps
|
||||
run: pip install pyinstaller -r requirements.txt
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
QT_QPA_PLATFORM: offscreen
|
||||
run: pyinstaller WarehouseMaster.spec
|
||||
|
||||
- name: Rename artifact (macOS/Linux)
|
||||
if: runner.os != 'Windows'
|
||||
shell: bash
|
||||
run: mv dist/WarehouseMaster "dist/${{ matrix.artifact }}"
|
||||
|
||||
- name: Upload to release
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
TAG="${{ github.ref_name }}"
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
TAG="${{ inputs.tag }}"
|
||||
fi
|
||||
gh release upload "$TAG" "dist/${{ matrix.artifact }}" --clobber
|
||||
10
.gitignore
vendored
10
.gitignore
vendored
|
|
@ -9,16 +9,8 @@ sheet_config.json
|
|||
token.json
|
||||
credentials.json
|
||||
|
||||
# internal/AI tool files
|
||||
# local dev scratch
|
||||
ТЕст/
|
||||
.claude/
|
||||
CLAUDE.md
|
||||
*.md.bak
|
||||
elegant-sauteeing-axolotl.md
|
||||
.aider*
|
||||
bugs.md
|
||||
*_ai_notes*
|
||||
llm_context*
|
||||
|
||||
# PyInstaller leftovers
|
||||
*.spec.bak
|
||||
|
|
|
|||
115
ARCHITECTURE.md
Normal file
115
ARCHITECTURE.md
Normal file
|
|
@ -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_<thing>()` 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)
|
||||
117
ARCHITECTURE.ru.md
Normal file
117
ARCHITECTURE.ru.md
Normal file
|
|
@ -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_<thing>()` метод
|
||||
|
||||
### Новый язык
|
||||
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)
|
||||
2
LICENSE
2
LICENSE
|
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2024
|
||||
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
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ a = Analysis(
|
|||
['main.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[('Inter.ttf', '.'), ('sheet_config.json', '.')],
|
||||
datas=[('Inter.ttf', '.')],
|
||||
hiddenimports=[],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue