192 lines
7.1 KiB
Python
192 lines
7.1 KiB
Python
"""
|
|
Main window.
|
|
|
|
Deliberately thin: it wires the toolbar, tabs, and dialogs together,
|
|
and delegates real work to app.services (fetching), app.workers
|
|
(saving/loading/stats), and app.services.odoo_export (file export).
|
|
|
|
JIRA and ShipStation are pulled independently (separate buttons,
|
|
separate workers) per how the business actually operates - they are
|
|
correlated afterwards for the dashboard via ticket_number, not forced
|
|
into one pipeline.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from PyQt6.QtGui import QAction
|
|
from PyQt6.QtWidgets import (
|
|
QMainWindow,
|
|
QWidget,
|
|
QVBoxLayout,
|
|
QToolBar,
|
|
QStatusBar,
|
|
QMessageBox,
|
|
QLabel,
|
|
QTabWidget,
|
|
QFileDialog,
|
|
)
|
|
|
|
from app.services import SERVICE_REGISTRY
|
|
from app.services.odoo_export import export_orders_to_csv
|
|
from app.ui.settings_dialog import SettingsDialog
|
|
from app.ui.widgets.orders_table import OrdersTableView
|
|
from app.ui.widgets.dashboard import DashboardWidget
|
|
from app.ui.widgets.order_detail_dialog import OrderDetailDialog
|
|
from app.workers import FetchOrdersWorker, load_all_orders, get_dashboard_stats
|
|
|
|
|
|
class MainWindow(QMainWindow):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.setWindowTitle("Order Manager")
|
|
self.resize(1150, 700)
|
|
|
|
self._workers: dict[str, FetchOrdersWorker] = {}
|
|
self._import_actions: dict[str, QAction] = {}
|
|
|
|
self._build_ui()
|
|
self._refresh_everything()
|
|
|
|
# -- UI construction -------------------------------------------------
|
|
|
|
def _build_ui(self) -> None:
|
|
central = QWidget()
|
|
layout = QVBoxLayout(central)
|
|
|
|
self.tabs = QTabWidget()
|
|
self.dashboard = DashboardWidget()
|
|
self.orders_table = OrdersTableView()
|
|
self.orders_table.order_double_clicked.connect(self._on_order_double_clicked)
|
|
self.tabs.addTab(self.dashboard, "Dashboard")
|
|
self.tabs.addTab(self.orders_table, "All Orders")
|
|
layout.addWidget(self.tabs)
|
|
|
|
self.setCentralWidget(central)
|
|
|
|
toolbar = QToolBar("Main")
|
|
toolbar.setMovable(False)
|
|
self.addToolBar(toolbar)
|
|
|
|
jira_action = QAction("Import from JIRA", self)
|
|
jira_action.triggered.connect(lambda: self._on_import_clicked("jira"))
|
|
toolbar.addAction(jira_action)
|
|
self._import_actions["jira"] = jira_action
|
|
|
|
shipstation_action = QAction("Import from ShipStation", self)
|
|
shipstation_action.triggered.connect(lambda: self._on_import_clicked("shipstation"))
|
|
toolbar.addAction(shipstation_action)
|
|
self._import_actions["shipstation"] = shipstation_action
|
|
|
|
toolbar.addSeparator()
|
|
|
|
export_action = QAction("Export Visible Orders to Odoo CSV", self)
|
|
export_action.triggered.connect(self._on_export_clicked)
|
|
toolbar.addAction(export_action)
|
|
|
|
toolbar.addSeparator()
|
|
|
|
settings_action = QAction("Settings", self)
|
|
settings_action.triggered.connect(self._on_settings_clicked)
|
|
toolbar.addAction(settings_action)
|
|
|
|
refresh_action = QAction("Refresh from Local DB", self)
|
|
refresh_action.triggered.connect(self._refresh_everything)
|
|
toolbar.addAction(refresh_action)
|
|
|
|
self.status_bar = QStatusBar()
|
|
self.setStatusBar(self.status_bar)
|
|
self.status_label = QLabel("Ready.")
|
|
self.status_bar.addWidget(self.status_label)
|
|
|
|
# -- actions -----------------------------------------------------------
|
|
|
|
def _on_settings_clicked(self) -> None:
|
|
dialog = SettingsDialog(self)
|
|
dialog.exec()
|
|
|
|
def _on_order_double_clicked(self, order) -> None:
|
|
dialog = OrderDetailDialog(order, self)
|
|
dialog.exec()
|
|
|
|
def _on_import_clicked(self, service_name: str) -> None:
|
|
service_cls = SERVICE_REGISTRY[service_name]
|
|
service = service_cls()
|
|
|
|
if not service.is_configured():
|
|
QMessageBox.warning(
|
|
self,
|
|
f"{service_name.title()} not configured",
|
|
f"Fill in the {service_name.title()} settings first, then try again.",
|
|
)
|
|
return
|
|
|
|
action = self._import_actions[service_name]
|
|
action.setEnabled(False)
|
|
self.status_label.setText(f"Importing orders from {service_name.title()}...")
|
|
|
|
worker = FetchOrdersWorker(service)
|
|
worker.finished_ok.connect(
|
|
lambda new_count, updated_count: self._on_import_finished(
|
|
service_name, new_count, updated_count
|
|
)
|
|
)
|
|
worker.failed.connect(lambda message: self._on_import_failed(service_name, message))
|
|
self._workers[service_name] = worker # keep a reference so it isn't garbage collected
|
|
worker.start()
|
|
|
|
def _on_import_finished(self, service_name: str, new_count: int, updated_count: int) -> None:
|
|
self._import_actions[service_name].setEnabled(True)
|
|
self.status_label.setText(
|
|
f"{service_name.title()} import complete: {new_count} new, {updated_count} updated."
|
|
)
|
|
self._refresh_everything()
|
|
|
|
def _on_import_failed(self, service_name: str, message: str) -> None:
|
|
self._import_actions[service_name].setEnabled(True)
|
|
self.status_label.setText(f"{service_name.title()} import failed.")
|
|
QMessageBox.critical(self, f"{service_name.title()} import failed", message)
|
|
|
|
def _on_export_clicked(self) -> None:
|
|
orders = self.orders_table.visible_orders()
|
|
if not orders:
|
|
QMessageBox.information(
|
|
self, "Nothing to export", "No orders match the current filters."
|
|
)
|
|
return
|
|
|
|
filepath, _ = QFileDialog.getSaveFileName(
|
|
self, "Export to Odoo CSV", "odoo_orders_export.csv", "CSV Files (*.csv)"
|
|
)
|
|
if not filepath:
|
|
return
|
|
|
|
try:
|
|
count = export_orders_to_csv(orders, filepath)
|
|
except OSError as exc:
|
|
QMessageBox.critical(self, "Export failed", str(exc))
|
|
return
|
|
|
|
self.status_label.setText(f"Exported {count} order(s) to {filepath}")
|
|
|
|
def _refresh_everything(self) -> None:
|
|
orders = load_all_orders()
|
|
self.orders_table.set_orders(orders)
|
|
self.dashboard.update_stats(get_dashboard_stats())
|
|
if not hasattr(self, "_suppress_ready_status"):
|
|
self.status_label.setText(f"{len(orders)} order(s) in local database.")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Future growth notes:
|
|
#
|
|
# - Odoo push API (once ready): add app/services/odoo_service.py with a
|
|
# push_orders(orders) method, wire a new toolbar action similarly to
|
|
# _on_import_clicked, and it can eventually replace/augment odoo_export.py.
|
|
# - Order detail view: connect a table double-click to a dialog showing
|
|
# selected_order().raw_data (full JIRA/ShipStation payload) for debugging
|
|
# ticket-number/SKU extraction as real data comes in.
|
|
# - Pipeline actions ("mark fulfilled", "create ShipStation label"): add
|
|
# toolbar actions gated on table selection.
|
|
# - If the window grows too much, split each tab's toolbar into its own
|
|
# QToolBar shown only while that tab is active.
|
|
# ---------------------------------------------------------------------------
|