Rework of workflow, added the dock back. This is now the beginning of the SKU mismatch issues. FKN FUN TIMES AHEAD

This commit is contained in:
2026-09-04 12:09:49 -05:00
parent a1b438a61f
commit 52bd6f5da2
8 changed files with 395 additions and 68 deletions
+111 -42
View File
@@ -12,6 +12,8 @@ here is purely a label-generation step for JIRA tickets.
"""
from __future__ import annotations
import webbrowser
from PyQt6.QtGui import QAction
from PyQt6.QtWidgets import (
QMainWindow,
@@ -26,6 +28,7 @@ from PyQt6.QtWidgets import (
QDialog,
)
from app.external_links import jira_ticket_url, google_maps_search_url
from app.return_labels import is_emailed_label_order
from app.services import SERVICE_REGISTRY
from app.services.odoo_export import export_orders_to_csv
@@ -91,62 +94,92 @@ class MainWindow(QMainWindow):
self.setCentralWidget(central)
toolbar = QToolBar("Main")
toolbar.setMovable(False)
self.addToolBar(toolbar)
menu_bar = self.menuBar()
file_menu = menu_bar.addMenu("&File")
settings_action = QAction("Settings...", self)
settings_action.triggered.connect(self._on_settings_clicked)
file_menu.addAction(settings_action)
file_menu.addSeparator()
exit_action = QAction("Exit", self)
exit_action.triggered.connect(self.close)
file_menu.addAction(exit_action)
data_menu = menu_bar.addMenu("&Data")
jira_action = QAction("Import from JIRA", self)
jira_action.triggered.connect(lambda: self._on_import_clicked("jira"))
toolbar.addAction(jira_action)
data_menu.addAction(jira_action)
self._import_actions["jira"] = jira_action
shipstation_action = QAction("Pull Tracking Numbers (ShipStation)", self)
shipstation_action.triggered.connect(lambda: self._on_import_clicked("shipstation"))
toolbar.addAction(shipstation_action)
data_menu.addAction(shipstation_action)
self._import_actions["shipstation"] = shipstation_action
toolbar.addSeparator()
export_action = QAction("Export Visible Orders to Odoo CSV", self)
export_action.setToolTip("Exports from whichever tab is currently open")
export_action.triggered.connect(self._on_export_clicked)
toolbar.addAction(export_action)
self._pack_ticket_action = QAction("Pack Ticket", self)
self._pack_ticket_action.setToolTip(
"Enter serial numbers and mark packed - select a ticket first"
)
self._pack_ticket_action.triggered.connect(self._on_pack_ticket_clicked)
toolbar.addAction(self._pack_ticket_action)
toolbar.addSeparator()
emergency_action = QAction("Send to ShipStation", self)
emergency_action.setToolTip("Select a ticket in Active Orders first")
emergency_action.triggered.connect(self._on_emergency_send_clicked)
toolbar.addAction(emergency_action)
self._return_label_action = QAction("Create Return Label", self)
self._return_label_action.setToolTip(
"For emailed-return-label tickets (SH007/OK012) - select one in Active Orders first"
)
self._return_label_action.triggered.connect(self._on_create_return_label_clicked)
toolbar.addAction(self._return_label_action)
toolbar.addSeparator()
settings_action = QAction("Settings", self)
settings_action.triggered.connect(self._on_settings_clicked)
toolbar.addAction(settings_action)
data_menu.addSeparator()
refresh_action = QAction("Refresh from Local DB", self)
refresh_action.triggered.connect(self._refresh_everything)
toolbar.addAction(refresh_action)
data_menu.addAction(refresh_action)
reset_action = QAction("Reset Local Database", self)
reset_action.setToolTip("Wipes the local cache - re-import from JIRA afterward")
export_action = QAction("Export Visible Orders to Odoo CSV...", self)
export_action.setStatusTip("Exports from whichever tab is currently open")
export_action.triggered.connect(self._on_export_clicked)
data_menu.addAction(export_action)
data_menu.addSeparator()
reset_action = QAction("Reset Local Database...", self)
reset_action.setStatusTip("Wipes the local cache - re-import from JIRA afterward")
reset_action.triggered.connect(self._on_reset_database_clicked)
toolbar.addAction(reset_action)
data_menu.addAction(reset_action)
orders_menu = menu_bar.addMenu("&Orders")
self._pack_ticket_action = QAction("Pack Ticket", self)
self._pack_ticket_action.setStatusTip(
"Enter serial numbers and mark packed - select a ticket first"
)
self._pack_ticket_action.triggered.connect(self._on_pack_ticket_clicked)
orders_menu.addAction(self._pack_ticket_action)
emergency_action = QAction("Send to ShipStation", self)
emergency_action.setStatusTip("Select a ticket in Active Orders first")
emergency_action.triggered.connect(self._on_emergency_send_clicked)
orders_menu.addAction(emergency_action)
self._return_label_action = QAction("Create Return Label", self)
self._return_label_action.setStatusTip(
"For emailed-return-label tickets (SH007/OK012) - select one in Active Orders first"
)
self._return_label_action.triggered.connect(self._on_create_return_label_clicked)
orders_menu.addAction(self._return_label_action)
orders_menu.addSeparator()
view_jira_action = QAction("View in JIRA", self)
view_jira_action.setStatusTip("Opens the selected ticket in JIRA")
view_jira_action.triggered.connect(self._on_view_in_jira_clicked)
orders_menu.addAction(view_jira_action)
lookup_address_action = QAction("Look Up Address on Google Maps", self)
lookup_address_action.setStatusTip(
"Opens the selected ticket's shipping address in Google Maps"
)
lookup_address_action.triggered.connect(self._on_lookup_address_clicked)
orders_menu.addAction(lookup_address_action)
# Quick-access toolbar for the highest-frequency actions - everything
# here is also in the menus above (same QAction objects, so there's
# nothing to keep in sync); this is just a shortcut for the few used
# constantly enough to want one click instead of a menu dropdown.
quick_toolbar = QToolBar("Quick Actions")
quick_toolbar.setMovable(False)
self.addToolBar(quick_toolbar)
quick_toolbar.addAction(self._import_actions["jira"])
quick_toolbar.addAction(self._pack_ticket_action)
quick_toolbar.addAction(self._return_label_action)
self.status_bar = QStatusBar()
self.setStatusBar(self.status_bar)
@@ -481,6 +514,42 @@ class MainWindow(QMainWindow):
self._refresh_everything()
self.status_label.setText(f"Saved pack data for {ticket_number}.")
def _currently_selected_order(self):
"""Unlike orders_table.selected_order() (Active Orders specifically),
this checks whichever tab is actually showing - View in JIRA and
Look Up Address are read-only lookups that make just as much sense
for a Cancelled or Done ticket as an Active one."""
current_widget = self.tabs.currentWidget()
if isinstance(current_widget, OrdersTableView):
return current_widget.selected_order()
return None
def _on_view_in_jira_clicked(self) -> None:
order = self._currently_selected_order()
if order is None:
QMessageBox.information(self, "No ticket selected", "Select a ticket first.")
return
url = jira_ticket_url(order.ticket_number)
if url is None:
QMessageBox.warning(
self,
"JIRA URL not set",
"Add your JIRA URL in Settings first (or this ticket has no ticket number).",
)
return
webbrowser.open(url)
def _on_lookup_address_clicked(self) -> None:
order = self._currently_selected_order()
if order is None:
QMessageBox.information(self, "No ticket selected", "Select a ticket first.")
return
url = google_maps_search_url(order.shipping_info)
if url is None:
QMessageBox.information(self, "No address", "This ticket has no address on file.")
return
webbrowser.open(url)
def _on_reset_database_clicked(self) -> None:
confirm = QMessageBox.warning(
self,
+39 -10
View File
@@ -4,15 +4,20 @@ Order detail dialog.
Mainly a debugging aid: shows exactly what JIRA sent back for this
ticket (raw payload), plus the tracking numbers ShipStation supplied
and the JIRA status they suggest - handy for the end-of-day close-out
without having to piece it together by hand.
without having to piece it together by hand. Also offers quick jumps
out to JIRA and Google Maps, since those are common next steps when
something about a ticket needs a closer look.
"""
from __future__ import annotations
import json
import webbrowser
from PyQt6.QtWidgets import QDialog, QVBoxLayout, QTextEdit, QLabel, QPushButton
from PyQt6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QTextEdit, QLabel, QPushButton
from app.external_links import jira_ticket_url, google_maps_search_url
from app.models import Order
from app.ticket_validation import validate_ticket
from app.tracking import suggest_jira_status
@@ -38,21 +43,31 @@ class OrderDetailDialog(QDialog):
info = order.shipping_info or {}
address_line2 = f" {info.get('address2')}" if info.get("address2") else ""
issues = validate_ticket(order.skus, order.line_items)
summary_lines = [
f"Ticket #: {order.ticket_number or '(none found)'}",
f"Company: {order.company}",
f"Created by: {order.creator or '(unknown)'}",
f"SKUs: {', '.join(order.skus or []) or '(none extracted)'}",
f"Status: {order.status}",
"",
"Shipping info (used by the emergency Send to ShipStation action):",
f" Name: {info.get('name') or '(missing)'}",
f" Address: {info.get('address1') or '(missing)'}{address_line2}",
f" {info.get('city', '')}, {info.get('state', '')} {info.get('zip', '')}".rstrip(),
f" Phone: {info.get('phone') or '(none)'} Email: {info.get('email') or '(none)'}",
"",
"Tracking numbers:",
]
if issues:
summary_lines.append("")
summary_lines.append("\u26a0 Flagged for review:")
summary_lines.extend(f" - {issue.message}" for issue in issues)
summary_lines.extend(
[
"",
"Shipping info (used by the emergency Send to ShipStation action):",
f" Name: {info.get('name') or '(missing)'}",
f" Address: {info.get('address1') or '(missing)'}{address_line2}",
f" {info.get('city', '')}, {info.get('state', '')} {info.get('zip', '')}".rstrip(),
f" Phone: {info.get('phone') or '(none)'} Email: {info.get('email') or '(none)'}",
"",
"Tracking numbers:",
]
)
summary_lines.extend(tracking_lines or [" (none pulled yet)"])
if suggested:
summary_lines.append("")
@@ -61,6 +76,16 @@ class OrderDetailDialog(QDialog):
summary_label = QLabel("\n".join(summary_lines))
layout.addWidget(summary_label)
link_buttons = QHBoxLayout()
jira_button = QPushButton("View in JIRA")
jira_button.clicked.connect(lambda: self._open_url(jira_ticket_url(order.ticket_number)))
link_buttons.addWidget(jira_button)
maps_button = QPushButton("Look Up Address on Google Maps")
maps_button.clicked.connect(lambda: self._open_url(google_maps_search_url(order.shipping_info)))
link_buttons.addWidget(maps_button)
link_buttons.addStretch()
layout.addLayout(link_buttons)
layout.addWidget(QLabel("Raw payload from source system:"))
text = QTextEdit()
text.setReadOnly(True)
@@ -71,3 +96,7 @@ class OrderDetailDialog(QDialog):
close_button = QPushButton("Close")
close_button.clicked.connect(self.accept)
layout.addWidget(close_button)
def _open_url(self, url: str | None) -> None:
if url:
webbrowser.open(url)
+17 -3
View File
@@ -30,17 +30,21 @@ from app.models import Order
from app.return_labels import is_emailed_label_order
from app.schedule import get_cutoff_time, is_past_cutoff_today
from app.status_rules import get_cancelled_statuses, status_in
from app.ticket_validation import validate_ticket
from app.tracking import get_fulfilled_statuses
CANCELLED_TEXT_COLOR = QColor(180, 0, 0)
FULFILLED_ROW_COLOR = QColor(210, 240, 210)
ISSUE_TEXT_COLOR = QColor(170, 100, 0)
CHECK_MARK = "\u2713"
WARNING_MARK = "\u26a0"
# Order here is display order, left to right.
COLUMNS = [
("company", "Company"),
("ticket_number", "Ticket #"),
("status", "Status"),
("issues_display", "Issues"),
("packed_display", "Done"),
("skus_display", "SKUs"),
("summary", "Summary"),
@@ -108,9 +112,14 @@ class OrdersTableModel(QAbstractTableModel):
order = self._orders[index.row()]
is_cancelled = status_in(order.status, self._cancelled_statuses)
field_name, _ = COLUMNS[index.column()]
if role == Qt.ItemDataRole.ForegroundRole:
return CANCELLED_TEXT_COLOR if is_cancelled else None
if is_cancelled:
return CANCELLED_TEXT_COLOR
if field_name == "issues_display" and validate_ticket(order.skus, order.line_items):
return ISSUE_TEXT_COLOR
return None
if role == Qt.ItemDataRole.BackgroundRole:
# Cancelled rows use red TEXT (above) instead of a background,
@@ -121,11 +130,16 @@ class OrdersTableModel(QAbstractTableModel):
return FULFILLED_ROW_COLOR
return None
if role == Qt.ItemDataRole.ToolTipRole and field_name == "issues_display":
issues = validate_ticket(order.skus, order.line_items)
return "\n".join(i.message for i in issues) if issues else None
if role != Qt.ItemDataRole.DisplayRole:
return None
field_name, _ = COLUMNS[index.column()]
if field_name == "issues_display":
issues = validate_ticket(order.skus, order.line_items)
return f"{WARNING_MARK} ({len(issues)})" if issues else ""
if field_name == "skus_display":
return ", ".join(order.skus or [])
if field_name == "packed_display":