Email procedure (beta), WorkFlow update
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
"""
|
||||
Pack Ticket dialog - where staff enter serial numbers (mostly via
|
||||
barcode scanner) and mark a ticket packed/ready to ship.
|
||||
|
||||
Barcode scanners act as a keyboard: they type the scanned value and
|
||||
then send an Enter keystroke automatically. So every field here
|
||||
connects its Enter/returnPressed signal to jump focus to the next
|
||||
field - staff scan device after device without touching the mouse or
|
||||
keyboard in between. This is the actual point of this dialog; get this
|
||||
wrong and it defeats the "minimal interactions" requirement entirely.
|
||||
|
||||
Suggested fields come from app.serial_suggestions, based on keywords in
|
||||
the ticket's line items - a starting point, not a fixed schema. Staff
|
||||
can add any custom field the suggestions miss.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from PyQt6.QtWidgets import (
|
||||
QDialog,
|
||||
QVBoxLayout,
|
||||
QHBoxLayout,
|
||||
QFormLayout,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QPushButton,
|
||||
QCheckBox,
|
||||
QScrollArea,
|
||||
QWidget,
|
||||
QDialogButtonBox,
|
||||
)
|
||||
|
||||
from app.models import Order
|
||||
from app.serial_suggestions import suggest_serial_fields
|
||||
|
||||
|
||||
class PackTicketDialog(QDialog):
|
||||
def __init__(self, order: Order, parent=None):
|
||||
super().__init__(parent)
|
||||
self.order = order
|
||||
self.setWindowTitle(f"Pack Ticket - {order.ticket_number or order.external_id}")
|
||||
self.resize(520, 600)
|
||||
|
||||
self._field_rows: list[tuple[QLineEdit, QLineEdit]] = [] # (label_edit, value_edit)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
info = order.shipping_info or {}
|
||||
kit_text = ", ".join(
|
||||
f"{item.get('sku', '')}: {item.get('item_name', '')}" for item in (order.line_items or [])
|
||||
) or ", ".join(order.skus or [])
|
||||
summary_lines = [
|
||||
f"Ticket: {order.ticket_number or order.external_id} Company: {order.company}",
|
||||
f"Customer: {info.get('name') or '(missing)'}",
|
||||
f"Kit: {kit_text or '(none)'}",
|
||||
]
|
||||
layout.addWidget(QLabel("\n".join(summary_lines)))
|
||||
|
||||
layout.addWidget(QLabel("Serial numbers (scan or type; Enter moves to the next field):"))
|
||||
|
||||
scroll_area = QScrollArea()
|
||||
scroll_area.setWidgetResizable(True)
|
||||
self._fields_widget = QWidget()
|
||||
self._fields_layout = QFormLayout(self._fields_widget)
|
||||
scroll_area.setWidget(self._fields_widget)
|
||||
layout.addWidget(scroll_area, stretch=1)
|
||||
|
||||
add_field_row = QHBoxLayout()
|
||||
self.new_field_label_input = QLineEdit()
|
||||
self.new_field_label_input.setPlaceholderText("Custom field name...")
|
||||
add_button = QPushButton("Add Field")
|
||||
add_button.clicked.connect(self._on_add_custom_field_clicked)
|
||||
self.new_field_label_input.returnPressed.connect(self._on_add_custom_field_clicked)
|
||||
add_field_row.addWidget(self.new_field_label_input, stretch=1)
|
||||
add_field_row.addWidget(add_button)
|
||||
layout.addLayout(add_field_row)
|
||||
|
||||
self.packed_checkbox = QCheckBox("Packed / ready to ship")
|
||||
self.packed_checkbox.setChecked(bool(order.packed))
|
||||
layout.addWidget(self.packed_checkbox)
|
||||
|
||||
button_box = QDialogButtonBox()
|
||||
button_box.addButton("Save", 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)
|
||||
|
||||
self._populate_initial_fields()
|
||||
|
||||
def _populate_initial_fields(self) -> None:
|
||||
existing = dict(self.order.serial_numbers or {})
|
||||
suggested = suggest_serial_fields(self.order.line_items or [])
|
||||
|
||||
# Suggested fields first (in suggestion order), pre-filled with any
|
||||
# already-saved value so re-opening a partially-packed ticket
|
||||
# doesn't lose earlier scans. Then any existing fields that aren't
|
||||
# part of the current suggestion set (e.g. a custom field added
|
||||
# last time, or a suggestion rule that's since changed).
|
||||
added_labels: set[str] = set()
|
||||
for label in suggested:
|
||||
self._add_field_row(label, existing.get(label, ""))
|
||||
added_labels.add(label)
|
||||
for label, value in existing.items():
|
||||
if label not in added_labels:
|
||||
self._add_field_row(label, value)
|
||||
|
||||
if self._field_rows:
|
||||
self._field_rows[0][1].setFocus()
|
||||
|
||||
def _add_field_row(self, label: str, value: str = "") -> None:
|
||||
label_edit = QLineEdit(label)
|
||||
label_edit.setReadOnly(True)
|
||||
label_edit.setStyleSheet("border: none; background: transparent;")
|
||||
value_edit = QLineEdit(value)
|
||||
|
||||
row_index = len(self._field_rows)
|
||||
value_edit.returnPressed.connect(lambda: self._focus_next(row_index))
|
||||
|
||||
self._fields_layout.addRow(label_edit, value_edit)
|
||||
self._field_rows.append((label_edit, value_edit))
|
||||
|
||||
def _focus_next(self, current_index: int) -> None:
|
||||
next_index = current_index + 1
|
||||
if next_index < len(self._field_rows):
|
||||
self._field_rows[next_index][1].setFocus()
|
||||
self._field_rows[next_index][1].selectAll()
|
||||
else:
|
||||
# Last known field - hand off to the checkbox rather than
|
||||
# silently submitting, so finishing still takes one deliberate
|
||||
# action instead of an accidental extra scan closing the dialog.
|
||||
self.packed_checkbox.setFocus()
|
||||
|
||||
def _on_add_custom_field_clicked(self) -> None:
|
||||
label = self.new_field_label_input.text().strip()
|
||||
if not label:
|
||||
return
|
||||
self._add_field_row(label, "")
|
||||
self.new_field_label_input.clear()
|
||||
self._field_rows[-1][1].setFocus()
|
||||
|
||||
def get_serial_numbers(self) -> dict[str, str]:
|
||||
return {
|
||||
label_edit.text(): value_edit.text()
|
||||
for label_edit, value_edit in self._field_rows
|
||||
if value_edit.text().strip()
|
||||
}
|
||||
|
||||
def get_packed(self) -> bool:
|
||||
return self.packed_checkbox.isChecked()
|
||||
Reference in New Issue
Block a user