feat: 展示PDD商品和采购订单信息 (#164)
This commit is contained in:
+54
-24
@@ -56,6 +56,18 @@ from qfluentwidgets import (
|
|||||||
|
|
||||||
PAGE_SIZE = 50
|
PAGE_SIZE = 50
|
||||||
CHECK_COLUMN = 0
|
CHECK_COLUMN = 0
|
||||||
|
TASK_TYPE_COLUMN = 1
|
||||||
|
GOODS_ID_COLUMN = 2
|
||||||
|
TITLE_COLUMN = 3
|
||||||
|
SHOP_COLUMN = 4
|
||||||
|
COLOR_COLUMN = 5
|
||||||
|
SIZE_COLUMN = 6
|
||||||
|
PRICE_COLUMN = 7
|
||||||
|
QUANTITY_COLUMN = 8
|
||||||
|
STATUS_COLUMN = 9
|
||||||
|
ORDER_NO_COLUMN = 10
|
||||||
|
DURATION_COLUMN = 11
|
||||||
|
UPDATED_AT_COLUMN = 12
|
||||||
REMOTE_TASK_ID_ROLE = Qt.UserRole + 1
|
REMOTE_TASK_ID_ROLE = Qt.UserRole + 1
|
||||||
|
|
||||||
|
|
||||||
@@ -76,6 +88,7 @@ class TaskRow:
|
|||||||
status: str = "待执行"
|
status: str = "待执行"
|
||||||
latest_run_status: str = ""
|
latest_run_status: str = ""
|
||||||
duration_seconds: Optional[int] = None
|
duration_seconds: Optional[int] = None
|
||||||
|
order_no: str = ""
|
||||||
updated_at: str = ""
|
updated_at: str = ""
|
||||||
|
|
||||||
|
|
||||||
@@ -88,6 +101,7 @@ class TaskTableModel(QAbstractTableModel):
|
|||||||
HEADERS = (
|
HEADERS = (
|
||||||
"选择",
|
"选择",
|
||||||
"任务类型",
|
"任务类型",
|
||||||
|
"PDD商品ID",
|
||||||
"商品标题",
|
"商品标题",
|
||||||
"店铺名",
|
"店铺名",
|
||||||
"颜色",
|
"颜色",
|
||||||
@@ -95,6 +109,7 @@ class TaskTableModel(QAbstractTableModel):
|
|||||||
"价格",
|
"价格",
|
||||||
"数量",
|
"数量",
|
||||||
"状态",
|
"状态",
|
||||||
|
"订单号",
|
||||||
"用时",
|
"用时",
|
||||||
"更新时间",
|
"更新时间",
|
||||||
)
|
)
|
||||||
@@ -138,15 +153,24 @@ class TaskTableModel(QAbstractTableModel):
|
|||||||
return self._display_value(task, column)
|
return self._display_value(task, column)
|
||||||
|
|
||||||
if role == Qt.TextAlignmentRole:
|
if role == Qt.TextAlignmentRole:
|
||||||
if column in (0, 1, 8, 9):
|
if column in (
|
||||||
|
CHECK_COLUMN,
|
||||||
|
TASK_TYPE_COLUMN,
|
||||||
|
STATUS_COLUMN,
|
||||||
|
DURATION_COLUMN,
|
||||||
|
):
|
||||||
return Qt.AlignCenter
|
return Qt.AlignCenter
|
||||||
if column in (6, 7):
|
if column in (PRICE_COLUMN, QUANTITY_COLUMN):
|
||||||
return Qt.AlignRight | Qt.AlignVCenter
|
return Qt.AlignRight | Qt.AlignVCenter
|
||||||
return Qt.AlignLeft | Qt.AlignVCenter
|
return Qt.AlignLeft | Qt.AlignVCenter
|
||||||
|
|
||||||
if role == Qt.ToolTipRole:
|
if role == Qt.ToolTipRole:
|
||||||
if column == 2:
|
if column == GOODS_ID_COLUMN:
|
||||||
|
return task.goods_id or "尚无PDD商品ID"
|
||||||
|
if column == TITLE_COLUMN:
|
||||||
return task.title or "尚未获取标题"
|
return task.title or "尚未获取标题"
|
||||||
|
if column == ORDER_NO_COLUMN:
|
||||||
|
return task.order_no or "尚无订单号"
|
||||||
|
|
||||||
if role == REMOTE_TASK_ID_ROLE:
|
if role == REMOTE_TASK_ID_ROLE:
|
||||||
return task.remote_task_id
|
return task.remote_task_id
|
||||||
@@ -367,32 +391,36 @@ class TaskTableModel(QAbstractTableModel):
|
|||||||
def _display_value(task: TaskRow, column: int) -> str:
|
def _display_value(task: TaskRow, column: int) -> str:
|
||||||
if column == CHECK_COLUMN:
|
if column == CHECK_COLUMN:
|
||||||
return ""
|
return ""
|
||||||
if column == 1:
|
if column == TASK_TYPE_COLUMN:
|
||||||
return task.task_type
|
return task.task_type
|
||||||
if column == 2:
|
if column == GOODS_ID_COLUMN:
|
||||||
|
return task.goods_id or "—"
|
||||||
|
if column == TITLE_COLUMN:
|
||||||
return task.title or "尚未获取标题"
|
return task.title or "尚未获取标题"
|
||||||
if column == 3:
|
if column == SHOP_COLUMN:
|
||||||
return task.shop_name or "—"
|
return task.shop_name or "—"
|
||||||
if column == 4:
|
if column == COLOR_COLUMN:
|
||||||
return task.color or "—"
|
return task.color or "—"
|
||||||
if column == 5:
|
if column == SIZE_COLUMN:
|
||||||
return task.size or "—"
|
return task.size or "—"
|
||||||
if column == 6:
|
if column == PRICE_COLUMN:
|
||||||
if task.price_cents is None:
|
if task.price_cents is None:
|
||||||
return "—"
|
return "—"
|
||||||
suffix = " 起" if task.price_is_starting else ""
|
suffix = " 起" if task.price_is_starting else ""
|
||||||
return f"¥{task.price_cents / 100:.2f}{suffix}"
|
return f"¥{task.price_cents / 100:.2f}{suffix}"
|
||||||
if column == 7:
|
if column == QUANTITY_COLUMN:
|
||||||
return "—" if task.quantity is None else str(task.quantity)
|
return "—" if task.quantity is None else str(task.quantity)
|
||||||
if column == 8:
|
if column == STATUS_COLUMN:
|
||||||
return task.status
|
return task.status
|
||||||
if column == 9:
|
if column == ORDER_NO_COLUMN:
|
||||||
|
return task.order_no or "—"
|
||||||
|
if column == DURATION_COLUMN:
|
||||||
if task.latest_run_status == "running":
|
if task.latest_run_status == "running":
|
||||||
return "进行中"
|
return "进行中"
|
||||||
if task.duration_seconds is None:
|
if task.duration_seconds is None:
|
||||||
return "—"
|
return "—"
|
||||||
return f"{max(0, task.duration_seconds)} 秒"
|
return f"{max(0, task.duration_seconds)} 秒"
|
||||||
if column == 10:
|
if column == UPDATED_AT_COLUMN:
|
||||||
return task.updated_at or "—"
|
return task.updated_at or "—"
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
@@ -575,18 +603,20 @@ class PDDTaskPage(QWidget):
|
|||||||
|
|
||||||
header = self.taskTable.horizontalHeader()
|
header = self.taskTable.horizontalHeader()
|
||||||
header.setSectionResizeMode(QHeaderView.Interactive)
|
header.setSectionResizeMode(QHeaderView.Interactive)
|
||||||
header.setSectionResizeMode(2, QHeaderView.Stretch)
|
header.setSectionResizeMode(TITLE_COLUMN, QHeaderView.Stretch)
|
||||||
header.setMinimumSectionSize(64)
|
header.setMinimumSectionSize(64)
|
||||||
self.taskTable.setColumnWidth(0, 64)
|
self.taskTable.setColumnWidth(CHECK_COLUMN, 64)
|
||||||
self.taskTable.setColumnWidth(1, 82)
|
self.taskTable.setColumnWidth(TASK_TYPE_COLUMN, 82)
|
||||||
self.taskTable.setColumnWidth(3, 108)
|
self.taskTable.setColumnWidth(GOODS_ID_COLUMN, 135)
|
||||||
self.taskTable.setColumnWidth(4, 108)
|
self.taskTable.setColumnWidth(SHOP_COLUMN, 108)
|
||||||
self.taskTable.setColumnWidth(5, 82)
|
self.taskTable.setColumnWidth(COLOR_COLUMN, 108)
|
||||||
self.taskTable.setColumnWidth(6, 96)
|
self.taskTable.setColumnWidth(SIZE_COLUMN, 82)
|
||||||
self.taskTable.setColumnWidth(7, 70)
|
self.taskTable.setColumnWidth(PRICE_COLUMN, 96)
|
||||||
self.taskTable.setColumnWidth(8, 116)
|
self.taskTable.setColumnWidth(QUANTITY_COLUMN, 70)
|
||||||
self.taskTable.setColumnWidth(9, 82)
|
self.taskTable.setColumnWidth(STATUS_COLUMN, 116)
|
||||||
self.taskTable.setColumnWidth(10, 156)
|
self.taskTable.setColumnWidth(ORDER_NO_COLUMN, 190)
|
||||||
|
self.taskTable.setColumnWidth(DURATION_COLUMN, 82)
|
||||||
|
self.taskTable.setColumnWidth(UPDATED_AT_COLUMN, 156)
|
||||||
self.taskTable.viewport().installEventFilter(self)
|
self.taskTable.viewport().installEventFilter(self)
|
||||||
|
|
||||||
self.emptyStateCard = CardWidget(self)
|
self.emptyStateCard = CardWidget(self)
|
||||||
|
|||||||
@@ -1766,5 +1766,6 @@ def summary_to_row(summary: TaskSummary) -> TaskRow:
|
|||||||
summary.latest_run_status.value if summary.latest_run_status else ""
|
summary.latest_run_status.value if summary.latest_run_status else ""
|
||||||
),
|
),
|
||||||
duration_seconds=summary.duration_seconds,
|
duration_seconds=summary.duration_seconds,
|
||||||
|
order_no=summary.order_no or "",
|
||||||
updated_at=summary.updated_at,
|
updated_at=summary.updated_at,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Dict, Iterable, List, Mapping
|
from typing import Any, Dict, Iterable, List, Mapping, Optional
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt, QTimer, pyqtSignal
|
from PyQt5.QtCore import Qt, QTimer, pyqtSignal
|
||||||
from PyQt5.QtGui import QCloseEvent, QKeySequence
|
from PyQt5.QtGui import QCloseEvent, QKeySequence
|
||||||
@@ -148,6 +148,21 @@ class ColorPriceView:
|
|||||||
price_inconsistent: bool = False
|
price_inconsistent: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PurchaseResultView:
|
||||||
|
"""采购任务的下单与核单结果。"""
|
||||||
|
|
||||||
|
mode: str
|
||||||
|
confirmed_spec: str
|
||||||
|
quantity: str
|
||||||
|
unit_price: str
|
||||||
|
total_price: str
|
||||||
|
order_no: str
|
||||||
|
ordered_at: str
|
||||||
|
match_status: str
|
||||||
|
payment_status: str
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class TaskDetailViewData:
|
class TaskDetailViewData:
|
||||||
"""详情窗口直接使用的展示数据。"""
|
"""详情窗口直接使用的展示数据。"""
|
||||||
@@ -169,6 +184,7 @@ class TaskDetailViewData:
|
|||||||
last_error: str
|
last_error: str
|
||||||
colors: List[ColorPriceView]
|
colors: List[ColorPriceView]
|
||||||
sizes: List[str]
|
sizes: List[str]
|
||||||
|
purchase: Optional[PurchaseResultView] = None
|
||||||
|
|
||||||
|
|
||||||
def _non_empty_text(value: Any) -> str:
|
def _non_empty_text(value: Any) -> str:
|
||||||
@@ -228,6 +244,72 @@ def _format_metric(value: Any) -> str:
|
|||||||
return "未采集"
|
return "未采集"
|
||||||
|
|
||||||
|
|
||||||
|
def _format_cent(value: Any) -> str:
|
||||||
|
if isinstance(value, int) and not isinstance(value, bool) and value >= 0:
|
||||||
|
return f"¥{value / 100:.2f}"
|
||||||
|
return "—"
|
||||||
|
|
||||||
|
|
||||||
|
def _purchase_result_view(
|
||||||
|
task_type: TaskType, pdd_data: Mapping[str, Any]
|
||||||
|
) -> Optional[PurchaseResultView]:
|
||||||
|
"""整理采购结果;非采购任务不显示采购信息卡片。"""
|
||||||
|
|
||||||
|
if task_type is not TaskType.PURCHASE:
|
||||||
|
return None
|
||||||
|
purchase = pdd_data.get("purchase")
|
||||||
|
if not isinstance(purchase, Mapping):
|
||||||
|
purchase = {}
|
||||||
|
confirmed = purchase.get("confirmed")
|
||||||
|
if not isinstance(confirmed, Mapping):
|
||||||
|
confirmed = {}
|
||||||
|
options = confirmed.get("options")
|
||||||
|
if not isinstance(options, Mapping):
|
||||||
|
options = {}
|
||||||
|
confirmed_spec = " / ".join(
|
||||||
|
_non_empty_text(value) for value in options.values()
|
||||||
|
if _non_empty_text(value)
|
||||||
|
) or "—"
|
||||||
|
mode_text = {"live": "真实采购", "dry_run": "采购演练"}
|
||||||
|
match_text = {
|
||||||
|
"matched": "已匹配",
|
||||||
|
"not_submitted": "未提交",
|
||||||
|
"not_found": "未找到",
|
||||||
|
"ambiguous": "多个候选",
|
||||||
|
"unknown": "结果不确定",
|
||||||
|
}
|
||||||
|
payment_text = {
|
||||||
|
"unpaid": "待付款",
|
||||||
|
"paid": "已支付",
|
||||||
|
"other": "其他状态",
|
||||||
|
"unknown": "未知",
|
||||||
|
}
|
||||||
|
quantity = confirmed.get("quantity")
|
||||||
|
return PurchaseResultView(
|
||||||
|
mode=mode_text.get(_non_empty_text(purchase.get("mode")), "—"),
|
||||||
|
confirmed_spec=confirmed_spec,
|
||||||
|
quantity=(
|
||||||
|
str(quantity)
|
||||||
|
if isinstance(quantity, int) and not isinstance(quantity, bool)
|
||||||
|
else "—"
|
||||||
|
),
|
||||||
|
unit_price=_format_cent(confirmed.get("unit_price_cent")),
|
||||||
|
total_price=_format_cent(confirmed.get("total_price_cent")),
|
||||||
|
order_no=_non_empty_text(purchase.get("order_no")) or "—",
|
||||||
|
ordered_at=(
|
||||||
|
_non_empty_text(purchase.get("ordered_at_raw"))
|
||||||
|
or _non_empty_text(purchase.get("ordered_at"))
|
||||||
|
or "—"
|
||||||
|
),
|
||||||
|
match_status=match_text.get(
|
||||||
|
_non_empty_text(purchase.get("match_status")), "—"
|
||||||
|
),
|
||||||
|
payment_status=payment_text.get(
|
||||||
|
_non_empty_text(purchase.get("payment_status")), "—"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def build_task_detail_view_data(detail: TaskDetail) -> TaskDetailViewData:
|
def build_task_detail_view_data(detail: TaskDetail) -> TaskDetailViewData:
|
||||||
"""从完整任务记录生成详情展示数据。
|
"""从完整任务记录生成详情展示数据。
|
||||||
|
|
||||||
@@ -310,6 +392,7 @@ def build_task_detail_view_data(detail: TaskDetail) -> TaskDetailViewData:
|
|||||||
last_error=last_error,
|
last_error=last_error,
|
||||||
colors=colors,
|
colors=colors,
|
||||||
sizes=sizes,
|
sizes=sizes,
|
||||||
|
purchase=_purchase_result_view(detail.task_type, pdd_data),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -401,6 +484,8 @@ class TaskDetailWindow(QWidget):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
content_layout.addWidget(self._build_basic_card(content))
|
content_layout.addWidget(self._build_basic_card(content))
|
||||||
|
if self.data.purchase is not None:
|
||||||
|
content_layout.addWidget(self._build_purchase_card(content))
|
||||||
content_layout.addWidget(
|
content_layout.addWidget(
|
||||||
self._build_spec_card("颜色", self.data.colors, content)
|
self._build_spec_card("颜色", self.data.colors, content)
|
||||||
)
|
)
|
||||||
@@ -508,6 +593,37 @@ class TaskDetailWindow(QWidget):
|
|||||||
layout.setColumnStretch(1, 1)
|
layout.setColumnStretch(1, 1)
|
||||||
return card
|
return card
|
||||||
|
|
||||||
|
def _build_purchase_card(self, parent: QWidget) -> CardWidget:
|
||||||
|
"""显示采购任务已经保存在本地的下单和核单结果。"""
|
||||||
|
|
||||||
|
purchase = self.data.purchase
|
||||||
|
assert purchase is not None
|
||||||
|
card = CardWidget(parent)
|
||||||
|
card.setObjectName("purchaseResultCard")
|
||||||
|
layout = QGridLayout(card)
|
||||||
|
layout.setContentsMargins(16, 14, 16, 14)
|
||||||
|
layout.setHorizontalSpacing(18)
|
||||||
|
layout.setVerticalSpacing(8)
|
||||||
|
values = (
|
||||||
|
("采购模式", purchase.mode),
|
||||||
|
("确认规格", purchase.confirmed_spec),
|
||||||
|
("确认数量", purchase.quantity),
|
||||||
|
("商品单价", purchase.unit_price),
|
||||||
|
("订单总价", purchase.total_price),
|
||||||
|
("订单号", purchase.order_no),
|
||||||
|
("下单时间", purchase.ordered_at),
|
||||||
|
("订单匹配", purchase.match_status),
|
||||||
|
("付款状态", purchase.payment_status),
|
||||||
|
)
|
||||||
|
layout.addWidget(SubtitleLabel("采购结果", card), 0, 0, 1, 2)
|
||||||
|
for row, (label, value) in enumerate(values, start=1):
|
||||||
|
layout.addWidget(CaptionLabel(label, card), row, 0)
|
||||||
|
text = self._copyable_label(value, label, card)
|
||||||
|
text.setWordWrap(True)
|
||||||
|
layout.addWidget(text, row, 1)
|
||||||
|
layout.setColumnStretch(1, 1)
|
||||||
|
return card
|
||||||
|
|
||||||
def _copyable_label(
|
def _copyable_label(
|
||||||
self, value: str, field_name: str, parent: QWidget
|
self, value: str, field_name: str, parent: QWidget
|
||||||
) -> CopyableBodyLabel:
|
) -> CopyableBodyLabel:
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ class TaskSummary:
|
|||||||
shop_name: Optional[str] = None
|
shop_name: Optional[str] = None
|
||||||
latest_run_status: Optional[RunStatus] = None
|
latest_run_status: Optional[RunStatus] = None
|
||||||
duration_seconds: Optional[int] = None
|
duration_seconds: Optional[int] = None
|
||||||
|
order_no: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
|
|||||||
@@ -136,7 +136,12 @@ class TaskRepository:
|
|||||||
" WHEN latest_run.started_at IS NOT NULL AND latest_run.finished_at IS NOT NULL"
|
" WHEN latest_run.started_at IS NOT NULL AND latest_run.finished_at IS NOT NULL"
|
||||||
" THEN MAX(0, CAST(strftime('%s', latest_run.finished_at)"
|
" THEN MAX(0, CAST(strftime('%s', latest_run.finished_at)"
|
||||||
" - strftime('%s', latest_run.started_at) AS INTEGER))"
|
" - strftime('%s', latest_run.started_at) AS INTEGER))"
|
||||||
" ELSE NULL END AS duration_seconds"
|
" ELSE NULL END AS duration_seconds,"
|
||||||
|
" CASE WHEN pdd_tasks.pdd_data IS NOT NULL"
|
||||||
|
" AND json_valid(pdd_tasks.pdd_data)"
|
||||||
|
" THEN CAST(json_extract(pdd_tasks.pdd_data,"
|
||||||
|
" '$.purchase.order_no') AS TEXT)"
|
||||||
|
" ELSE NULL END AS order_no"
|
||||||
" FROM pdd_tasks"
|
" FROM pdd_tasks"
|
||||||
" LEFT JOIN task_runs AS latest_run ON latest_run.id = ("
|
" LEFT JOIN task_runs AS latest_run ON latest_run.id = ("
|
||||||
" SELECT id FROM task_runs WHERE task_id = pdd_tasks.id"
|
" SELECT id FROM task_runs WHERE task_id = pdd_tasks.id"
|
||||||
@@ -1830,9 +1835,13 @@ class TaskRepository:
|
|||||||
clauses.append(
|
clauses.append(
|
||||||
"(remote_task_id LIKE ? ESCAPE '\\'"
|
"(remote_task_id LIKE ? ESCAPE '\\'"
|
||||||
" OR COALESCE(goods_id, '') LIKE ? ESCAPE '\\'"
|
" OR COALESCE(goods_id, '') LIKE ? ESCAPE '\\'"
|
||||||
" OR COALESCE(title, '') LIKE ? ESCAPE '\\')"
|
" OR COALESCE(title, '') LIKE ? ESCAPE '\\'"
|
||||||
|
" OR COALESCE(CASE WHEN pdd_data IS NOT NULL"
|
||||||
|
" AND json_valid(pdd_data)"
|
||||||
|
" THEN json_extract(pdd_data, '$.purchase.order_no')"
|
||||||
|
" END, '') LIKE ? ESCAPE '\\')"
|
||||||
)
|
)
|
||||||
parameters.extend((pattern, pattern, pattern))
|
parameters.extend((pattern, pattern, pattern, pattern))
|
||||||
return (" WHERE " + " AND ".join(clauses) if clauses else "", parameters)
|
return (" WHERE " + " AND ".join(clauses) if clauses else "", parameters)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -1859,6 +1868,7 @@ class TaskRepository:
|
|||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
duration_seconds=row["duration_seconds"],
|
duration_seconds=row["duration_seconds"],
|
||||||
|
order_no=row["order_no"],
|
||||||
updated_at=row["updated_at"],
|
updated_at=row["updated_at"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,15 @@ os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
|||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt
|
||||||
from PyQt5.QtWidgets import QApplication
|
from PyQt5.QtWidgets import QApplication
|
||||||
|
|
||||||
from src.pdd_ui import CHECK_COLUMN, PDDTaskPage, TaskRow, TaskTableModel
|
from src.pdd_ui import (
|
||||||
|
CHECK_COLUMN,
|
||||||
|
GOODS_ID_COLUMN,
|
||||||
|
ORDER_NO_COLUMN,
|
||||||
|
TITLE_COLUMN,
|
||||||
|
PDDTaskPage,
|
||||||
|
TaskRow,
|
||||||
|
TaskTableModel,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TaskTableModelCheckTests(unittest.TestCase):
|
class TaskTableModelCheckTests(unittest.TestCase):
|
||||||
@@ -166,7 +174,9 @@ class TaskTableModelCheckTests(unittest.TestCase):
|
|||||||
page.detailRequested.connect(received.append)
|
page.detailRequested.connect(received.append)
|
||||||
|
|
||||||
self.assertNotIn("操作", page.taskModel.HEADERS)
|
self.assertNotIn("操作", page.taskModel.HEADERS)
|
||||||
self.assertEqual(page.taskModel.columnCount(), 11)
|
self.assertEqual(page.taskModel.columnCount(), 13)
|
||||||
|
self.assertEqual(page.taskModel.HEADERS[GOODS_ID_COLUMN], "PDD商品ID")
|
||||||
|
self.assertEqual(page.taskModel.HEADERS[ORDER_NO_COLUMN], "订单号")
|
||||||
page._on_table_activated(page.taskModel.index(0, CHECK_COLUMN))
|
page._on_table_activated(page.taskModel.index(0, CHECK_COLUMN))
|
||||||
page._on_table_activated(page.taskModel.index(0, 2))
|
page._on_table_activated(page.taskModel.index(0, 2))
|
||||||
|
|
||||||
@@ -174,6 +184,35 @@ class TaskTableModelCheckTests(unittest.TestCase):
|
|||||||
self.assertIn("Enter", page.taskTable.accessibleDescription())
|
self.assertIn("Enter", page.taskTable.accessibleDescription())
|
||||||
page.deleteLater()
|
page.deleteLater()
|
||||||
|
|
||||||
|
def test_goods_id_order_no_and_full_value_tooltips_are_visible(self):
|
||||||
|
model = TaskTableModel()
|
||||||
|
model.set_tasks(
|
||||||
|
[
|
||||||
|
TaskRow(
|
||||||
|
"PUR-1",
|
||||||
|
"采购",
|
||||||
|
goods_id="897186799891",
|
||||||
|
title="很长的商品标题",
|
||||||
|
order_no="ORDER-20260811",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
model.data(model.index(0, GOODS_ID_COLUMN)), "897186799891"
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
model.data(model.index(0, ORDER_NO_COLUMN)), "ORDER-20260811"
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
model.data(model.index(0, TITLE_COLUMN), Qt.ToolTipRole),
|
||||||
|
"很长的商品标题",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
model.data(model.index(0, ORDER_NO_COLUMN), Qt.ToolTipRole),
|
||||||
|
"ORDER-20260811",
|
||||||
|
)
|
||||||
|
|
||||||
def test_remove_emits_stable_checked_task_ids(self):
|
def test_remove_emits_stable_checked_task_ids(self):
|
||||||
page = PDDTaskPage()
|
page = PDDTaskPage()
|
||||||
page.set_tasks(
|
page.set_tasks(
|
||||||
|
|||||||
@@ -1086,6 +1086,7 @@ class PDDTaskPageEventTest(unittest.TestCase):
|
|||||||
quantity=2,
|
quantity=2,
|
||||||
status=TaskStatus.MANUAL_REVIEW,
|
status=TaskStatus.MANUAL_REVIEW,
|
||||||
updated_at="2026-08-06T08:00:00Z",
|
updated_at="2026-08-06T08:00:00Z",
|
||||||
|
order_no="ORDER-001",
|
||||||
)
|
)
|
||||||
|
|
||||||
row = summary_to_row(summary)
|
row = summary_to_row(summary)
|
||||||
@@ -1093,6 +1094,7 @@ class PDDTaskPageEventTest(unittest.TestCase):
|
|||||||
self.assertEqual(row.task_type, "采购")
|
self.assertEqual(row.task_type, "采购")
|
||||||
self.assertEqual(row.status, "需要人工处理")
|
self.assertEqual(row.status, "需要人工处理")
|
||||||
self.assertEqual(row.goods_id, "")
|
self.assertEqual(row.goods_id, "")
|
||||||
|
self.assertEqual(row.order_no, "ORDER-001")
|
||||||
self.assertFalse(hasattr(row, "pdd_data"))
|
self.assertFalse(hasattr(row, "pdd_data"))
|
||||||
|
|
||||||
def test_summary_to_row_uses_task_specific_running_text(self):
|
def test_summary_to_row_uses_task_specific_running_text(self):
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
|||||||
|
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt
|
||||||
from PyQt5.QtTest import QTest
|
from PyQt5.QtTest import QTest
|
||||||
from PyQt5.QtWidgets import QApplication
|
from PyQt5.QtWidgets import QApplication, QWidget
|
||||||
|
|
||||||
from src.task_detail_view import (
|
from src.task_detail_view import (
|
||||||
CopyableBodyLabel,
|
CopyableBodyLabel,
|
||||||
@@ -145,6 +145,57 @@ class TaskDetailViewTest(unittest.TestCase):
|
|||||||
expected,
|
expected,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_purchase_result_shows_order_and_prefers_raw_order_time(self):
|
||||||
|
purchase_data = {
|
||||||
|
"purchase": {
|
||||||
|
"mode": "live",
|
||||||
|
"confirmed": {
|
||||||
|
"options": {"color": "黑色", "size": "L"},
|
||||||
|
"quantity": 2,
|
||||||
|
"unit_price_cent": 3990,
|
||||||
|
"total_price_cent": 7980,
|
||||||
|
},
|
||||||
|
"order_no": "ORDER-20260811",
|
||||||
|
"ordered_at": "2026-08-11T08:00:00+08:00",
|
||||||
|
"ordered_at_raw": "2026-08-11 08:00:00",
|
||||||
|
"match_status": "matched",
|
||||||
|
"payment_status": "unpaid",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
detail = replace(
|
||||||
|
make_detail(purchase_data),
|
||||||
|
remote_task_id="PUR-001",
|
||||||
|
task_type=TaskType.PURCHASE,
|
||||||
|
)
|
||||||
|
|
||||||
|
data = build_task_detail_view_data(detail)
|
||||||
|
|
||||||
|
self.assertEqual(data.purchase.mode, "真实采购")
|
||||||
|
self.assertEqual(data.purchase.confirmed_spec, "黑色 / L")
|
||||||
|
self.assertEqual(data.purchase.order_no, "ORDER-20260811")
|
||||||
|
self.assertEqual(data.purchase.ordered_at, "2026-08-11 08:00:00")
|
||||||
|
self.assertEqual(data.purchase.match_status, "已匹配")
|
||||||
|
self.assertEqual(data.purchase.payment_status, "待付款")
|
||||||
|
|
||||||
|
window = TaskDetailWindow(detail)
|
||||||
|
self.assertIsNotNone(window.findChild(QWidget, "purchaseResultCard"))
|
||||||
|
self.assertTrue(
|
||||||
|
any(
|
||||||
|
label.text() == "ORDER-20260811"
|
||||||
|
for label in window.findChildren(CopyableBodyLabel)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
window.close()
|
||||||
|
self.app.processEvents()
|
||||||
|
|
||||||
|
def test_collect_detail_does_not_show_purchase_result_card(self):
|
||||||
|
window = TaskDetailWindow(make_detail(collected_data()))
|
||||||
|
|
||||||
|
self.assertIsNone(window.findChild(QWidget, "purchaseResultCard"))
|
||||||
|
|
||||||
|
window.close()
|
||||||
|
self.app.processEvents()
|
||||||
|
|
||||||
def test_window_builds_color_section_before_size_section(self):
|
def test_window_builds_color_section_before_size_section(self):
|
||||||
window = TaskDetailWindow(make_detail(collected_data()))
|
window = TaskDetailWindow(make_detail(collected_data()))
|
||||||
|
|
||||||
|
|||||||
@@ -228,6 +228,44 @@ class TaskRepositoryTests(unittest.TestCase):
|
|||||||
self.assertEqual([task.remote_task_id for task in tasks], ["COLLECT-BLACK"])
|
self.assertEqual([task.remote_task_id for task in tasks], ["COLLECT-BLACK"])
|
||||||
self.assertEqual(self.repository.count_tasks(filters), 1)
|
self.assertEqual(self.repository.count_tasks(filters), 1)
|
||||||
|
|
||||||
|
def test_list_and_keyword_search_extract_purchase_order_no_safely(self) -> None:
|
||||||
|
self.repository.add_claimed_task(
|
||||||
|
self._task("PURCHASE-ORDER", TaskType.PURCHASE)
|
||||||
|
)
|
||||||
|
self.repository.add_claimed_task(self._task("COLLECT-BROKEN"))
|
||||||
|
connection = open_database(self.db_path)
|
||||||
|
try:
|
||||||
|
with connection:
|
||||||
|
connection.execute(
|
||||||
|
"UPDATE pdd_tasks SET pdd_data = ? WHERE remote_task_id = ?",
|
||||||
|
(
|
||||||
|
'{"purchase":{"order_no":"ORDER-20260811"}}',
|
||||||
|
"PURCHASE-ORDER",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
connection.execute(
|
||||||
|
"UPDATE pdd_tasks SET pdd_data = ? WHERE remote_task_id = ?",
|
||||||
|
("不是有效JSON", "COLLECT-BROKEN"),
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
tasks = self.repository.list_tasks()
|
||||||
|
by_id = {task.remote_task_id: task for task in tasks}
|
||||||
|
searched = self.repository.list_tasks(
|
||||||
|
TaskFilters(keyword="20260811")
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(by_id["PURCHASE-ORDER"].order_no, "ORDER-20260811")
|
||||||
|
self.assertIsNone(by_id["COLLECT-BROKEN"].order_no)
|
||||||
|
self.assertEqual(
|
||||||
|
[task.remote_task_id for task in searched], ["PURCHASE-ORDER"]
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.repository.count_tasks(TaskFilters(keyword="ORDER-20260811")),
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
|
||||||
def test_keyword_treats_percent_as_normal_text(self) -> None:
|
def test_keyword_treats_percent_as_normal_text(self) -> None:
|
||||||
self.repository.add_claimed_task(self._task("TASK-100%", title="百分号"))
|
self.repository.add_claimed_task(self._task("TASK-100%", title="百分号"))
|
||||||
self.repository.add_claimed_task(self._task("TASK-OTHER", title="普通商品"))
|
self.repository.add_claimed_task(self._task("TASK-OTHER", title="普通商品"))
|
||||||
|
|||||||
Reference in New Issue
Block a user