Version: 0.3.63

# CitiFox ERP App

CitiFox ERP App is an internal Python desktop application for garment ERP workflows.

The app currently focuses on:

- importing and reading Purchase Order PDF files
- managing PO, Costing, Tech Pack, BOM, Samples, Production, HR, and History data
- syncing shared data through a host API
- keeping fast local runtime cache on Windows
- packaging and publishing desktop builds to the server

The desktop app is built with Python and Tkinter. The host/local API uses the shared database layer and supports both SQLite and PostgreSQL depending on runtime configuration.

## Current Development Model

The project is designed to work across multiple machines with three separate layers:

- `source code`: synchronized through Git
- `shared data`: synchronized through the server or host API
- `local state`: kept only on each local machine

### Files That Belong In Git

- Python source code
- `README.md`
- `requirements.txt`
- build, run, bootstrap, and publish scripts
- safe example files in `settings/*.example.json`
- shared configuration templates such as `settings/report_template.json`

### Files That Must Not Be Committed

- remembered login data
- Tech Pack download password
- local `device_id`
- local `ui_state`
- runtime cache, debug data, build output, and generated packages

The `.gitignore` excludes:

- `settings/auth_settings.json`
- `settings/download_tech_pack_settings.json`
- `settings/server_settings.json`
- `settings/ui_state.json`
- `settings/permissions_settings.json`
- runtime folders such as `data/`, `debug/`, `build/`, and `dist/`

### Safe Settings Templates

The `settings/` folder contains safe example files:

- `auth_settings.example.json`
- `download_tech_pack_settings.example.json`
- `server_settings.example.json`
- `ui_state.example.json`

These files do not contain real credentials and are used to initialize local runtime settings on a new machine.

## New Development Machine Setup

On a new Windows development machine:

1. Clone the repository.
2. Run:

```powershell
scripts\bootstrap_dev_machine.bat
```

The script will:

- create local `settings/*.json` files from example files if they do not exist
- ensure the virtual environment exists
- install `requirements.txt`
- create required local runtime folders under `D:\Hermes\local`

Then run:

```powershell
scripts\run_app.bat
```

## Git Helper Scripts

The `scripts/` folder contains quick Git scripts for daily use:

- `github_download_latest.bat`
  - downloads the latest source code from GitHub
  - stops if the working tree has uncommitted changes
- `github_upload_changes.bat`
  - stages all changes
  - asks for a commit message
  - commits and pushes the current branch

PowerShell versions are also available:

- `github_download_latest.ps1`
- `github_upload_changes.ps1`

## Reset Local Runtime State

To reset one local machine for testing:

```powershell
scripts\reset_local_runtime_state.bat
```

This removes:

- `settings/auth_settings.json`
- `settings/download_tech_pack_settings.json`
- `settings/ui_state.json`
- local sample cover page asset cache under `D:\Hermes\local\data\sample_cover_page_assets`

After reset, run:

```powershell
scripts\bootstrap_dev_machine.bat
```

## Project Rules

### Source Of Truth

- Online mode is the preferred source of truth when multiple machines are used.
- Shared assets such as cover page templates, buyer logos, and Tech Pack source files should live on the host when possible.
- Do not copy runtime files from one machine to another.
- If a setting must be shared, use an example file or server-side configuration.
- Shared templates and shared file assets must be stored under the server runtime `/shared` root.
- Use clear root-level asset buckets under `/home/deploy/sunny_erp_local/shared`, matching existing names such as `packing-list-assets`, `costing-form-assets`, `sample-cover-assets`, and `carton-sticker-assets`.
- Do not use a local desktop file path as the normal shared template source; local files are fallback only.
- Cross-module derived fields must not depend on UI tab load order. If a field is displayed in one module but sourced from another module, the consumer module must be able to resolve it from in-memory data, session cache, local cache, or source/server data during startup.
- Refresh-only fixes are not acceptable for cross-module fields. Values that appear after `F5` must also appear after closing and reopening the app.
- In online/server mode, local UI cache is never the source of truth for mutable relationship data. Relationship fields such as Tech Pack Baby Style links must load from the server on app open, not only after a manual refresh.
- Production fields derived from Tech Pack, especially `Parent` and `Season`, must always use the same fallback chain on startup and refresh.
- Cross-module lookup data must be built once per refresh and reused while building list rows. Do not call source/server/cache fallback inside a per-row loop for fields such as Production `Parent` or `Season`; doing so can block the background worker and leave the tab blank.

### Translation Contract

All new tabs, windows, dialogs, toolbars, filter rows, table headings, status text, warning messages, and action buttons must use language keys instead of hard-coded display text.

Rules:

- Add every new UI text key to `language/en.json`, `language/vi.json`, `language/ko.json`, and `language/zh.json`.
- The desktop app must use the `language/` folder inside the app project as the only language source. Do not add or depend on `D:\Hermes\local\language`.
- Use `self.t("key")` when creating widgets and update the same widgets in `app_modules/translation_module.py` when they need to refresh after changing language.
- Register toolbar buttons, filter labels, section labels, and other persistent widgets in `self.widget_text` when they need live translation updates.
- Treeview headings must also use translation keys. Do not leave column titles such as `Export`, `Image`, `Status`, `Item Code`, `Warehouse`, or `Production Orders` as raw text.
- If a table uses internal field names for filtering or sorting, keep the internal keys stable and translate only the visible labels.
- Before finishing language-related work, verify that JSON parses successfully, no `[TODO]` values remain, and Korean/Chinese files do not contain mojibake text.

### Desktop Update Contract

The desktop app checks the configured update manifest URL and compares the running `APP_VERSION` with the server version.

Rules:

- The manifest endpoint must return `version` and `download_url`; relative download URLs are resolved against the manifest URL.
- When the server version is newer, the header `Update App` button appears next to the notification button and shows `!`.
- Startup auto-check for the update manifest runs inside the `Preparing workspace` overlay after module preload and before the app becomes usable, so the user does not see a delayed update check after entering the app. Keep this phase visible briefly even when the manifest responds quickly, and do not schedule heavy background image sync immediately after the overlay closes.
- `Update App` starts `scripts\update_app.ps1`, then closes the current app. The script downloads the release zip, extracts it, copies files into the running app folder, and relaunches the `.exe`.
- Keep `scripts\update_app.ps1` included in packaged builds and release zips.
- The updater must not overwrite existing user `settings\*.json` files during copy.
- Automatic update is supported only for packaged Windows desktop builds, not source-mode runs.

### Layout Contract For List Tabs

The standard layout template is documented in:

- `docs/ui_module_layout_template.md`

Current list tabs that should follow this contract:

- `Tech Pack`
- `Samples`
- `BOM`
- `Production`

The list tab layout is:

- row 0: toolbar
- row 1: filter/search row
- row 2: section label
- row 3: main table
- row 4: module status bar

Rules:

- Do not insert extra spacer frames between the table and the status bar.
- Do not add custom `pady` to the table frame if the other standard tabs do not use it.
- If a tab has no image column, the `Export` checkbox column is first.
- If a tab has an image column, the image column stays first and `Export` is second.
- The `Export` header must support select all / deselect all for visible rows.
- `Export report` should use checked rows first.
- `Delete` should use checked rows first.
- If a table displays derived or expanded rows, `Delete` must resolve the visible row identity, not only the base entity identity. Example: Costing rows expanded by `style_code + option` must delete the matching option record, not the base style group.
- Do not report delete success from the button handler alone. After a delete write, reload or update the authoritative source/cache and verify the deleted visible identities are gone from the refreshed table. If any selected identity remains visible, show a warning/error instead of a success status.

### Detail Window Contract

Detail windows should:

- open as `Toplevel`
- allow multiple windows if the workflow needs comparison
- use wide fields or `Text` widgets for long content
- show real image previews instead of raw image paths where possible
- guard every callback against destroyed widgets
- avoid `grab_set()` for normal data-entry popups. A popup such as `Add Baby Style` must stay modeless so switching to another window cannot lock the main app or the detail window. Use modal grabs only for truly blocking startup/auth/critical confirmations, and always release the grab on save, cancel, close, or error.
- avoid `wait_window()` together with `grab_set()` for normal edit/detail forms. This has caused repeated stuck-window cases after switching to another application and returning to ERP. Normal edit forms such as Costing Form must stay modeless, resizable, and able to regain focus after Alt-Tab.
- if a popup should behave like a normal work window, do not call `transient()` because it can remove normal window controls such as maximize on Windows.
- run server writes and heavy saves in a worker thread, then update Tkinter widgets only through the main thread. A background save must not hold a modal grab while waiting for network/server I/O.

Every callback related to refresh, translation, UI state persistence, `after`, `trace_add`, or `WM_DELETE_WINDOW` must handle these states:

- popup not opened yet
- popup currently open
- popup just closed

### Large Tab Loading

Large list tabs should load source data in a worker thread so the main UI does not freeze.

Current tabs following the async loading pattern:

- `Tech Pack`
- `Costing`
- `BOM`
- `Samples`

Worker threads may:

- call host APIs
- read local database data
- normalize payloads
- prepare pure Python snapshots

Worker threads must not call:

- Tkinter widgets
- `winfo_exists()` or any `winfo_*` method
- `messagebox`
- `tree.insert` or `tree.delete`
- `apply_*_filter`
- `refresh_*_tree`
- `set_status`

UI updates must return to the main thread through:

```python
schedule_root_after(0, ...)
```

Each async module must use:

- `module_data_loading`
- `module_load_tokens`
- `module_load_callbacks`

When moving a module to async loading:

1. Move source I/O first.
2. Move CPU build/enrich only after checking for side effects.
3. Review destructive business side effects separately before moving them into a worker.

### Main UI Local Cache

Main UI cache is stored in SQLite:

```text
<APP_DIR>\data\main_ui_local_cache\main_ui_cache.sqlite3
```

Cacheable modules:

- `Purchase Order`
- `Costing`
- `Tech Pack`
- `BOM`
- `Production`
- `Samples`
- `HR`
- `History`

Rules:

- Main UI must not use the old JSON cache folder `<APP_DIR>\data\main_ui_session_cache`.
- If SQLite cache misses or the boot session changed, load from source/server and write a new cache snapshot.
- Cache is tied to the Windows boot session ID.
- Restarting the app in the same Windows boot session can reuse cache.
- Restarting Windows changes the boot session and forces fresh source loading.
- Cache has a `source_signature` based on server mode/base URL.
- Do not include `device_id` in the source signature.
- Online/server mode modules that contain frequently edited relationships must not trust session cache as the first-load source. `Tech Pack` must fetch from the server on app open so Baby Style links, parent relationships, and other detail patches survive close/reopen without requiring `F5`.
- Session cache may be used as a speed hint only when it cannot overwrite newer server data. After any source/server refresh, write the refreshed snapshot back to SQLite cache.
- `Refresh(F5)` must force reload only the currently open module.
- Do not turn `F5` into a global refresh.

Important cached snapshots include:

- `all_orders`
- `costing_items`
- `tech_pack_rows`
- `tech_pack_library`
- `bom_items_enriched`
- `production_rows`
- `sample_rows`
- `history_rows`
- `hr_employee_rows`
- `hr_attendance_rows`

When the app closes, `on_app_close()` must flush the current in-memory snapshot to local cache before destroying the root window.

The app also auto-saves session cache every 5 minutes:

- skip if app is closing
- skip if a module is currently loading
- capture the snapshot on the main thread
- write SQLite data in a worker thread

### Startup Loading Overlay

After the main window appears, the app may still need a short time to hydrate local cache and preload allowed modules.

The main UI may show a loading overlay during startup preload so users know data is still being prepared, but the overlay must never block input indefinitely.

Implementation notes:

- `main_ui_loading_overlay` is a transient `Toplevel`.
- `main_ui_loading_show_after_id` tracks the delayed overlay show callback.
- The overlay title is `Preparing workspace`.
- Do not call `grab_set()` from the startup/loading overlay. Loading UI is informational only; it must not capture all clicks.
- Do not hide the overlay just because the current tab finished loading when the normal preload path is healthy; background preload should finish through the allowed tab list.
- If a module is still loading after the stale preload timeout, release/hide the overlay, set a status such as `Workspace is ready. Some data is still loading in the background.`, and let the remaining work continue in background.
- hiding the overlay must cancel any pending delayed show callback
- hiding the overlay must release any leftover Tk grab state and return focus to the main root window
- delayed overlay show must no-op when `startup_preload_running` is already false

This prevents a stale overlay/grab race where the visible UI is loaded but tables, navigation, or text fields such as filter `Value` cannot receive clicks or keyboard input.

### Startup And Long Import Safety

Long imports, startup preload, and runtime-folder migration must fail soft. A single stuck file, locked debug log, or slow server request must not make the next app launch unusable.

Rules:

- Startup runtime migration must tolerate locked files. If a legacy `debug/`, `data/`, or shared file cannot be moved because another process holds it, skip that file and continue launching.
- Never redirect test stdout/stderr into the legacy project `debug/` folder while startup migration may move that folder. Use `D:\Hermes\local\debug` for temporary launch logs.
- Before diagnosing a frozen launch, check for stale ERP Python processes and a stale local server on `127.0.0.1:8000`. A previous import can leave an old `uvicorn local_server.app:app` process alive and make the new app talk to the wrong server.
- Heavy imports must run in a worker thread and must not hold any Tk modal grab while waiting for file parsing, network I/O, server save, sync, or refresh.
- Bulk imports must not perform expensive post-save work after every row. Save rows in a batch-oriented flow when possible; at minimum defer `load_bom_items_from_source()`, Tech Pack consumption sync, refresh, and history-style side effects until after the whole import finishes.
- After a bulk import worker finishes, refresh/sync once and report completion on the main thread. If the final sync is slow or fails, show an issue message without locking the app.
- If a user reports that `Preparing workspace` finished but the app cannot be clicked, first suspect a stale grab/overlay or stale server process, not a normal data problem.

### Host-First Data Safety

Host-first does not mean the desktop can assume every host endpoint is available or complete.

For each load/save/delete flow, keep these layers clear:

- dedicated host API endpoint
- host app-setting fallback
- local runtime fallback only when truly needed

If the host returns `404`, `500`, `502`, `503`, `504`, timeout, or connection errors:

- the desktop must not break the UI
- fallback behavior must be explicit
- destructive writes must not silently fall back to the wrong source

Critical empty-response rule:

- A successful host/API response with an empty list is valid source data, not a cache miss.
- In online/server mode, if a load endpoint returns successfully with `[]` or `count = 0`, the desktop must return that empty result and must not fallback to app-setting cache or local SQLite.
- This is especially important after delete flows. If the server confirms deletion and the next refresh returns zero rows, local cache must not rehydrate deleted rows back into the UI.
- Server delete success should also clean any local mirror/cache for the deleted entity when the local identity is known, so switching data modes later does not make old deleted data appear again.
- A delete flow must verify the same identity shape that the UI row uses. If the UI row is keyed by a compound identity such as `style_code + option`, `po_id + line_id`, or `style_code + attachment_id`, the post-delete refresh must check that full compound identity. A base parent key disappearing or succeeding is not enough.
- Only use app-setting/local fallback when the host request fails, the endpoint is missing, or the code has explicitly classified the error as recoverable fallback behavior.

## Project Map

### Desktop UI Layer

- `main.py`
  - main desktop coordinator
  - creates `POInternalApp`
  - binds split modules into the app class
  - keeps central business logic, import/export orchestration, permissions, cache, and app state

- `app_modules\po_module.py`
  - Purchase Order list and detail UI
  - PO import, edit, delete, quick status, and PO history entry points

- `app_modules\costing_list_module.py`
  - Costing list UI
  - Costing import, Costing form, collection list, status, and export report

- `app_modules\bom_module.py`
  - BOM list/detail UI
  - BOM item fields, requirements, inventory, purchase fields, status, and export report

- `app_modules\supplier_module.py`
  - `BOM > Supplier list`
  - supplier add/edit/delete and supplier permissions

- `app_modules\production_module.py`
  - Production list/detail UI
  - builds Production Order rows from PO, Tech Pack, and Costing snapshots
  - shows Tech Pack parent relationship in the Production list
  - manages Factory List and active factory dropdown in Production detail

- `app_modules\tech_pack_list_module.py`
  - Tech Pack list UI
  - import, add style, edit style, edit season, delete style, image search, sync/download, and export report

- `app_modules\tech_pack_detail_module.py`
  - Tech Pack Detail window
  - tabs: `Overview`, `BOM`, `Constructions`, `Instructions`, `Stitching`, `POM`, `Samples`, `Consumption`, `Attachments`, `More`

- `app_modules\samples_module.py`
  - Samples module
  - new, copy, edit, delete, import report, export report, cover page, print

- `app_modules\hr_module.py`
  - HR module
  - employee, attendance, quick attendance, attendance overview, export report

- `app_modules\login_dialog.py`
  - login, register, forgot password, reset password token

- `app_modules\report_builder_module.py`
  - `Tool > Report Builder`
  - `Tool > Download Tech Pack`

- `app_modules\translation_module.py`
  - UI translation refresh
  - permission UI labels
  - dynamic module text updates

### Core Helpers

- `app_core\ui_helpers.py`
  - date picker
  - thumbnail helpers
  - striped `Treeview`

- `app_core\server_api.py`
  - desktop-to-host API client functions

- `app_core\permissions.py`
  - permission catalog and effective permission map helpers

- `app_core\paths.py`
  - runtime path helpers

- `app_core\version.py`
  - app name, version, and release channel

### Parser And Import Layer

- `parser.py`
  - Purchase Order PDF parser

- `costing_parser.py`
  - Costing Line Sheet parser

- `costing_ws_parser.py`
  - Costing worksheet Excel parser
  - Import WS priority: when a workbook contains `BFL` and/or `BLS`, import those sheet(s) first and do not import `Costing Worksheet`; only fall back to `Costing Worksheet` when neither order sheet exists.

- `tech_pack_parser.py`
  - Tech Pack parser for overview, BOM, construction, stitching, POM, and attachments

### Server And Database Layer

- `database.py`
  - SQLite and PostgreSQL-compatible database layer
  - migrations
  - record normalization
  - permissions, settings, Tech Pack, BOM, Samples, Production, History storage

- `local_server\app.py`
  - FastAPI local/host API
  - release endpoints
  - shared files
  - app settings
  - business record endpoints

### Build And Release

- `build_exe.py`
  - builds the Windows desktop app
  - prepares `dist\SunnyERPApp`
  - creates `dist\SunnyERPApp_package.zip`

- `SunnyERPApp.spec`
  - PyInstaller spec
  - collects required submodules including `app_modules`

- `scripts\build_app_only.bat`
  - local desktop build only
  - verifies required release files exist

- `scripts\build_and_publish_release.ps1`
  - bumps version unless skipped
  - builds desktop release unless skipped
  - verifies local release folder and package zip
  - uploads package to server
  - extracts remote release folder from the uploaded zip
  - verifies remote folder and remote zip contents
  - syncs backend source files
  - restarts backend

## Permissions

Permission source:

- `app_core\permissions.py`

The Admin permission editor renders actions from:

- `ACTIONS_BY_MODULE`
- `ACTION_KEYS`
- translation labels in `main.py` and `app_modules\translation_module.py`

Permission editor save/reload rules:

- keep the selected target account by normalized email, not by the visible account label
- the visible account label includes the role, so changing a user's role changes the label
- after saving permissions, fetch permission settings from source again to verify the persisted state
- if save fails, show an explicit error instead of silently refreshing stale permissions

Current module access keys include:

- `module.dashboard`
- `module.purchase_order`
- `module.costing`
- `module.production`
- `module.qa`
- `module.finance`
- `module.tool`
- `module.setting`
- `module.bom`
- `module.tech_pack`
- `module.samples`
- `module.hr`
- `module.history`

Important action groups:

- Purchase Order
  - `action.purchase_order.import_pdf`
  - `action.purchase_order.import_folder`
  - `action.purchase_order.export_report`
  - `action.purchase_order.delete_po`
  - `action.purchase_order.edit_po`
  - `action.purchase_order.compare_po`
  - `action.purchase_order.approve`

- Costing
  - `action.costing.import_pdf`
  - `action.costing.import_ws`
  - `action.costing.import_folder`
  - `action.costing.export_report`
  - `action.costing.adjust_crop`
  - `action.costing.delete_style`
  - `action.costing.edit_row`
  - `action.costing.collection_list`
  - `action.costing.approve`

- Production
  - `action.production.edit`
  - `action.production.delete`
  - `action.production.export_report`
  - `action.production.packing_list`
  - `action.production.factory_list`

- BOM
  - `action.bom.new`
  - `action.bom.copy`
  - `action.bom.edit`
  - `action.bom.delete`
  - `action.bom.export_report`
  - `action.bom.generate_from_tech_pack`
  - `action.bom.approve`
  - `action.bom.supplier_list`
  - `action.bom.supplier_add`
  - `action.bom.supplier_edit`
  - `action.bom.supplier_delete`

- Tech Pack
  - `action.tech_pack.import_pdf`
  - `action.tech_pack.import_folder`
  - `action.tech_pack.export_report`
  - `action.tech_pack.delete_style`
  - `action.tech_pack.edit_style`
  - `action.tech_pack.detail_edit`
  - `action.tech_pack.detail_save`
  - `action.consumption.approve`

- Samples
  - `action.samples.new`
  - `action.samples.copy`
  - `action.samples.import_report`
  - `action.samples.export_report`
  - `action.samples.delete`
  - `action.samples.edit`

- HR
  - `action.hr.manage_shared_lists`
  - `action.hr.employee.new`
  - `action.hr.employee.edit`
  - `action.hr.employee.delete`
  - `action.hr.employee.toggle_active`
  - `action.hr.attendance.new`
  - `action.hr.attendance.delete`
  - `action.hr.attendance.quick`
  - `action.hr.attendance.overview`
  - `action.hr.attendance.check_in`
  - `action.hr.attendance.check_out`
  - `action.hr.attendance.edit`
  - `action.hr.export_report`

- Settings
  - `action.setting.save_online_setup`
  - `action.setting.test_connection`
  - `action.setting.check_updates`
  - `action.setting.save_mail_settings`
  - `action.setting.send_test_email`

- Tool
  - `action.tool.report_builder`
  - `action.tool.download_tech_pack`

Recent permission audit result:

- every key used by `register_permission_widget(...)` exists in the permission catalog
- every key checked by `is_action_allowed(...)` exists in the permission catalog
- labels in `main.py` and `app_modules\translation_module.py` match the permission catalog

## Search And Filter Template

List modules should use the shared search/filter pattern unless a module has a documented exception.

Current modules using this template:

- Purchase Order
- Costing
- Tech Pack
- Samples
- BOM
- Production

Rules:

- Keep the first filter row as the root row: `Select`, `Value`, `From`, `To`, then `Sort by`, `Order`, `+ Filter`, and `Clear` on the same row.
- Additional `+ Filter` rows must stay in the same controls frame and align to the root filter columns.
- Additional rows include `Select`, `Value`, `From`, `To`, and `Remove`.
- Multiple filters combine with `AND`.
- Date fields enable `From` / `To` and disable `Value`; non-date fields enable `Value` and disable `From` / `To`.
- Text field filters should use partial contains matching. Keep exact matching only for fields where partial matching would be ambiguous, such as boolean `Active` status.
- Search/filter execution should happen only after the user presses `Enter` in a filter input, or after an explicit action such as `Clear` / `Remove`. Do not filter on every key press or date variable write because large datasets can freeze the desktop UI.
- Field selection changes should update input state only. They should not apply the filter until the next explicit submit.
- Sorting can apply immediately, but it must use the last committed filter snapshot, not unsubmitted text currently being typed.
- Future modules should reuse `app_core/filter_helpers.py` for dynamic filter rows and translation refresh, then implement only module-specific value extraction and date-field lists.

## Purchase Order

### Main List

The main `PO` list shows the latest version of each PO Number.

Old versions are still kept in the database/source for history comparison, but are not shown as duplicate rows in the main list.

The list expands each real `mfg_style` into its own display row:

- a PO with several styles appears as several UI rows
- each UI row still maps back to the original PO record
- operations such as detail, history, delete, edit, and export must resolve back to the original PO ID

Rules:

- Do not display a comma-joined style string as a single style.
- Search by `Style` must match any `mfg_style` inside PO line items.
- Search by `PO Number` must normalize leading zeroes for comparison.
- If the source has `PO#0120728` and storage has `120728`, searching either value should find the record.
- If a PO Number starts with `0`, the UI must preserve the leading zero when displaying it.
- Field-specific PO filters should use partial text matching for table columns, not exact matching. Date fields use the date range inputs, and `PO Number` also supports normalized leading-zero comparison.
- PO list follows the shared Search And Filter Template.
- The PO list shows `Version #` after `Revised time`; this is the count of saved versions for the PO Number.
- The PO list shows line-item `Cost`, `Comp`, and `Retail` after `Size Scale`.
- For style-specific display rows, line-item `Cost`, `Comp`, and `Retail` must be summarized from that style's items only.
- If a style has multiple unique `Cost`, `Comp`, or `Retail` values, display the unique values as a comma-separated list.
- Petite size labels from PO items must display in the compact Tech Pack format: `PETSM` / `PET SM` as `PS`, `PETMD` / `PET MD` as `PM`, and `PETLG` / `PET LG` as `PL`.
- The PO list footer shows `Filtered Total` for `Total Qty`, calculated from the currently filtered rows.

### PO Detail

`PO Detail` opens in a separate window by double-clicking a PO row.

Current tabs:

- `Header`
- `Items`
- `Notes`

When opened from a style-specific PO row:

- `Items` should filter to that style
- searching by PO Number should show all line items for the PO
- searching by style or all fields may further filter the item table

The `Items` tab includes totals:

- `Filtered Total Packs`
- `Filtered Total Units`

These totals represent only the currently visible item rows after filtering.

### PO History

The old `Compare PO` toolbar button has been renamed to `PO history`.

Rules:

- `PO history` works on the currently selected PO.
- It compares consecutive versions of the same PO Number.
- Version order uses `created_at`, `updated_at`, and `id`.
- The comparison shows header changes and line item changes.
- PO history comparison must compare all business header fields and all line item fields that exist in the payload.
- Technical metadata such as `id`, `created_at`, `updated_at`, and `raw_json` is excluded from content comparison.
- `status`, `is_active`, `source_pdf`, and `SimpleDomesticPT` are part of the comparison.
- Duplicate imports are skipped if the content signature is unchanged.
- `SimpleDomesticPT` is part of the content/version comparison, but it is not a separate PO key.

### Quick Status

`Quick Status` updates the status of one or more POs.

Rules:

- checked rows are used first
- if no row is checked, the selected row is used
- only status is changed
- original customer-sent PO fields must not be edited
- changing to `approved` requires `action.purchase_order.approve`
- status changes are saved as new versions for PO history
- batch updates must run in a worker thread
- do not call a blocking `refresh_orders()` after Quick Status

## Tech Pack

### List

`Tech Pack` list rows are built from:

- `tech_pack_library`
- Costing snapshot
- Samples snapshot
- Purchase Order style mapping

Important fields:

- `style_code`
- `style_identity`
- `season`
- `season_base`
- `season_year`
- `buyer`
- `parent_summary`
- `po_summary`
- `bom_summary`
- `construction_summary`
- `sample_summary`
- `production_summary`
- `quality_summary`
- `latest_user`

`style_identity` is the business identity of a style version:

```text
Style Number + Season-Year
```

Example:

```text
RLL1560 HOLIDAY-27
```

`style_code` remains the technical/source key in `tech_pack_records`.

### Add Style

`Tech Pack > Add Style > Save` has been optimized.

Old behavior:

- save one style
- call `refresh_tech_pack_module()`
- reload Costing snapshot
- reload full Tech Pack library
- reload hidden styles
- rebuild all rows
- redraw the whole tree

This caused long UI lag as data grew.

Current behavior:

- save the new style record
- collect existing raw Season options from Tech Pack, Costing, and PO data
- let the user set `Season` from a dropdown and enter `Season Year`
- save `season`, `season_year`, `season_display`, and `style_identity` on the new Tech Pack record
- save the same raw season/year into `overview_data` so Tech Pack Detail opens with matching Overview values
- upsert the saved record into `tech_pack_library` in RAM
- if the style was previously deleted/hidden, remove it from `tech_pack_hidden_styles`
- rebuild `tech_pack_rows` from current in-memory state
- apply the filter and redraw the list locally
- save the `tech_pack` session cache
- reset filter field to `All`
- clear date filters
- search/focus the newly added style so it is visible immediately

This avoids a full source/server refresh after adding one style.

Season dropdown rules:

- dropdown values are raw seasons, not combined display values
- source values such as `SPRING 27`, `SPRING-27`, `SPRING-2027`, and `SPRING/27` are normalized to `SPRING`
- season names that legitimately contain numbers without a year separator, such as `BTS/SUMMER2`, are kept as-is

### Edit Season

`Tech Pack > Edit season` updates raw season data for checked styles.

Behavior:

- the toolbar button sits beside `Search by image`
- the action uses the same `action.tech_pack.edit_style` permission as other style edits
- at least one Tech Pack row must be checked
- the popup has `Season` and `Season Year`
- `Season` uses the same raw-season dropdown rules as Add Style
- if all checked styles share the same season/year, the popup pre-fills those values
- save patches each checked Tech Pack record with `season`, `season_year`, `season_display`, `style_identity`, and matching `overview_data`
- after saving, Tech Pack rows are rebuilt from RAM and the session cache is updated
- Samples and Production are refreshed when available because they read season from Tech Pack

### Download Tech Pack Automation

`Tool > Download Tech Pack` and `Tech Pack > Sync Tech Pack` use Playwright automation against the customer site.

Rules:

- Do not rely on fixed sleeps as the main control flow.
- Each step must have an anchor/checkpoint before moving on: login controls visible, search controls visible, `Style No` input value verified, result row detected, detail page verified by style marker and `Tech Pack Report` button, report dialog verified, then report/PDF target verified.
- Login is a common miss point. Always verify the `User ID` and password fields received values, submit with retry, handle `End other session`, and confirm the page has left the login form before opening Tech Pack search.
- Search result matching must prefer exact normalized style code before substring fallback, so similar style codes do not open the wrong record.
- If a click does not produce the expected next state, retry the same action after a short wait and capture debug snapshots when debug mode is enabled.
- Keep status messages specific to the current style and step so missed anchors are diagnosable from the latest log.

### Detail Window

`Tech Pack Detail` uses session window cache.

Rules:

- first open renders `Overview`
- other tabs render lazily when clicked
- closing the window withdraws it instead of destroying it
- reopening the same style in the same session reuses the cached window
- app close clears the detail window cache

When editing minor data inside a cached detail window:

- patch the related record
- update `tech_pack_library` in RAM
- refresh only the current panel
- do not call a full `refresh_tech_pack_module()` unless the workflow truly needs a full reload

### Season And Style Identity

`Overview` keeps two separate fields:

- `Season`
- `Season Year`

Display fields can combine them as:

```text
Season-Year
```

Examples:

- `BTS/Summer2` + `27` -> `BTS/Summer2-27`
- `HOLIDAY` + `27` -> `HOLIDAY-27`

If source data already contains `Season-Year`, the app should split it back into separate `Season` and `Season Year` fields for the Overview form.

### Baby Style

`Tech Pack Detail > Overview` supports adding Baby Styles.

Current behavior:

- the `+` button opens a manual table input popup
- each row is one baby style
- save links all valid styles to the current lead style
- missing baby styles are created as default Tech Pack records
- the baby style record is patched with `overview_data.lead_style_no`
- `lead_season` and `lead_season_year` are copied from the lead style when available
- after linking/unlinking, the app refreshes Tech Pack from the source/server, updates RAM, refreshes the current panel, and writes the refreshed Tech Pack cache

Important persistence rule:

- If Baby Style links appear after saving or after pressing `F5`, they must also appear immediately after closing and reopening the app.
- In online/server mode, Tech Pack startup must not hydrate Baby Style state from stale `main_ui` session cache before checking the server.
- The server `tech_pack_records` table is the source of truth; local cache must never be allowed to make a saved Baby Style relationship appear lost.
- The `Add Baby Style` popup must remain modeless and its save must run in the background. Do not call `grab_set()` in this popup, because Alt-Tab/window switching can leave both the main UI and Tech Pack Detail unclickable.

Unlinking must set these fields to empty values:

- `overview_data.lead_style_no`
- `lead_season`
- `lead_season_year`

Do not just remove keys, because record-patch merge behavior does not delete missing keys on the server.

## BOM

### BOM List And Detail

`BOM` is a separate module:

- `app_modules\bom_module.py`

It is connected to:

- `main.py`
- `database.py`
- `app_core\server_api.py`
- `local_server\app.py`

The BOM list follows the standard list tab layout.

The `Export` column is first because the list does not have an image column.

The BOM list shows `Section` after `Type`; this value comes from `Edit BOM Item > Basic Information > Section`.

The BOM list filter and sort dropdowns must use the same field order and visible labels as the table headers, except for the leading `All` filter option. Do not keep obsolete field labels such as `PO Number` when the table header is `Linked POs`.

`BOM > Quick Status` updates the status of checked BOM rows in a background worker. If no rows are checked, it uses the selected BOM row. Changing to `approved` requires the `Approve BOM` permission. After the worker saves, the BOM list must refresh and verify the updated item IDs show the target status before reporting success.

### FDS Fabric Import

FDS import creates fabric BOM items from `Fabric Detail Sheet (FDS)` workbooks.

Rules:

- `BOM > Import FDS` opens a choice menu with `Single file` and `Folder`.
- `Single file` imports one Excel FDS workbook.
- `Folder` imports all immediate `.xls`, `.xlsx`, and `.xlsm` files in the selected folder and skips temporary Excel files whose names start with `~$`.
- Import one BOM item per print type: `Solid`, `Print`, and `Digital Print`.
- If color data is available, split again by color.
- Business identity is `item_code`; `item_id` remains a hidden technical key.
- Before importing, check existing BOM items by `Article / Part No` and `FDS Date`. If both values match an existing item, treat the FDS as a duplicate, notify the user, and skip that file.
- Item code format is `{article}-{print_type}` when no color exists, for example `WFS258697HLDS-DIGITAL`.
- Item code format is `{article}-{color}-{print_type}` when color exists.
- `section` should use the FDS `Commodity (Knit/Woven/Yarn)` value, such as `Knit`, not the generic item type `Fabric`.
- `fds_date` stores the date the supplier sent the FDS file.
- Purchase detail includes both bulk `Lead Time` and `Sample Lead Time`.
- FDS `SURCHARGE FOR UNDER MOQ/MCQ` is free-form supplier text. Do not parse it into `Charge MOQ` or `Charge MCQ`; store the raw text in `Purchase Note`.
- Keep full parsed FDS data in `source_payload_json` so unmapped testing/customs/dye details are not lost.

### Bulk Fabric Import

`BOM > Import Bulk Fabric` creates fabric BOM items from booking/bulk fabric workbooks.

Rules:

- The button sits next to `Import FDS`.
- Clicking the button opens a choice menu with `Single file` and `Folder`.
- Import work runs in a background worker thread; worker code must not touch Tk widgets directly.
- Import reads all sheets in the selected `.xls`, `.xlsx`, or `.xlsm` workbook.
- Folder import reads only immediate files in the selected folder and skips temporary Excel files whose names start with `~$`.
- Folder import must only process one Excel format at a time. If the folder contains mixed supported extensions, such as both `.xlsx` and `.xlsm`, stop with a warning instead of importing a mixed batch.
- Header rows are detected by column names, not fixed row numbers.
- `Fabric` maps to `item_name`.
- `Type of fabric` maps to `section`.
- `Item type` is always `Fabric`.
- `Fabric code` maps to `item_code` and `article_ref`.
- `Fabrication` maps to `composition`.
- `Color` maps to `color`.
- `Color pantone` maps to `color_code`.
- If the `Color` or `Color pantone` cell is formatted with strikethrough, treat that color row as canceled and skip it. Do not create BOM, approve BOM, or sync Production consumption for struck-through color rows.
- Strikethrough detection is supported for `.xlsx` and `.xlsm`; legacy `.xls` formatting may not be detectable by the current parser.
- `CW` maps to width; prefer `CW (CM)` when present, otherwise use `CW`.
- `Weight` maps to `gsm_value` / `gsm_unit`.
- `Loss` maps to `waste_percent`; multiplier values are converted to percent, for example `1.02` becomes `2`.
- `Cons` maps to Tech Pack Detail `Consumption > Production > Consumption`.
- `Fabric Qty (kg)` maps to Tech Pack Detail `Consumption > Production` material lines as `fabric_weight` with unit `KG`, then displays in Production Order Detail `Materials > Fabric Weight`.
- Bulk-imported rows use BOM version `BULK`.
- Bulk-imported BOM items are saved as `approved`.
- After saving BOM items, import syncs each imported item into the matching style's Tech Pack Detail `Consumption > Production` lines. If the line already exists, update it; otherwise add it. Use the per-style `Cons` value from the workbook, because one shared BOM item can have different consumption values in different styles.
- Rows without a numeric `Fabric Qty (kg)` leave `Fabric Weight` blank.
- If all mapped material fields match exactly, keep one BOM item and append all matching styles into `style_code`, separated by `, `.
- If any mapped material field differs, keep separate BOM items.
- If separate BOM items would collide on the same `style_code + bom_version + item_code`, append a normalized color/hash token to `item_code` so different material rows do not overwrite each other.
- Consumers must treat BOM `style_code` as a possible comma-separated style list and match an individual style against any token in that list.
- BOM requirement/Linked PO calculations must also treat BOM `style_code` as a possible comma-separated style list. For shared BOM items, aggregate consumption and approved PO demand from every style token, not from the normalized concatenated string.
- Keep the original parsed workbook row in `source_payload_json` so unmapped source data is not lost.

### Bulk Trims Import

`BOM > Import Bulk Trims` creates trims, label, and packaging BOM items from balance-sheet workbooks whose target sheet starts with `THE BALANCE SHEET`.

Rules:

- The button sits next to `Import Bulk Fabric`.
- Clicking the button opens a choice menu with `Single file` and `Folder`.
- Import work runs in a background worker thread; worker code must not touch Tk widgets directly.
- Import supports `.xlsx` and `.xlsm` files.
- Import reads every sheet whose `A1` is `THE BALANCE SHEET`, detects the trim table header by column names such as `ITEM`, `DESCRIPTION`, `UNIT`, `TRIM COLOR`, `SIZE`, `CONS`, and `LOSS`, and deduplicates repeated sheets by `style_code + item_code`.
- Some balance sheets do not have a `STYLE#` column in the material table. In that layout, use the worksheet title as `style_code` when it looks like a style code, for example `CBL6508` or `CGL2226`.
- Import must understand merged cells, fill-down cells, and simple same-sheet formulas that point back to the order summary style/color rows.
- Rows with blank `STYLE#`, no formula reference, and no valid sheet-title style are skipped. Do not assign common blank-style rows such as `MOBILON TAPE` or `LOGO NECK TAPE` to every style automatically.
- Do not import `SELF FABRIC` rows through Bulk Trims; those belong to Bulk Fabric/FDS logic.
- For label/packaging sections that are shared by size, such as `Main Label`, `Care Label`, `Hangtag`, `Price Sticker`, and `Carton Barcode Sticker`, do not fill a blank `TRIM COLOR` cell from the balance-sheet style/color summary. A blank source color means the item is common across PO colors. Thread rows and rows with an explicit `TRIM COLOR` value still keep their material color.
- Bulk-imported trim rows use BOM version `BULK-TRIMS`.
- Bulk-imported trim rows are saved as `checking`.
- `checking` and `draft` Bulk Trims rows must show `Linked POs = -` and all requirement/inventory derived quantities as zero. Only `approved` BOM rows may link to PO demand, Tech Pack consumption, Production, Materials PO, Warehouse, or auto-generated material demand.
- Bulk Trims save must defer expensive post-save sync/refresh work until the full import finishes. A 200-row import must not run `load BOM + sync Tech Pack` 200 times.
- `ITEM` maps to `item_name`.
- `STYLE#`, the referenced order-summary style, or the valid sheet title maps to `style_code`.
- `DESCRIPTION` maps to `description` and contributes to `spec_summary`.
- `UNIT` maps to `uom` and `purchase_uom`; normalize `METER` to `M` and `PCS` to `EA`.
- `TRIM COLOR` maps to `color`; if a direct style row has no explicit trim color, use the matching order-summary color when available, except for the shared label/packaging sections listed above.
- `SIZE` maps to `trim_size_um`.
- BOM requirement/Linked PO matching normalizes plus-size labels from balance sheets: `1XL`, `2XL`, and `3XL` must match PO sizes `1X`, `2X`, and `3X`; petite aliases such as `PETSM/PETMD/PETLG` normalize to `PS/PM/PL`.
- `CONS` maps to `qty_per_garment`.
- `LOSS` maps to `waste_percent`; multiplier values are converted to percent, for example `1.08` becomes `8` and `1.1` becomes `10`.
- `ORDER Q'TY`, `NEED Q'TY`, `PLACED ORDER Q'TY`, and `ACTUAL IN_HOUSE` stay in `source_payload_json`; they do not directly create Materials PO or inventory records.
- Classification:
  - `MOBILON TAPE` -> type `Trims`, section `Mobilon Tape`
  - `LOGO NECK TAPE` -> type `Trims`, section `Logo Neck Tape`
  - `THREAD` -> type `Trims`, section `Thread`
  - `ELASTIC` -> type `Trims`, section `Elastic`
  - `PRICE TICKET` / `PRICE STICKER` -> type `Packaging`, section `Price Sticker`
  - `PRICE STIKER` -> type `Packaging`, section `Price Sticker`
  - `HANGTAG` / `HANG TAG` -> type `Packaging`, section `Hangtag`
  - `HANGER` -> type `Packaging`, section `Hanger`
  - `HEAT TRANSFER LABEL` -> type `Label`, section `Heat Transfer Label`
  - `MAIN LABEL` -> type `Label`, section `Main Label`
  - `CARE LABEL` -> type `Label`, section `Care Label`
  - `CARTON BARCODE STICKER` -> type `Packaging`, section `Carton Barcode Sticker`
  - `ZIPPER` -> type `Trims`, section `Zipper`
  - `INTERLINING` -> type `Trims`, section `Interlining`
- Keep the original parsed workbook row in `source_payload_json` so unmapped source data is not lost.

### Supplier List

`BOM > Supplier list` is handled by:

- `app_modules\supplier_module.py`

The supplier list supports:

- add supplier
- edit supplier
- delete supplier
- active/inactive status
- permission-controlled buttons

Supplier permissions:

- `action.bom.supplier_list`
- `action.bom.supplier_add`
- `action.bom.supplier_edit`
- `action.bom.supplier_delete`

### Purchase Fields

`Edit BOM Item > Purchase` includes fields for material purchasing:

- Default Supplier
- Standard Price
- currency
- MOQ
- MOQ unit
- Charge MOQ
- Charge MOQ unit
- MCQ
- MCQ unit
- Charge MCQ
- Charge MCQ unit
- Lead Time
- Lead Time unit
- Sample Lead Time
- Sample Lead Time unit
- Purchase UOM
- FDS Date

UOM/currency/unit fields are normalized to uppercase on save.

`Default Supplier` should only show active suppliers.

### Requirement And Materials PO Notes

`Production Req` and `Settlement Req` are analytical demand fields.

They must not automatically create Materials POs unless a clear business rule has been approved.

Current Materials PO generation formula:

```text
Booking Req - (On Hand + Ordered but not yet received)
```

`BOM Ordered Qty` is the total of active Materials PO quantity, not only approved Materials PO quantity.

## Costing

### Costing List

The Costing module supports:

- Line Sheet PDF import
- WS Excel import
- folder import
- export report
- crop adjustment
- delete style
- edit row
- collection list
- approval permission

### Costing Form

`Costing form` is available from the Costing toolbar.

It is the source of truth for:

- labor cost
- import cost
- logistics cost

Tech Pack Detail continues to hold:

- style specs
- BOM data
- consumption data

Costing form data is stored host-first through app-setting key:

- `costing_form_records`

The Excel template is stored as a shared host asset and cached locally at:

```text
D:\Hermes\local\data\costing_form_assets
```

The app should use `costing_form_assets.template_url` when available and should not rebuild the template locally on every export.

## Tech Pack Detail > Consumption

Consumption versions:

- `Costing`
- `Booking`
- `Production`
- `Settlement`
- `Sample`

Rules:

- `Costing`, `Sample`, `Booking`, `Production`, and `Settlement` can only link to BOM items with status `approved`.
- BOM items with status `draft` or `checking` must not link into other tabs and must not create material demand.
- The picker for consumption tabs uses the shared approved BOM list, not only BOM items generated from the current style.
- If a BOM item changes away from `approved`, cleanup should remove it from all consumption tabs.
- `Production` consumption is auto-synced from approved BOM items that have `Qty Per Garment` greater than zero. BOM rows that differ only by size but share the same item type, section, name, color, spec, UOM, consumption, and waste are grouped into one consumption line with `Trim Size = ALL`. The line keeps a representative approved BOM item ID/code for status cleanup and stores the full source BOM item list in `source_payload_json`.
- BOM requirement/Linked PO calculation must use approved PO line items. If `Trim Size` is a garment size such as `S`, `M`, `L`, `XL`, `PS`, `PM`, or `PL`, calculate demand from matching PO line-item size only. Non-garment trim sizes such as thread cone size `2500M` still link by style-level PO demand; their requirement remains zero when consumption is zero or blank.
- Thread BOM items use `Trim Size = ALL`; cone length belongs in `Length` and `Length Unit` such as `2500` + `M`. Thread requirement is calculated in meters first, then divided by cone length and rounded up to the next whole cone.

UOM sync:

- editing BOM `Purchase UOM` updates matching consumption lines
- editing `Consumption UOM` and saving can update BOM `purchase_uom` and `uom`
- matching prefers `bom_item_id`, then falls back to `item_code`

## Samples

The Samples module supports:

- new sample
- copy sample
- edit sample
- delete sample
- import sample report
- export report
- cover page
- print
- sync with downloaded Tech Pack data

Samples must use async import/report flows where file processing is heavy.

## Production

### Production List

The Production module lives in:

- `app_modules\production_module.py`

The Production tab follows the standard list layout:

- toolbar
- filter/search row
- main table
- status bar

Toolbar buttons:

- `Edit`
- `Delete`
- `Export report`
- `Packing list`
- `Factory list`

The main table column order starts with:

1. `Image`
2. `Export`

Then:

- `Create Time`
- `Production Order`
- `Factory`
- `MR P.I.C`
- `Buyer`
- `Style`
- `Parent`
- `Season`
- `PO Number`
- `PO date`
- `PO time`
- `Ship date`
- `Cancel date`
- `Ex-fty date Plan`
- `ACTUAL EX-FTY`
- `Color`
- `Size Range`
- `Size Scale`
- `Order Qty`
- `Ship Qty`
- `Status`
- `Over/Short ship qty`
- `Over/Short ship %`
- `Final inspection date`
- `Carton Qty`
- `ODD CARTON QTY`
- `CBM`
- `Net weight`
- `Gross weight`
- `Revised time`

Rules:

- Do not generate one row for every color/size combination.
- A PO may contain many line items, but Production list rows must be grouped by the intended Production Order logic.
- Preserve leading zeroes in displayed PO numbers.
- `Parent` is synchronized from Tech Pack by style and appears after `Style`.
- `Parent` uses Tech Pack list `parent_summary` first, then falls back to `overview_data.lead_style_no`.
- `Parent` must resolve during app startup/reopen even if the Tech Pack tab has not loaded yet.
- `Parent` must not be a refresh-only value; if it appears after `F5`, it must also appear after app restart.
- `Season` must link by style with PO, Costing, and Tech Pack.
- `Season` must follow the same startup-safe fallback rule as `Parent`.
- Production uses the shared local SQLite cache.
- The main Production cache snapshot is `production_rows`.
- `F5` forces source/server refresh and writes a new cache.
- Opening the app again in the same boot session can hydrate Production from cache.

### Production Order Detail

The Production Order Detail window currently has a working structure for further design:

- `Overview`
- `PO & Quantity`
- `Ex-Factory`
- `Cutting`
- `Packing`
- `Inspection`
- `Materials`
- `History / Notes`

`Overview` includes PO-derived fields:

- `Dept Number`
- `Store`

`PO & Quantity` shows PO line item data:

- `SKU`
- `Style`
- `Color`
- `Size`
- `Description`
- `UPC`
- `Cost`
- `Comp`
- `Retail`
- `Pack Qty`
- `Total Packs`
- `Total Units`

It also includes:

- `Unit per pack`
- `Color count`

`Unit per pack` is derived from total `Pack Qty`.

PO line items in this tab are grouped by color because Packing List export is packed by color.

Shared PO/order fields can remain grouped at the top of the detail window, but color-specific quantity fields must live inside each color tab.

Each color tab includes its own:

- `Size Range`
- `Size Scale`
- `Order Qty`
- `Ship Qty`
- `Over/Short ship qty`
- `Over/Short ship %`
- `Unit per pack`
- PO line item table for that color

`Ship Qty` is editable per color because different colors in the same PO can ship different quantities.

`Unit per pack` is calculated per color because each color/size system can use a different pack ratio.

`Size Range` and `Size Scale` show only the sizes that actually exist in the PO for that color.

Color-specific ship quantities are saved in:

- `color_quantity_json`

The Production row's main `Ship Qty` is the total of all color-level ship quantities.

`Production Order Detail > Materials` is linked from `Tech Pack Detail > Consumption > Production` by normalized style code. It displays the latest Production consumption lines and calculates `Required Qty` from `Ship Qty` when available, otherwise `Order Qty`, multiplied by `Consumption` and `Waste %`. It also shows `Fabric Weight` for fabric material lines when the source import provides actual fabric usage. Saving a Production Order stores the displayed materials as `materials_json` for cache/export fallback, but the next detail open should refresh from Tech Pack consumption first.

Material lines in `Production Order Detail > Materials` must be compared with PO colors for the style. If a material color matches one of the style's real PO colors, display it only for Production Orders whose PO detail contains that color. If a material color does not match any PO color for the style, treat it as shared/common across all PO colors, such as Mobilon `CLEAR`. Common/shared color labels such as blank, `ALL`, `CLEAR`, or `DTM` remain visible. This prevents a one-color PO from pulling material demand for other colors in the same style while preserving color-independent trims.

In `Production Order Detail > Materials`, the `Consumption` and `Fabric Weight` cells are editable. Saving the Production Order syncs edited values back to the matching `Tech Pack Detail > Consumption > Production` line. `Fabric Weight` accepts a plain number or a value with unit, such as `100 KG` or `220 LBS`.

`Production Order Detail > Cutting` sits before `Packing` and currently stores:

- `Actual Cut Qty`
`Fabric Weight` is kept in `Materials`, not `Cutting`, because it belongs to the material line that consumed the fabric.

Production Order Detail window state:

- stores geometry under `production_detail_window_geometry`
- restores the last size and position when opened again
- captures geometry when the user clicks `Save`, `Close`, or the window close button

### Production Packing

The `Packing` tab in Production Order Detail includes:

- `Unit per carton`
- `Carton dimension`
- `Carton Qty`
- `ODD CARTON QTY`
- `ODD carton dimension`
- `CBM`
- `Net weight`
- `Gross weight`

Packing calculation rules:

- `Carton Qty` is the integer part of total color `Ship Qty / Unit per carton`.
- remaining quantity is stored as `ODD CARTON QTY`.
- `ODD carton dimension` is `60 x 48 x 25 (cm)` when the odd quantity is at least half of `Unit per carton`.
- `ODD carton dimension` is `60 x 48 x 15 (cm)` when the odd quantity is less than half of `Unit per carton`.

Production detail packing fields are saved as Production overlays and must be merged back when Production rows are rebuilt from PO data.

The merge list must include:

- `unit_per_carton`
- `carton_dimension`
- `odd_carton_dimension`
- calculated carton fields
- weight fields
- `color_quantity_json`

After saving Production detail, the app refreshes the in-memory Production rows and updates both main UI local SQLite cache and session cache.

### Production Packing List Export

The `Packing list` toolbar button exports a Packing List Excel file for selected Production rows.

Selection behavior:

- checked Production rows are exported
- if no row is checked, the current selected row is exported
- multiple selected rows are exported into one workbook with multiple sheets
- each color is exported as its own sheet
- each sheet name is based on `Style + PO + Color`, sanitized for Excel's sheet name rules

The export uses the clean Excel template configured by app setting:

- `production_packing_list_assets.template_url`

Current hosted template URL:

```text
https://texmind.xyz/shared/packing-list-assets/packing_list_form.xlsx
```

Server file location:

```text
/home/deploy/sunny_erp_local/shared/packing-list-assets/packing_list_form.xlsx
```

Client cache location:

```text
D:\Hermes\local\data\packing_list_assets
```

Fallbacks:

- bundled template: `assets\templates\packing_list_form.xlsx`
- legacy local fallback: `C:\Users\ADMIN\Desktop\Packing list form.xlsx`

Packing List export fills:

- style
- PO number, preserving leading zeroes
- store
- dept
- color
- size ratio
- unit per pack
- carton quantity
- odd carton quantity
- box size
- CBM
- summary quantities
- net/gross weight when available

Size systems:

- Petite: `PXS`, `PS`, `PM`, `PL`, `PXL`
- Missy: `XS`, `S`, `M`, `L`, `XL`
- Women: `1X`, `2X`, `3X`

Packing List export orders size columns according to the detected size system for each color.

Do not add missing sizes just to complete a size system. For example, if a PO color has `S-M-L-XL`, do not add `XS` only because Missy supports `XS`.

Petite size display mapping:

- `PET SM` exports as `PS`
- `PET MD` exports as `PM`
- `PET LG` exports as `PL`
- `PET XL` exports as `PXL`

The clean template removes unsupported conditional formatting extensions so `openpyxl` can load and save it without the previous conditional-formatting warning.

Excel template export safety:

- Do not use `openpyxl.copy_worksheet()` as the normal export path for user-facing template workbooks.
- Create a new clean `Workbook()` for the exported file, then clone only the required template sheet content and layout into each output sheet.
- Safe sheet data to clone includes cell values, cell styles, merged ranges, row heights, column widths, page margins, print options, and whitelisted page setup values such as orientation, paper size, scale, and DPI.
- Do not clone workbook-level template sheets, drawings, printer settings relationships, or custom worksheet relationships unless the export explicitly rebuilds the matching `.rels` parts. Broken sheet relationships can make Excel repair `/xl/worksheets/sheet*.xml` and may destroy the visible layout after the user opens the file.
- Keep copied column dimensions limited to the template's real used columns. Avoid carrying style ranges such as `max=16384` into generated sheets unless the workbook truly requires all Excel columns.
- Current Production Packing List and Carton Sticker exports follow this clean-workbook clone path.

### Production Style Season Shared Fields

Production rows use `style + season` as the business grouping key, matching Tech Pack.

When one Production PO row is edited, these fields are shared across all Production PO rows with the same `style + season`:

- `Factory`
- `MR P.I.C`
- `Unit per carton`

After `Unit per carton` is synced, each PO row recalculates its own carton quantity from its own ship quantity.

The shared values are saved as Production detail overlays so they remain synchronized after refresh and app restart.

### Production Carton Sticker Export

The `Carton sticker` toolbar button exports carton label stickers for selected Production rows.

Selection behavior matches `Packing list`:

- checked Production rows are exported
- if no row is checked, the current selected row is exported
- each color is exported as its own sheet
- each sheet is based on the store-specific clean template sheet

The export uses the clean Excel template configured by app setting:

- `production_carton_sticker_assets.template_url`

Current hosted template URL:

```text
https://texmind.xyz/shared/carton-sticker-assets/20260727/e572a1c5a646458e8cc18129f6a5f7ed/Bealls_Carton_Label_Sticker_TAG.xlsx
```

Client cache location:

```text
D:\Hermes\local\data\carton_sticker_assets
```

Fallbacks:

- bundled template: `assets\templates\Bealls_Carton_Label_Sticker_TAG.xlsx`
- legacy local fallback: `C:\Users\ADMIN\Desktop\Bealls_Carton_Label_Sticker_TAG.xlsx`

Template contract:

- store-specific sheets are named like `STORE#50`, `STORE#115`, `STORE#995`
- the export picks the sheet by Production `store_number`
- if a store sheet is missing, that row is skipped and reported after export
- the text block fills `PO#`, `COLOR`, and leaves `Size:` blank
- the size table below the text block carries the size ratio
- master carton and odd carton both show the same one-inner-pack size ratio
- odd carton quantity is represented by the text line, for example `Inner packs: 8pcs/4ppk`, not by multiplying the ratio table
- Carton Sticker export must use the clean-workbook clone helper instead of copying/removing template worksheets. Store sheets can contain page setup and column metadata that Excel may repair if copied with stale relationships.

### Factory List

The Production toolbar includes:

- `Factory list`

Factory List follows the same design pattern as:

- `BOM > Supplier list`

Factory records support active/inactive status.

Production detail `Factory` field is a dropdown and must only show active factories.

Initial demo data contains three factories.

## HR

The HR module supports:

- employee list
- new employee
- edit employee
- toggle employee active status
- attendance list
- new attendance
- edit attendance
- delete attendance
- quick attendance
- attendance overview
- check in
- check out
- export report

HR actions are permission controlled through the `action.hr.*` keys.

## History And Latest User

The app records entity history for major business records.

History is used for:

- audit trail
- latest user display
- record change summaries

Large history payloads must be compacted before saving.

If history save fails because of payload size, the main business save must not fail only because History failed.

## Build And Publish

### Local Build

Build the desktop app locally:

```powershell
scripts\build_app_only.bat
```

The local build must produce:

- `dist\SunnyERPApp`
- `dist\SunnyERPApp_package.zip`

Required package content includes:

- `SunnyERPApp.exe`
- `app_core`
- `app_modules`
- `app_modules\production_module.py`
- `local_server`
- `database.py`
- `requirements.txt`
- `settings`

### Publish Release

Publish release:

```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\build_and_publish_release.ps1
```

The publish script:

- bumps the version
- builds the desktop release
- verifies local release folder contents
- verifies the package zip contains all release files
- uploads `SunnyERPApp_package.zip`
- extracts the server release folder from that zip
- verifies remote folder and remote zip contents
- syncs backend source files
- restarts backend
- verifies release endpoints

Important release endpoints:

- `https://texmind.xyz/api/releases/latest`
- `https://texmind.xyz/manifests/desktop.json`

The release process was hardened to prevent downloaded builds from missing `app_modules`, especially:

- `app_modules\production_module.py`

This prevents startup errors such as:

```text
'POInternalApp' object has no attribute '_build_production_module'
```

## Running Local Host For Testing

The local host mode is used before or beside VPS/host testing.

The local backend runs at:

```text
http://127.0.0.1:8000
```

Important local URL:

```text
http://127.0.0.1:8000/health
```

The database layer supports both:

- SQLite
- PostgreSQL

The host selects PostgreSQL through:

```text
SUNNY_ERP_DATABASE_URL
```

The desktop app must not connect directly to PostgreSQL. It should use the host API.

## macOS Build

On macOS:

```bash
cd "/path/to/po_internal_app"
chmod +x build_mac.sh
./build_mac.sh
```

Or:

```bash
cd "/path/to/po_internal_app"
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt pyinstaller
python build_mac.py
```

Build output:

- `dist/SunnyERPApp.app`
- `dist/SunnyERPApp_macOS.zip`

GitHub Actions macOS build outputs:

- `SunnyERPApp-macOS-arm64`: for Apple Silicon Macs
- `SunnyERPApp-macOS-x64`: for Intel Macs

Each artifact contains:

- `SunnyERPApp_macOS_arm64.zip` or `SunnyERPApp_macOS_x64.zip`
- `RUN_FOR_TESTERS_macOS.txt`

On macOS, unzip the macOS zip and open `SunnyERPApp.app`. Do not run the Windows `SunnyERPApp.exe` file on macOS.

On macOS, app data is stored under:

```text
~/sunny_erp_local
```

## 2026-07-27 Session Notes

Work completed during this session:

- PO delete flow was investigated after the UI reported delete success while the row stayed visible after refresh/restart.
- PO season display was aligned with Tech Pack data so PO rows can recover season values from Tech Pack cache/source during startup and refresh, instead of staying blank until a manual refresh.
- HR moved `Manage shared lists` into the HR area.
- HR employee add/edit now includes `Display name` under `Full Name`, matching the register terminology.
- HR employee save/load/list display was adjusted for `Display name`.
- HR status bar and employee cache behavior were reviewed after add employee caused old rows to disappear from the visible list.
- HR employee tools now include `Delete employee`.
- HR employee list double-click opens edit employee.
- Tech Pack import/sync update flow was reviewed so ERP updates as soon as the server-side upload/process is done, with short polling only when the server queues work.
- Tech Pack Detail attachments now support delete beside download for imported Tech Pack files.
- Tech Pack Detail Attachment Pages now support `+` upload, download, and delete per attachment page.
- Tech Pack Detail delete actions use the Tech Pack Detail window as the messagebox parent.
- Tech Pack Detail attachment delete/save runs in a background thread so the detail window stays responsive.
- Production gained a `Carton sticker` button after `Packing list`.
- Carton sticker export uses selected Production rows, exports each color to its own store-specific sheet, leaves the text `Size:` value blank, and writes the size ratio in the table below.
- Carton sticker odd carton rows keep the same one-inner-pack size ratio in the table and express odd quantity through the `Inner packs` text line.
- The clean Bealls carton sticker template was copied into `assets\templates`.
- The clean Bealls carton sticker template was uploaded to the shared server path under `/shared/carton-sticker-assets` and stored in app setting `production_carton_sticker_assets.template_url`.
- Future clients load the carton sticker template from the shared server URL first and cache it under `D:\Hermes\local\data\carton_sticker_assets`.
- Local carton sticker templates are fallback only and should not be the normal source for users.

## 2026-07-28 Session Notes

Work completed during this session:

- PO list gained a `Version #` column after `Revised time`.
- PO version count is calculated from saved versions grouped by PO Number.
- PO history comparison now compares dynamic business header fields and line item fields instead of a fixed narrow field list.
- PO history comparison includes `status`, `is_active`, `source_pdf`, and `SimpleDomesticPT`, while excluding technical metadata.
- PO list gained line-item `Cost`, `Comp`, and `Retail` columns after `Size Scale`.
- Style-specific PO display rows summarize line-item `Cost`, `Comp`, and `Retail` from matching style items.
- PO list gained a footer `Filtered Total` for `Total Qty`.
- Historical note superseded: `Preparing workspace` must not hold a modal Tk grab. If startup preload is slow, hide/release the overlay and keep remaining data work in the background so the app remains clickable.

## 2026-08-01 Session Notes

Work completed during this session:

- BOM list gained a `Section` column after `Type`, sourced from `Edit BOM Item > Basic Information > Section`.
- `BOM > Import FDS` now opens a choice menu with `Single file` and `Folder` import modes.
- FDS folder import processes immediate `.xls`, `.xlsx`, and `.xlsm` files and skips temporary Excel files starting with `~$`.
- FDS import creates fabric BOM items by print type and color, using `item_code` as the business identity.
- FDS import no longer requires `Style Code`; `Article / Part No` and `FDS Date` are the duplicate check fields.
- If an existing BOM item has the same `Article / Part No` and `FDS Date`, the FDS file is reported as duplicate and skipped.
- FDS `SURCHARGE FOR UNDER MOQ/MCQ` is stored as raw text in `Purchase Note`; do not parse it into `Charge MOQ` or `Charge MCQ`.
- FDS import stores supplier sent date in `FDS Date`, adds `Sample Lead Time`, and keeps raw parsed data in `source_payload_json`.
- BOM delete/load behavior was fixed so a successful online API response with `[]` or `count = 0` is treated as valid source data, not a cache miss.
- After online BOM delete succeeds, local mirror/cache for that item is cleaned to prevent deleted rows from reappearing after refresh or data mode changes.
- Server-side BOM persistence was updated and smoke-tested to accept FDS BOM items without `Style Code`.

## Current Notes For Future Work

- Continue refining Production Order Detail tabs.
- Keep Production list grouped correctly; do not explode rows by color and size.
- Keep Add Style fast by avoiding full Tech Pack refresh after saving one style.
- Avoid blocking refreshes after small record patches.
- Keep permissions synchronized between button registration, permission checks, and Admin permission catalog.
- Keep release packaging checks strict so server downloads always contain all required modules.
- Ask before changing data structures or behavior that affects other modules.
