# Order Manager A PyQt6 desktop app that independently pulls orders from JIRA and ShipStation, tracks them by company (Signify Health / Oak Street Health) and ticket number, and gives you a dashboard of what's come in today. Odoo integration is file-based for now (CSV export) since the live Odoo side is still in development. ## Setup 1. `pip install -r requirements.txt` 2. Run it once: `python main.py` (this creates `.env` from `.env.example`) 3. Click **Settings** and fill in: - **JIRA**: Site URL, Email, API Token, JQL (defaults to tickets created today - adjust to match your project/label). The deliverable field IDs are already defaulted from your export (`customfield_10573,customfield_10570` for Signify; `customfield_12790,customfield_13021` for Oak Street) - update these if your field IDs ever change. - **ShipStation**: API Key. The store IDs are already defaulted (`se-221889` Signify, `se-367672` Oak Street) - just add your key. - **Companies**: SKU prefix -> company mapping (defaults to `SH:Signify Health,OK:Oak Street Health`) and the ticket number pattern (defaults to `AR-######`-style). 4. Click **Import from JIRA** and/or **Import from ShipStation** - they run independently, each in the background so the UI stays responsive. 5. Check the **Dashboard** tab for totals by company/source and how many tickets are matched across both systems vs. only seen in one. 6. Use **All Orders** to filter by company/source or search by ticket number/SKU, and **Export Visible Orders to Odoo CSV** to hand off whatever's currently filtered. Orders are cached locally in SQLite (`orders.db`). Re-importing updates existing orders rather than duplicating them (matched on source + source ID). ## How company/ticket matching works - **JIRA orders**: each company has its own pair of "deliverable" custom fields in JIRA (e.g. Signify's `Deliverables` + `Hardware Needed`; Oak Street's own versions). A ticket can list several deliverables (`customfield_10573`, etc. - configurable via `JIRA_SIGNIFY_SKU_FIELDS` / `JIRA_OAKSTREET_SKU_FIELDS`). Each deliverable value looks like `SH011: Shipping - Return Label iPad - Physical in Box` - the app pulls out just the `SH011` code as the SKU and keeps the description for the summary. Company is then resolved from the SKU prefix (`SH`/`OK`) via `COMPANY_SKU_MAP`, same as before. Since these tickets' actual JIRA "Summary" field is usually blank, the app falls back to the joined deliverable descriptions for the Summary column when there's nothing else there. - **ShipStation orders**: company is derived from which store the order lives in (`SHIPSTATION_STORE_MAP`), since each company has its own store/UPS account. - **Ticket number** (`AR-######`) is the JIRA issue key directly on JIRA-sourced orders. On ShipStation-sourced orders, since V2's API doesn't have a dedicated "orders" endpoint with a guaranteed order-number field, the app scans the whole shipment payload for the configured pattern (`TICKET_NUMBER_REGEX`). Once you see what a real ShipStation payload for one of your shipments looks like, this can be tightened to read one specific field for speed/reliability - just point me at where it actually shows up. ## A note on the ShipStation V2 API ShipStation's V2 API doesn't expose a dedicated "list orders" endpoint the way the older V1 API did - it's built around `/v2/shipments`. This app pulls today's shipments from that endpoint and filters client-side to your two configured store IDs. If your account's shipments don't carry an `items`/SKU array the way we expect (this can depend on how orders reach ShipStation), the company will still resolve correctly (from `store_id`, which is reliable) even if the SKU column comes back empty - worth checking against a real imported order early on. ## Moving storage to your MariaDB LXC later In Settings (or directly in `.env`), change: ``` DB_URL=mysql+pymysql://user:password@:3306/order_manager ``` and `pip install pymysql`. No code changes needed. Note: this is a local cache rebuilt from JIRA/ShipStation, so if you change `DB_URL` or the schema changes in a future update, it's safe to just delete `orders.db` and re-import rather than migrate it. ## Project layout ``` main.py entry point app/ config.py reads/writes .env, defines the settings schema database.py SQLAlchemy engine/session (SQLite now, MariaDB later) models.py Order table - company/ticket_number/skus + shared shape companies.py SKU-prefix and store-ID -> company resolution workers.py background thread(s) for API calls + DB save/load/stats services/ base.py OrderService interface - implement this for new sources jira_service.py JIRA REST API -> NormalizedOrder (SKU field, company) shipstation_service.py ShipStation V2 (/v2/shipments) -> NormalizedOrder odoo_export.py CSV export for the (in-development) Odoo import template __init__.py SERVICE_REGISTRY - register new sources here ui/ main_window.py tabs, toolbar (independent import buttons), export settings_dialog.py auto-built from config.SETTINGS_SCHEMA widgets/ dashboard.py summary stat cards orders_table.py sortable/filterable Qt table for orders ``` ## Adding real Odoo API access next 1. Add settings to `app/config.py` -> `SETTINGS_SCHEMA` (Odoo group). 2. Create `app/services/odoo_service.py`. If it's pull-based (Odoo has orders you need to see), subclass `OrderService` like the other two. If it's push-based (you're sending orders to Odoo), it doesn't need to subclass `OrderService` - a `push_orders(orders)` method is fine, called from a new toolbar action the same way `_on_export_clicked` calls `export_orders_to_csv`. 3. Register/wire it up the same way JIRA and ShipStation are. Since everything funnels through the same `Order` table, the table view, dashboard, and settings UI don't need to change as sources are added.