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:
+111
-42
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user