Initial commit
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user