More email SKU shenanigans, now with more TEST labels!
This commit is contained in:
@@ -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 = []
|
||||
|
||||
@@ -2,8 +2,17 @@
|
||||
Return label creation dialog - for the emailed-return-label workflow
|
||||
(SH007 / OK012). Shows the ticket's description (where staff note what
|
||||
boxes are needed) and lets them specify however many packages, each
|
||||
with its own weight/dimensions, replacing the old "always 1x1x1, 1oz"
|
||||
placeholder with real per-request control.
|
||||
with its own weight/dimensions.
|
||||
|
||||
These packages genuinely reach ShipStation now: a standalone return
|
||||
label (no linked outbound shipment) turned out to report success while
|
||||
being invisible in ShipStation's UI - confirmed against the team's own
|
||||
working manual process, not just the API docs. The fix (in
|
||||
shipstation_send.py) creates a minimal, cheap dummy outbound label
|
||||
first and links the real return to it via outbound_label_id, matching
|
||||
what ShipStation's GUI does automatically when creating a return from
|
||||
an existing shipment. The dialog itself doesn't need to know about
|
||||
that - it just collects real packages, same as before.
|
||||
|
||||
WORKAROUND NOTE: this dialog crashed the whole process on close,
|
||||
confirmed via two full crash dumps (identical fault offset both times)
|
||||
@@ -54,6 +63,7 @@ CHARGE_EVENT_LABELS = {
|
||||
"on_carrier_acceptance": "On carrier acceptance (label may go unused free)",
|
||||
}
|
||||
|
||||
DEFAULT_WEIGHT_LB = "0.00"
|
||||
DEFAULT_WEIGHT_OZ = "1.00"
|
||||
DEFAULT_DIMENSION_IN = "1.00"
|
||||
|
||||
@@ -87,6 +97,11 @@ class ReturnLabelDialog(QDialog):
|
||||
self.summary_label = QLabel()
|
||||
layout.addWidget(self.summary_label)
|
||||
|
||||
self.step_status_label = QLabel()
|
||||
self.step_status_label.setWordWrap(True)
|
||||
self.step_status_label.setStyleSheet("font-weight: bold;")
|
||||
layout.addWidget(self.step_status_label)
|
||||
|
||||
layout.addWidget(QLabel("Description (box requirements from JIRA):"))
|
||||
self.description_label = QLabel()
|
||||
self.description_label.setWordWrap(True)
|
||||
@@ -120,7 +135,7 @@ class ReturnLabelDialog(QDialog):
|
||||
|
||||
button_box = QDialogButtonBox()
|
||||
self.create_button = button_box.addButton(
|
||||
"Create Return Label", QDialogButtonBox.ButtonRole.AcceptRole
|
||||
"Step 1: Create Dummy Shipment", QDialogButtonBox.ButtonRole.AcceptRole
|
||||
)
|
||||
button_box.addButton(QDialogButtonBox.StandardButton.Cancel)
|
||||
button_box.accepted.connect(self.accept)
|
||||
@@ -139,7 +154,7 @@ class ReturnLabelDialog(QDialog):
|
||||
"""
|
||||
self.order = order
|
||||
self.setWindowTitle(f"Create Return Label - {order.ticket_number or order.external_id}")
|
||||
self.resize(560, 480)
|
||||
self.resize(560, 520)
|
||||
|
||||
info = order.shipping_info or {}
|
||||
address_line2 = f" {info.get('address2')}" if info.get("address2") else ""
|
||||
@@ -152,6 +167,27 @@ class ReturnLabelDialog(QDialog):
|
||||
self.summary_label.setText("\n".join(summary_lines))
|
||||
self.description_label.setText(order.description or "(no description on this ticket)")
|
||||
|
||||
# This dialog is used for BOTH steps of the workflow - the button
|
||||
# (and what clicking it actually does, wired up in main_window.py)
|
||||
# depends on whether a dummy shipment already exists for this
|
||||
# ticket. Splitting these apart on purpose, per your request: a
|
||||
# problem in the dummy step shouldn't be masked by immediately
|
||||
# attempting the return step too.
|
||||
if order.dummy_outbound_label_id:
|
||||
self.step_status_label.setText(
|
||||
f"Step 1 done - dummy shipment {order.dummy_outbound_label_id} already exists. "
|
||||
"If you've verified it looks right in ShipStation (Order #, Ship From/Store all "
|
||||
"populated), click below to create the actual return label from it."
|
||||
)
|
||||
self.create_button.setText("Step 2: Create Return Label")
|
||||
else:
|
||||
self.step_status_label.setText(
|
||||
"Step 1: this creates a cheap dummy outbound shipment first (1x1x1in, 1oz). "
|
||||
"Go verify it in ShipStation before running this again to create the actual "
|
||||
"return label - that way a problem in either step is easy to isolate."
|
||||
)
|
||||
self.create_button.setText("Step 1: Create Dummy Shipment")
|
||||
|
||||
# Clear out any package rows left over from a previous ticket.
|
||||
# setParent(None) + deleteLater() rather than an immediate delete -
|
||||
# deferred deletion here is deliberate, letting Qt clean these up
|
||||
@@ -169,13 +205,15 @@ class ReturnLabelDialog(QDialog):
|
||||
row_layout = QHBoxLayout(row_widget)
|
||||
row_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
weight_field = _make_number_field(DEFAULT_WEIGHT_OZ)
|
||||
weight_lb_field = _make_number_field(DEFAULT_WEIGHT_LB)
|
||||
weight_oz_field = _make_number_field(DEFAULT_WEIGHT_OZ)
|
||||
length_field = _make_number_field(DEFAULT_DIMENSION_IN)
|
||||
width_field = _make_number_field(DEFAULT_DIMENSION_IN)
|
||||
height_field = _make_number_field(DEFAULT_DIMENSION_IN)
|
||||
|
||||
for label_text, widget in [
|
||||
("Weight (oz):", weight_field),
|
||||
("Weight (lb):", weight_lb_field),
|
||||
("+ (oz):", weight_oz_field),
|
||||
("L (in):", length_field),
|
||||
("W (in):", width_field),
|
||||
("H (in):", height_field),
|
||||
@@ -186,7 +224,8 @@ class ReturnLabelDialog(QDialog):
|
||||
|
||||
entry = {
|
||||
"widget": row_widget,
|
||||
"weight": weight_field,
|
||||
"weight_lb": weight_lb_field,
|
||||
"weight_oz": weight_oz_field,
|
||||
"length": length_field,
|
||||
"width": width_field,
|
||||
"height": height_field,
|
||||
@@ -204,7 +243,10 @@ class ReturnLabelDialog(QDialog):
|
||||
def get_packages(self) -> List[dict]:
|
||||
return [
|
||||
{
|
||||
"weight_oz": _parse_number(entry["weight"].text()),
|
||||
"weight_oz": (
|
||||
_parse_number(entry["weight_lb"].text()) * 16
|
||||
+ _parse_number(entry["weight_oz"].text())
|
||||
),
|
||||
"length": _parse_number(entry["length"].text()),
|
||||
"width": _parse_number(entry["width"].text()),
|
||||
"height": _parse_number(entry["height"].text()),
|
||||
|
||||
Reference in New Issue
Block a user