Files
Order-Manager/README.md
T
2026-08-26 09:15:35 -05:00

125 lines
6.1 KiB
Markdown

# 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@<lxc-ip>: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.