134 lines
4.6 KiB
Python
134 lines
4.6 KiB
Python
"""
|
|
Background work that shouldn't run on the GUI thread.
|
|
|
|
FetchOrdersWorker runs a service's fetch_orders() call off the main
|
|
thread and reports back via signals. As we add more long-running
|
|
operations (creating ShipStation labels, pushing to Odoo, etc.) they
|
|
should follow this same pattern rather than blocking the UI.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import List
|
|
|
|
from PyQt6.QtCore import QThread, pyqtSignal
|
|
from sqlalchemy import select
|
|
|
|
from app.database import get_session
|
|
from app.models import Order
|
|
from app.services.base import OrderService, NormalizedOrder
|
|
|
|
|
|
class FetchOrdersWorker(QThread):
|
|
"""Fetches orders from a given service and saves new/updated ones to the DB."""
|
|
|
|
finished_ok = pyqtSignal(int, int) # (new_count, updated_count)
|
|
failed = pyqtSignal(str)
|
|
|
|
def __init__(self, service: OrderService, parent=None):
|
|
super().__init__(parent)
|
|
self.service = service
|
|
|
|
def run(self) -> None:
|
|
try:
|
|
orders = self.service.fetch_orders()
|
|
except Exception as exc: # noqa: BLE001 - surface any failure to the UI
|
|
self.failed.emit(str(exc))
|
|
return
|
|
|
|
try:
|
|
new_count, updated_count = save_orders(orders)
|
|
except Exception as exc: # noqa: BLE001
|
|
self.failed.emit(f"Fetched {len(orders)} orders but failed to save them: {exc}")
|
|
return
|
|
|
|
self.finished_ok.emit(new_count, updated_count)
|
|
|
|
|
|
def save_orders(orders: List[NormalizedOrder]) -> tuple[int, int]:
|
|
"""Insert new orders / update existing ones (matched by source + external_id)."""
|
|
session = get_session()
|
|
new_count = 0
|
|
updated_count = 0
|
|
try:
|
|
for order in orders:
|
|
existing = session.execute(
|
|
select(Order).where(
|
|
Order.source == order["source"],
|
|
Order.external_id == order["external_id"],
|
|
)
|
|
).scalar_one_or_none()
|
|
|
|
if existing is None:
|
|
session.add(
|
|
Order(
|
|
source=order["source"],
|
|
external_id=order["external_id"],
|
|
ticket_number=order.get("ticket_number"),
|
|
company=order.get("company", "Unknown"),
|
|
skus=order.get("skus", []),
|
|
summary=order["summary"],
|
|
status=order["status"],
|
|
source_created_at=order["source_created_at"],
|
|
raw_data=order["raw_data"],
|
|
)
|
|
)
|
|
new_count += 1
|
|
else:
|
|
existing.ticket_number = order.get("ticket_number")
|
|
existing.company = order.get("company", "Unknown")
|
|
existing.skus = order.get("skus", [])
|
|
existing.summary = order["summary"]
|
|
existing.status = order["status"]
|
|
existing.source_created_at = order["source_created_at"]
|
|
existing.raw_data = order["raw_data"]
|
|
updated_count += 1
|
|
|
|
session.commit()
|
|
finally:
|
|
session.close()
|
|
|
|
return new_count, updated_count
|
|
|
|
|
|
def load_all_orders() -> List[Order]:
|
|
session = get_session()
|
|
try:
|
|
return list(
|
|
session.execute(select(Order).order_by(Order.source_created_at.desc())).scalars()
|
|
)
|
|
finally:
|
|
session.close()
|
|
|
|
|
|
def get_dashboard_stats() -> dict:
|
|
"""
|
|
Counts for the dashboard tab: totals by company and by source, plus
|
|
how many orders are only in one source so far (imported from JIRA
|
|
but not yet seen in ShipStation, or vice versa) - useful as an
|
|
at-a-glance "what's still missing" signal since the two sources are
|
|
pulled independently.
|
|
"""
|
|
orders = load_all_orders()
|
|
|
|
by_company: dict[str, int] = {}
|
|
by_source: dict[str, int] = {}
|
|
tickets_by_source: dict[str, set] = {}
|
|
|
|
for order in orders:
|
|
by_company[order.company] = by_company.get(order.company, 0) + 1
|
|
by_source[order.source] = by_source.get(order.source, 0) + 1
|
|
if order.ticket_number:
|
|
tickets_by_source.setdefault(order.source, set()).add(order.ticket_number)
|
|
|
|
jira_tickets = tickets_by_source.get("jira", set())
|
|
shipstation_tickets = tickets_by_source.get("shipstation", set())
|
|
|
|
return {
|
|
"total": len(orders),
|
|
"by_company": by_company,
|
|
"by_source": by_source,
|
|
"jira_only_count": len(jira_tickets - shipstation_tickets),
|
|
"shipstation_only_count": len(shipstation_tickets - jira_tickets),
|
|
"matched_count": len(jira_tickets & shipstation_tickets),
|
|
}
|