EMailed label return generation (alpha)

This commit is contained in:
2026-09-02 09:30:19 -05:00
parent 6655b02f4a
commit 826a70bdf6
13 changed files with 603 additions and 5 deletions
+4
View File
@@ -27,6 +27,7 @@ from PyQt6.QtWidgets import (
)
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.tracking import get_fulfilled_statuses
@@ -42,6 +43,7 @@ COLUMNS = [
("status", "Status"),
("skus_display", "SKUs"),
("summary", "Summary"),
("return_label_display", "Return Label"),
("outgoing_tracking_display", "Outgoing Tracking"),
("return_tracking_display", "Return Tracking"),
("uploaded_display", "Uploaded"),
@@ -122,6 +124,8 @@ class OrdersTableModel(QAbstractTableModel):
if field_name == "skus_display":
return ", ".join(order.skus or [])
if field_name == "return_label_display":
return CHECK_MARK if is_emailed_label_order(order.skus or []) else ""
if field_name == "outgoing_tracking_display":
return _format_tracking_numbers(order.tracking_numbers, is_return=False)
if field_name == "return_tracking_display":
+138
View File
@@ -0,0 +1,138 @@
"""
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.
After a successful create, the app does NOT email anything - per the
team's workflow, that happens from ShipStation itself so it goes out
through their branded return-email template.
"""
from __future__ import annotations
from typing import List
from PyQt6.QtWidgets import (
QDialog,
QVBoxLayout,
QHBoxLayout,
QFormLayout,
QLabel,
QTextEdit,
QTableWidget,
QDoubleSpinBox,
QPushButton,
QComboBox,
QDialogButtonBox,
)
from app.models import Order
CHARGE_EVENT_LABELS = {
"carrier_default": "Carrier default",
"on_creation": "On label creation",
"on_carrier_acceptance": "On carrier acceptance (label may go unused free)",
}
COLUMNS = ["Weight (oz)", "Length (in)", "Width (in)", "Height (in)"]
DEFAULT_WEIGHT_OZ = 1.0
DEFAULT_DIMENSION_IN = 1.0
class ReturnLabelDialog(QDialog):
def __init__(self, order: Order, parent=None):
super().__init__(parent)
self.order = order
self.setWindowTitle(f"Create Return Label - {order.ticket_number or order.external_id}")
self.resize(560, 560)
layout = QVBoxLayout(self)
info = order.shipping_info or {}
address_line2 = f" {info.get('address2')}" if info.get("address2") else ""
summary_lines = [
f"Ticket: {order.ticket_number or order.external_id} Company: {order.company}",
f"Customer: {info.get('name') or '(missing)'}",
f"Address: {info.get('address1') or '(missing)'}{address_line2}, "
f"{info.get('city', '')}, {info.get('state', '')} {info.get('zip', '')}",
]
layout.addWidget(QLabel("\n".join(summary_lines)))
layout.addWidget(QLabel("Description (box requirements from JIRA):"))
description_box = QTextEdit()
description_box.setReadOnly(True)
description_box.setPlainText(order.description or "(no description on this ticket)")
description_box.setMaximumHeight(100)
layout.addWidget(description_box)
layout.addWidget(QLabel("Packages - one row per box:"))
self.table = QTableWidget(0, len(COLUMNS))
self.table.setHorizontalHeaderLabels(COLUMNS)
layout.addWidget(self.table)
package_buttons = QHBoxLayout()
add_button = QPushButton("Add Package")
add_button.clicked.connect(self._add_package_row)
remove_button = QPushButton("Remove Selected")
remove_button.clicked.connect(self._remove_selected_row)
package_buttons.addWidget(add_button)
package_buttons.addWidget(remove_button)
package_buttons.addStretch()
layout.addLayout(package_buttons)
form = QFormLayout()
self.charge_event_combo = QComboBox()
for value, label in CHARGE_EVENT_LABELS.items():
self.charge_event_combo.addItem(label, userData=value)
form.addRow("Charge event:", self.charge_event_combo)
layout.addLayout(form)
button_box = QDialogButtonBox()
self.create_button = button_box.addButton(
"Create Return Label", QDialogButtonBox.ButtonRole.AcceptRole
)
button_box.addButton(QDialogButtonBox.StandardButton.Cancel)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
layout.addWidget(button_box)
# Start with one package pre-filled at your current standard
# (1x1x1, 1oz) - fully editable, just a familiar starting point.
self._add_package_row()
def _add_package_row(self) -> None:
row = self.table.rowCount()
self.table.insertRow(row)
defaults = [DEFAULT_WEIGHT_OZ, DEFAULT_DIMENSION_IN, DEFAULT_DIMENSION_IN, DEFAULT_DIMENSION_IN]
for col, default_value in enumerate(defaults):
spin = QDoubleSpinBox()
spin.setRange(0, 9999)
spin.setDecimals(2)
spin.setValue(default_value)
self.table.setCellWidget(row, col, spin)
def _remove_selected_row(self) -> None:
rows = {index.row() for index in self.table.selectedIndexes()}
for row in sorted(rows, reverse=True):
self.table.removeRow(row)
def get_packages(self) -> List[dict]:
packages = []
for row in range(self.table.rowCount()):
weight_spin = self.table.cellWidget(row, 0)
length_spin = self.table.cellWidget(row, 1)
width_spin = self.table.cellWidget(row, 2)
height_spin = self.table.cellWidget(row, 3)
packages.append(
{
"weight_oz": weight_spin.value(),
"length": length_spin.value(),
"width": width_spin.value(),
"height": height_spin.value(),
}
)
return packages
def get_charge_event(self) -> str:
return self.charge_event_combo.currentData()