warehouse-master/ARCHITECTURE.md
2026-06-13 19:23:26 +03:00

115 lines
5.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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)