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.

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

### 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.
- 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 shows a modal loading overlay during that startup preload so users know the app is not ready for interaction yet.

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`.
- After login, keep the overlay modal until every allowed startup preload module has finished loading.
- Do not hide the overlay just because the current tab finished loading; background preload must finish through the last allowed tab first.
- If a module is still loading after the stale preload timeout, update status only. Do not release the overlay/grab while startup data is still loading.
- hiding the overlay must cancel any pending delayed show callback
- hiding the overlay must release 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 text fields such as filter `Value` cannot receive clicks or keyboard input.

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

## 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.
- 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.
- 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`.

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

### 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` can use BOM items with status `draft`, `checking`, or `approved`.
- `Booking`, `Production`, and `Settlement` can only use `approved` BOM items.
- The picker for `Booking`, `Production`, and `Settlement` 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 `Booking`, `Production`, and `Settlement`.
- Do not remove it from `Costing` just because the BOM item is still `draft` or `checking`.

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`
- additional workflow tabs to be refined later

`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 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.

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

### 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`.
- `Preparing workspace` startup overlay now stays modal until all allowed startup preload modules finish loading.
- Startup preload timeout no longer hides the workspace overlay while data is still loading.

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