More email SKU shenanigans, now with more TEST labels!

This commit is contained in:
2026-09-14 11:41:27 -05:00
parent a6f60408ff
commit b5570d39f0
12 changed files with 815 additions and 139 deletions
+39 -6
View File
@@ -27,10 +27,10 @@ from PyQt6.QtWidgets import (
)
from app.models import Order
from app.return_labels import is_emailed_label_order
from app.return_labels import is_emailed_label_order, get_emailed_label_skus
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.ticket_validation import validate_ticket, make_validation_context
from app.tracking import get_fulfilled_statuses
CANCELLED_TEXT_COLOR = QColor(180, 0, 0)
@@ -83,12 +83,31 @@ class OrdersTableModel(QAbstractTableModel):
self._cancelled_statuses: set[str] = set()
self._fulfilled_statuses: set[str] = set()
self._cutoff_time = None
self._issues_by_id: dict[int, list] = {}
self._is_emailed_label_by_id: dict[int, bool] = {}
def set_orders(self, orders: List[Order]) -> None:
# Re-read status lists and cutoff time each refresh, in case Settings changed.
self._cancelled_statuses = get_cancelled_statuses()
self._fulfilled_statuses = get_fulfilled_statuses()
self._cutoff_time = get_cutoff_time()
# Computed ONCE per refresh here, not per cell paint - both of
# these read Settings from disk internally, and Qt calls data()
# extremely frequently (every cell, every repaint, constantly
# during scrolling) - doing this per-cell instead of per-refresh
# was a severe, real performance bug, not just a minor slowdown.
# validation_context is ALSO fetched once here rather than once
# per order inside validate_ticket() - same fix, applied to the
# cost of computing the cache itself, not just using it.
validation_context = make_validation_context()
self._issues_by_id = {
order.id: validate_ticket(order.skus, order.line_items, **validation_context)
for order in orders
}
emailed_skus = get_emailed_label_skus()
self._is_emailed_label_by_id = {
order.id: is_emailed_label_order(order.skus or [], emailed_skus) for order in orders
}
self.beginResetModel()
self._orders = orders
self.endResetModel()
@@ -117,7 +136,7 @@ class OrdersTableModel(QAbstractTableModel):
if role == Qt.ItemDataRole.ForegroundRole:
if is_cancelled:
return CANCELLED_TEXT_COLOR
if field_name == "issues_display" and validate_ticket(order.skus, order.line_items):
if field_name == "issues_display" and self._issues_by_id.get(order.id):
return ISSUE_TEXT_COLOR
return None
@@ -131,14 +150,14 @@ class OrdersTableModel(QAbstractTableModel):
return None
if role == Qt.ItemDataRole.ToolTipRole and field_name == "issues_display":
issues = validate_ticket(order.skus, order.line_items)
issues = self._issues_by_id.get(order.id, [])
return "\n".join(i.message for i in issues) if issues else None
if role != Qt.ItemDataRole.DisplayRole:
return None
if field_name == "issues_display":
issues = validate_ticket(order.skus, order.line_items)
issues = self._issues_by_id.get(order.id, [])
return f"{WARNING_MARK} ({len(issues)})" if issues else ""
if field_name == "skus_display":
return ", ".join(order.skus or [])
@@ -149,7 +168,7 @@ class OrdersTableModel(QAbstractTableModel):
filled = sum(1 for v in serials.values() if (v or "").strip())
return f"{filled} entered" if filled else ""
if field_name == "return_label_display":
return CHECK_MARK if is_emailed_label_order(order.skus or []) else ""
return CHECK_MARK if self._is_emailed_label_by_id.get(order.id) else ""
if field_name == "outgoing_tracking_display":
return _format_tracking_numbers(order.tracking_numbers, is_return=False)
if field_name == "return_tracking_display":
@@ -281,6 +300,20 @@ class OrdersTableView(QWidget):
source_index = self._proxy_model.mapToSource(indexes[0])
return self._source_model.order_at(source_index.row())
def select_ticket(self, ticket_number: str) -> bool:
"""Re-selects a row by ticket number - a table refresh (set_orders)
clears whatever was selected, since the underlying model resets.
Used after a step in a multi-step action (like the return-label
dummy-then-return flow) so the next step doesn't silently find
nothing selected. Returns whether the ticket was found/selected."""
for row in range(self._proxy_model.rowCount()):
proxy_index = self._proxy_model.index(row, 0)
source_index = self._proxy_model.mapToSource(proxy_index)
if self._source_model.order_at(source_index.row()).ticket_number == ticket_number:
self.table.selectRow(proxy_index.row())
return True
return False
def visible_orders(self) -> List[Order]:
"""Orders currently passing the active filters - used for export."""
result = []