fix: use API workdays and final delivery date in bot replies
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
**日期**:2026-09-02
|
||||
**目的**:将 AM516 交期预测输出的自然日转换为工作日,并据此得到最终交货日期。
|
||||
|
||||
**当前临时处理**:因 AR836 排期较晚,AM516 飞书机器人先在 API 返回后的输出层执行同一换算;不修改 AR836 API 原始数据。待 API 正式提供工作日口径后,应移除本地临时换算,避免重复转换。
|
||||
**2026-09-20 实现更新**:API 已返回 `AM516工作日交期` 和 `AM516最终交货日期`。本仓库机器人回复已改为直接使用这两个字段,移除本地临时换算及日期推算;运行端需同步本次改动后生效。下文保留为原始需求与验证记录。
|
||||
|
||||
## 一、需求背景
|
||||
|
||||
|
||||
@@ -40,17 +40,18 @@ Expected:
|
||||
- record-table columns follow this order: 序号、记录时间、型号、推荐类型、差异项、API状态、AM516预测交期、可用库存、主导因素;
|
||||
- unavailable candidates remain in their original rule order and are recorded as `❌ 不存在` with unavailable result fields shown as `—`;
|
||||
|
||||
## Sample 1B | Natural Days To Workdays
|
||||
## Sample 1B | API Workdays And Final Delivery Date
|
||||
|
||||
Rules and expected results:
|
||||
|
||||
- treat `AM516总交期`, or numeric `总交期` when the AM516 field is absent, as the original natural-day lead time `N`;
|
||||
- convert `N` with the approved quotient-and-remainder rule, including `r = 4` as one rest day and `r = 0` as no remainder workday;
|
||||
- exclude the local query date itself, then count the converted workdays using the configured China statutory workday calendar;
|
||||
- statutory holidays are excluded and official makeup weekend workdays are included;
|
||||
- for a query date of `2026-09-02`, `31` natural days convert to `22` workdays and produce `2026-10-09`;
|
||||
- output `2026-10-09(原始31个自然日,折算22个工作日)` instead of adding a separate National Day exception;
|
||||
- when counting reaches an unconfigured calendar year, do not silently fall back to a Monday-Friday estimate.
|
||||
- read `data.交期汇总.AM516工作日交期` and `data.交期汇总.AM516最终交货日期` directly;
|
||||
- when the API returns `8` and `2026-10-08`, output `AM516预测交期:8个工作日,最终交货日期:2026-10-08`;
|
||||
- use the same workdays-and-date form in the fastest-candidate summary, initial and continuation candidate details, and newly appended record rows;
|
||||
- ignore natural-day totals and the old `AM516预测交期` field when selecting the displayed result; do not recalculate workdays or dates locally;
|
||||
- a valid API date in another calendar year remains usable without a local holiday calendar;
|
||||
- accept zero workdays; reject negative, fractional, boolean, or non-numeric workday values and invalid dates;
|
||||
- if either new field is missing or invalid, report the lead-time result as unavailable instead of falling back to old fields or estimating;
|
||||
- choose the fastest candidate using the API workday count, even when the natural-day totals would rank candidates differently.
|
||||
|
||||
## Sample 1A | Continue With Other Candidates
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
6. Use model recommendation rules to generate the complete ordered internal candidate pool.
|
||||
7. Immediately add the Feishu `OnIt` (typing) reaction to the original message to acknowledge the valid request.
|
||||
8. Run the controlled script for the original model baseline and only the first four candidate models.
|
||||
9. Convert each numeric natural-day lead time to workdays with the approved quotient-and-remainder rule, then count those workdays from the local query date through the configured China statutory workday calendar. The query date itself is excluded.
|
||||
10. Summarize the current batch, including the original natural days, converted workdays, and calculated delivery date; state how many candidates remain and invite the same sender to ask naturally for more when candidates remain.
|
||||
9. Read `data.交期汇总.AM516工作日交期` and `data.交期汇总.AM516最终交货日期` directly. Require a nonnegative whole-number workday count and a valid `YYYY-MM-DD` date; missing or invalid fields make the result unavailable. Do not convert natural days or recalculate dates locally.
|
||||
10. Summarize the current batch with the API workdays and final delivery date, for example `AM516预测交期:8个工作日,最终交货日期:2026-10-08`; state how many candidates remain and invite the same sender to ask naturally for more when candidates remain. Newly appended records use the same form; historical records are unchanged.
|
||||
11. Append a non-secret record.
|
||||
12. Return Feishu-friendly text with the fixed disclaimer.
|
||||
|
||||
|
||||
@@ -93,12 +93,10 @@ Do not use temporary curl, WebFetch, or any API method outside the controlled sc
|
||||
|
||||
## Output Selection
|
||||
|
||||
- Prefer numeric `data.交期汇总.AM516总交期`; if it is absent, fall back to numeric `data.交期汇总.总交期`. Treat this value as the original natural-day lead time `N`.
|
||||
- Convert `N` with `(q, r) = divmod(N, 7)`: `r = 0` gives `q × 5`; `1 ≤ r ≤ 4` gives `q × 5 + MAX(r - 1, 0)`; `5 ≤ r ≤ 6` gives `q × 5 + r - 2`. The result is workday lead time `W`.
|
||||
- Starting from the local query date, exclude the query date itself and count forward `W` workdays using the configured China statutory calendar. Statutory holidays are not workdays, ordinary Saturdays and Sundays are not workdays, and official makeup Saturdays or Sundays are workdays.
|
||||
- The configured 2026 calendar follows `国办发明电〔2025〕7号`. If counting reaches a year whose official calendar is not configured, do not silently estimate a delivery date; mark that calendar year as pending.
|
||||
- Display the calculated delivery date together with both `N` and `W`, for example `2026-10-09(原始31个自然日,折算22个工作日)`.
|
||||
- Use `data.交期汇总.AM516预测交期` only as a fallback when the API does not return a usable whole-number natural-day lead time, and explicitly mark that no conversion was performed.
|
||||
- Read the workday count directly from `data.交期汇总.AM516工作日交期` and the corresponding final delivery date from `data.交期汇总.AM516最终交货日期`.
|
||||
- Require a nonnegative whole-number workday count (including zero) and a valid `YYYY-MM-DD` date. Both must be returned by the API; missing or invalid values make the lead-time result unavailable.
|
||||
- Display `8个工作日,最终交货日期:2026-10-08` consistently in the overall judgment, candidate details, and newly appended records. Rank the fastest candidate by the API workday count.
|
||||
- Do not convert natural days, recalculate dates from the local query date, or fall back to the old `AM516预测交期` field. `AM516总交期` and `总交期` remain natural-day reference fields and are not shown in the reply.
|
||||
|
||||
## Candidate Batch Rule
|
||||
|
||||
@@ -163,13 +161,13 @@ Use this field order for a successful Feishu report:
|
||||
总体判断:
|
||||
- 现货候选:{fully covered candidate or none}
|
||||
- 最快候选型号:{fastest candidate}
|
||||
- 最快候选交期:{calculated delivery date, original natural days, and converted workdays}
|
||||
- 最快候选交期:{API workdays}个工作日,最终交货日期:{API final delivery date}
|
||||
- 是否完全覆盖数量:{yes / no / unavailable}
|
||||
- 主导因素:{main factor}
|
||||
|
||||
1. {model}({candidate type and reducer brand})
|
||||
- 数量:{quantity}台
|
||||
- AM516预测交期:{calculated delivery date}(原始{N}个自然日,折算{W}个工作日)
|
||||
- AM516预测交期:{API workdays}个工作日,最终交货日期:{API final delivery date}
|
||||
- 可用库存/覆盖情况:{summary-level inventory and coverage only}
|
||||
- 主导因素:{main factor}
|
||||
- 推荐理由:{rule-based recommendation reason}
|
||||
|
||||
@@ -12,7 +12,7 @@ import threading
|
||||
import time
|
||||
import unicodedata
|
||||
from dataclasses import dataclass
|
||||
from datetime import date, datetime, timedelta
|
||||
from datetime import date, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -82,40 +82,6 @@ CANDIDATE_CONTINUATION_TTL_SECONDS = 6 * 60 * 60
|
||||
RECEIVED_REACTION_TYPE = "OnIt"
|
||||
|
||||
|
||||
def _inclusive_date_range(start: date, end: date) -> frozenset[date]:
|
||||
return frozenset(
|
||||
start + timedelta(days=offset)
|
||||
for offset in range((end - start).days + 1)
|
||||
)
|
||||
|
||||
|
||||
# 国办发明电〔2025〕7号:国务院办公厅关于2026年部分节假日安排的通知。
|
||||
CHINA_WORKDAY_CALENDAR_SOURCE = (
|
||||
"https://www.gov.cn/zhengce/zhengceku/202511/content_7047091.htm"
|
||||
)
|
||||
CHINA_OFFICIAL_HOLIDAYS = {
|
||||
2026: frozenset().union(
|
||||
_inclusive_date_range(date(2026, 1, 1), date(2026, 1, 3)),
|
||||
_inclusive_date_range(date(2026, 2, 15), date(2026, 2, 23)),
|
||||
_inclusive_date_range(date(2026, 4, 4), date(2026, 4, 6)),
|
||||
_inclusive_date_range(date(2026, 5, 1), date(2026, 5, 5)),
|
||||
_inclusive_date_range(date(2026, 6, 19), date(2026, 6, 21)),
|
||||
_inclusive_date_range(date(2026, 9, 25), date(2026, 9, 27)),
|
||||
_inclusive_date_range(date(2026, 10, 1), date(2026, 10, 7)),
|
||||
)
|
||||
}
|
||||
CHINA_OFFICIAL_MAKEUP_WORKDAYS = {
|
||||
2026: frozenset(
|
||||
{
|
||||
date(2026, 1, 4),
|
||||
date(2026, 2, 14),
|
||||
date(2026, 2, 28),
|
||||
date(2026, 5, 9),
|
||||
date(2026, 9, 20),
|
||||
date(2026, 10, 10),
|
||||
}
|
||||
)
|
||||
}
|
||||
CANDIDATE_CONTINUATION_SHORT_PHRASES = {
|
||||
"还有吗",
|
||||
"还有呢",
|
||||
@@ -209,12 +175,6 @@ class CandidateUnavailableError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class WorkdayCalendarUnavailableError(ValueError):
|
||||
def __init__(self, year: int) -> None:
|
||||
self.year = year
|
||||
super().__init__(f"{year}年国家法定工作日历未配置")
|
||||
|
||||
|
||||
_candidate_continuations: dict[str, CandidateContinuation] = {}
|
||||
_candidate_continuation_lock = threading.Lock()
|
||||
|
||||
@@ -536,52 +496,7 @@ def run_controlled_query(request: RequestInput) -> dict[str, Any]:
|
||||
return payload
|
||||
|
||||
|
||||
def natural_days_to_workdays(natural_days: int) -> int:
|
||||
"""Convert natural days with AR51's fixed quotient-and-remainder rule."""
|
||||
if isinstance(natural_days, bool) or not isinstance(natural_days, int):
|
||||
raise TypeError("自然日交期必须是整数")
|
||||
if natural_days < 0:
|
||||
raise ValueError("自然日交期不能小于0")
|
||||
|
||||
full_weeks, remainder = divmod(natural_days, 7)
|
||||
if remainder == 0:
|
||||
remainder_workdays = 0
|
||||
elif remainder <= 4:
|
||||
remainder_workdays = max(remainder - 1, 0)
|
||||
else:
|
||||
remainder_workdays = remainder - 2
|
||||
return full_weeks * 5 + remainder_workdays
|
||||
|
||||
|
||||
def is_china_official_workday(day: date) -> bool:
|
||||
holidays = CHINA_OFFICIAL_HOLIDAYS.get(day.year)
|
||||
makeup_workdays = CHINA_OFFICIAL_MAKEUP_WORKDAYS.get(day.year)
|
||||
if holidays is None or makeup_workdays is None:
|
||||
raise WorkdayCalendarUnavailableError(day.year)
|
||||
if day in makeup_workdays:
|
||||
return True
|
||||
if day in holidays:
|
||||
return False
|
||||
return day.weekday() < 5
|
||||
|
||||
|
||||
def add_china_official_workdays(start: date, workdays: int) -> date:
|
||||
"""Count official workdays after start; the start date itself is excluded."""
|
||||
if isinstance(workdays, bool) or not isinstance(workdays, int):
|
||||
raise TypeError("工作日交期必须是整数")
|
||||
if workdays < 0:
|
||||
raise ValueError("工作日交期不能小于0")
|
||||
|
||||
current = start
|
||||
counted = 0
|
||||
while counted < workdays:
|
||||
current += timedelta(days=1)
|
||||
if is_china_official_workday(current):
|
||||
counted += 1
|
||||
return current
|
||||
|
||||
|
||||
def whole_natural_day_count(value: Any) -> int | None:
|
||||
def whole_day_count(value: Any) -> int | None:
|
||||
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
||||
return None
|
||||
if isinstance(value, float) and not value.is_integer():
|
||||
@@ -593,9 +508,7 @@ def whole_natural_day_count(value: Any) -> int | None:
|
||||
def lead_time_details(
|
||||
payload: dict[str, Any],
|
||||
requested_quantity: int,
|
||||
calculation_date: date | None = None,
|
||||
) -> dict[str, Any]:
|
||||
calculation_date = calculation_date or datetime.now().astimezone().date()
|
||||
data = payload.get("data")
|
||||
if not isinstance(data, dict):
|
||||
return {
|
||||
@@ -635,31 +548,23 @@ def lead_time_details(
|
||||
if isinstance(main_factor, (str, int, float))
|
||||
else "接口未返回,需人工复核"
|
||||
)
|
||||
predicted_date = summary.get("AM516预测交期")
|
||||
total_days = whole_natural_day_count(summary.get("AM516总交期"))
|
||||
total_days = whole_day_count(summary.get("AM516总交期"))
|
||||
if total_days is None:
|
||||
total_days = whole_natural_day_count(summary.get("总交期"))
|
||||
workdays: int | None = None
|
||||
delivery_date: date | None = None
|
||||
if total_days is not None:
|
||||
workdays = natural_days_to_workdays(total_days)
|
||||
total_days = whole_day_count(summary.get("总交期"))
|
||||
workdays = whole_day_count(summary.get("AM516工作日交期"))
|
||||
delivery_date = summary.get("AM516最终交货日期")
|
||||
if isinstance(delivery_date, str) and re.fullmatch(r"[0-9]{4}-[0-9]{2}-[0-9]{2}", delivery_date):
|
||||
try:
|
||||
delivery_date = add_china_official_workdays(calculation_date, workdays)
|
||||
except WorkdayCalendarUnavailableError as exc:
|
||||
prediction_value = (
|
||||
f"【{exc}】(原始{total_days}个自然日,折算{workdays}个工作日)"
|
||||
)
|
||||
else:
|
||||
prediction_value = (
|
||||
f"{delivery_date.isoformat()}"
|
||||
f"(原始{total_days}个自然日,折算{workdays}个工作日)"
|
||||
)
|
||||
prediction = f"AM516预测交期:{prediction_value}"
|
||||
elif isinstance(predicted_date, (str, int, float)):
|
||||
prediction_value = f"{predicted_date}(接口未返回自然日交期,未换算)"
|
||||
date.fromisoformat(delivery_date)
|
||||
except ValueError:
|
||||
delivery_date = None
|
||||
else:
|
||||
delivery_date = None
|
||||
if workdays is not None and delivery_date is not None:
|
||||
prediction_value = f"{workdays}个工作日,最终交货日期:{delivery_date}"
|
||||
prediction = f"AM516预测交期:{prediction_value}"
|
||||
else:
|
||||
prediction = "接口未返回可用交期汇总,需人工复核。"
|
||||
prediction = "接口未返回有效工作日交期和最终交货日期,需人工复核。"
|
||||
prediction_value = None
|
||||
|
||||
inventory_info = data.get("库存信息")
|
||||
@@ -738,7 +643,7 @@ def lead_time_details(
|
||||
"prediction_value": prediction_value,
|
||||
"total_days": total_days,
|
||||
"workdays": workdays,
|
||||
"delivery_date": delivery_date.isoformat() if delivery_date else None,
|
||||
"delivery_date": delivery_date,
|
||||
"main_factor": main_factor_text,
|
||||
"inventory": inventory_text,
|
||||
"available_inventory": (
|
||||
@@ -936,7 +841,6 @@ def build_report(request: RequestInput, candidate_offset: int = 0) -> str:
|
||||
if candidate_offset < 0:
|
||||
raise InputError("候选批次位置无效,请重新发起型号和数量查询。")
|
||||
|
||||
calculation_date = datetime.now().astimezone().date()
|
||||
all_candidate_specs = generate_candidate_specs(request.model)
|
||||
candidate_specs = all_candidate_specs[
|
||||
candidate_offset : candidate_offset + CANDIDATE_BATCH_SIZE
|
||||
@@ -952,10 +856,9 @@ def build_report(request: RequestInput, candidate_offset: int = 0) -> str:
|
||||
baseline = lead_time_details(
|
||||
baseline_payload,
|
||||
request.quantity,
|
||||
calculation_date=calculation_date,
|
||||
)
|
||||
if not baseline["has_lead_time"]:
|
||||
raise RuntimeError("原型号接口未返回可用交期汇总,未生成候选结果。")
|
||||
raise RuntimeError("原型号接口未返回有效工作日交期和最终交货日期,未生成候选结果。")
|
||||
candidates.append(
|
||||
{
|
||||
"index": 1,
|
||||
@@ -979,7 +882,6 @@ def build_report(request: RequestInput, candidate_offset: int = 0) -> str:
|
||||
details = lead_time_details(
|
||||
payload,
|
||||
request.quantity,
|
||||
calculation_date=calculation_date,
|
||||
)
|
||||
return spec, details if details["has_lead_time"] else None
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ from __future__ import annotations
|
||||
import sys
|
||||
import types
|
||||
import unittest
|
||||
from datetime import date
|
||||
from types import SimpleNamespace
|
||||
from unittest import mock
|
||||
|
||||
@@ -34,8 +33,9 @@ def successful_payload() -> dict[str, object]:
|
||||
"data": {
|
||||
"时间戳": "2026-08-31 12:00:00",
|
||||
"交期汇总": {
|
||||
"AM516预测交期": "2026-09-01",
|
||||
"AM516总交期": 1,
|
||||
"AM516工作日交期": 1,
|
||||
"AM516最终交货日期": "2026-09-01",
|
||||
"AM516主导因素": "现货",
|
||||
},
|
||||
"库存信息": {"可用库存(扣除了待产数量)": 100},
|
||||
@@ -437,89 +437,120 @@ class CandidateBatchTests(unittest.TestCase):
|
||||
self.assertNotIn(model, report)
|
||||
|
||||
|
||||
class WorkdayConversionTests(unittest.TestCase):
|
||||
def test_natural_day_conversion_rule(self) -> None:
|
||||
expected = {
|
||||
0: 0,
|
||||
1: 0,
|
||||
2: 1,
|
||||
3: 2,
|
||||
4: 3,
|
||||
5: 3,
|
||||
6: 4,
|
||||
7: 5,
|
||||
18: 13,
|
||||
23: 16,
|
||||
26: 18,
|
||||
31: 22,
|
||||
33: 23,
|
||||
36: 25,
|
||||
43: 30,
|
||||
48: 34,
|
||||
}
|
||||
|
||||
for natural_days, workdays in expected.items():
|
||||
with self.subTest(natural_days=natural_days):
|
||||
self.assertEqual(
|
||||
bridge.natural_days_to_workdays(natural_days),
|
||||
workdays,
|
||||
)
|
||||
|
||||
def test_2026_official_holiday_and_makeup_workdays(self) -> None:
|
||||
self.assertTrue(bridge.is_china_official_workday(date(2026, 9, 20)))
|
||||
self.assertFalse(bridge.is_china_official_workday(date(2026, 9, 25)))
|
||||
self.assertFalse(bridge.is_china_official_workday(date(2026, 10, 1)))
|
||||
self.assertTrue(bridge.is_china_official_workday(date(2026, 10, 10)))
|
||||
self.assertFalse(bridge.is_china_official_workday(date(2026, 10, 11)))
|
||||
|
||||
def test_workday_dates_match_validation_samples(self) -> None:
|
||||
start = date(2026, 9, 2)
|
||||
samples = {
|
||||
18: date(2026, 9, 20),
|
||||
23: date(2026, 9, 23),
|
||||
26: date(2026, 9, 28),
|
||||
16: date(2026, 9, 17),
|
||||
31: date(2026, 10, 9),
|
||||
33: date(2026, 10, 10),
|
||||
36: date(2026, 10, 13),
|
||||
43: date(2026, 10, 20),
|
||||
48: date(2026, 10, 26),
|
||||
}
|
||||
|
||||
for natural_days, expected_date in samples.items():
|
||||
with self.subTest(natural_days=natural_days):
|
||||
workdays = bridge.natural_days_to_workdays(natural_days)
|
||||
self.assertEqual(
|
||||
bridge.add_china_official_workdays(start, workdays),
|
||||
expected_date,
|
||||
)
|
||||
|
||||
def test_api_natural_days_drive_displayed_delivery_date(self) -> None:
|
||||
class ApiWorkdayOutputTests(unittest.TestCase):
|
||||
def test_api_workdays_and_date_override_legacy_fields(self) -> None:
|
||||
payload = successful_payload()
|
||||
summary = payload["data"]["交期汇总"]
|
||||
summary["AM516预测交期"] = "2026-10-03"
|
||||
summary["AM516总交期"] = 31
|
||||
summary["AM516工作日交期"] = 8
|
||||
summary["AM516最终交货日期"] = "2026-10-08"
|
||||
|
||||
details = bridge.lead_time_details(
|
||||
payload,
|
||||
requested_quantity=10,
|
||||
calculation_date=date(2026, 9, 2),
|
||||
)
|
||||
details = bridge.lead_time_details(payload, requested_quantity=10)
|
||||
|
||||
self.assertEqual(details["total_days"], 31)
|
||||
self.assertEqual(details["workdays"], 22)
|
||||
self.assertEqual(details["delivery_date"], "2026-10-09")
|
||||
self.assertEqual(details["workdays"], 8)
|
||||
self.assertEqual(details["delivery_date"], "2026-10-08")
|
||||
self.assertEqual(
|
||||
details["prediction"],
|
||||
"AM516预测交期:2026-10-09(原始31个自然日,折算22个工作日)",
|
||||
"AM516预测交期:8个工作日,最终交货日期:2026-10-08",
|
||||
)
|
||||
|
||||
def test_unconfigured_calendar_year_is_not_silently_estimated(self) -> None:
|
||||
with self.assertRaisesRegex(
|
||||
bridge.WorkdayCalendarUnavailableError,
|
||||
"2027年国家法定工作日历未配置",
|
||||
def test_new_fields_work_without_natural_days_or_local_calendar(self) -> None:
|
||||
payload = successful_payload()
|
||||
summary = payload["data"]["交期汇总"]
|
||||
del summary["AM516总交期"]
|
||||
summary["AM516工作日交期"] = 8
|
||||
summary["AM516最终交货日期"] = "2027-01-15"
|
||||
|
||||
details = bridge.lead_time_details(payload, requested_quantity=10)
|
||||
|
||||
self.assertTrue(details["has_lead_time"])
|
||||
self.assertEqual(details["workdays"], 8)
|
||||
self.assertEqual(details["delivery_date"], "2027-01-15")
|
||||
|
||||
def test_zero_workdays_are_preserved(self) -> None:
|
||||
payload = successful_payload()
|
||||
payload["data"]["交期汇总"]["AM516工作日交期"] = 0
|
||||
|
||||
details = bridge.lead_time_details(payload, requested_quantity=10)
|
||||
|
||||
self.assertTrue(details["has_lead_time"])
|
||||
self.assertEqual(
|
||||
details["prediction_value"], "0个工作日,最终交货日期:2026-09-01"
|
||||
)
|
||||
|
||||
def test_missing_new_fields_do_not_fall_back_to_old_fields(self) -> None:
|
||||
for field in ("AM516工作日交期", "AM516最终交货日期"):
|
||||
with self.subTest(field=field):
|
||||
payload = successful_payload()
|
||||
summary = payload["data"]["交期汇总"]
|
||||
summary["AM516预测交期"] = "2026-09-01"
|
||||
summary["总交期"] = 15
|
||||
del summary[field]
|
||||
|
||||
details = bridge.lead_time_details(payload, requested_quantity=10)
|
||||
|
||||
self.assertFalse(details["has_lead_time"])
|
||||
self.assertIsNone(details["prediction_value"])
|
||||
self.assertIn("未返回有效工作日交期和最终交货日期", details["prediction"])
|
||||
|
||||
def test_invalid_workdays_and_dates_are_not_displayed_as_valid(self) -> None:
|
||||
invalid_values = {
|
||||
"AM516工作日交期": [None, True, -1, 1.5, "8", float("nan"), float("inf")],
|
||||
"AM516最终交货日期": [None, True, 20261008, "", "2026-02-30", "20261008"],
|
||||
}
|
||||
for field, values in invalid_values.items():
|
||||
for value in values:
|
||||
with self.subTest(field=field, value=value):
|
||||
payload = successful_payload()
|
||||
payload["data"]["交期汇总"][field] = value
|
||||
|
||||
details = bridge.lead_time_details(payload, requested_quantity=10)
|
||||
|
||||
self.assertFalse(details["has_lead_time"])
|
||||
self.assertIsNone(details["prediction_value"])
|
||||
|
||||
def test_report_and_record_use_api_workdays_for_fastest_candidate(self) -> None:
|
||||
request = bridge.RequestInput(model="eRob70H50I-FS-18CN[V5]", quantity=10)
|
||||
fastest_model = bridge.generate_candidate_specs(request.model)[0]["model"]
|
||||
|
||||
def query(query_request: bridge.RequestInput) -> dict[str, object]:
|
||||
payload = successful_payload()
|
||||
summary = payload["data"]["交期汇总"]
|
||||
fastest = query_request.model == fastest_model
|
||||
# Deliberately disagree with natural-day ranking to detect old conversion.
|
||||
summary["AM516总交期"] = 31 if fastest else 1
|
||||
summary["AM516工作日交期"] = 8 if fastest else 18
|
||||
summary["AM516最终交货日期"] = "2026-10-08" if fastest else "2026-10-21"
|
||||
return payload
|
||||
|
||||
with (
|
||||
mock.patch.object(bridge, "run_controlled_query", side_effect=query),
|
||||
mock.patch.object(bridge, "write_record") as write_record,
|
||||
):
|
||||
bridge.add_china_official_workdays(date(2026, 12, 31), 1)
|
||||
report = bridge.build_report(request)
|
||||
|
||||
record = bridge.render_record(*write_record.call_args.args)
|
||||
self.assertIn(f"最快候选型号:{fastest_model}", report)
|
||||
self.assertIn("最快候选交期:8个工作日,最终交货日期:2026-10-08", report)
|
||||
for output in (report, record):
|
||||
self.assertIn("8个工作日,最终交货日期:2026-10-08", output)
|
||||
self.assertIn("18个工作日,最终交货日期:2026-10-21", output)
|
||||
self.assertNotIn("自然日", output)
|
||||
self.assertNotIn("折算", output)
|
||||
|
||||
def test_baseline_missing_new_fields_stops_before_recording(self) -> None:
|
||||
payload = successful_payload()
|
||||
del payload["data"]["交期汇总"]["AM516工作日交期"]
|
||||
request = bridge.RequestInput(model="eRob70H50I-FS-18CN[V5]", quantity=10)
|
||||
with (
|
||||
mock.patch.object(bridge, "run_controlled_query", return_value=payload),
|
||||
mock.patch.object(bridge, "write_record") as write_record,
|
||||
):
|
||||
with self.assertRaisesRegex(RuntimeError, "工作日交期和最终交货日期"):
|
||||
bridge.build_report(request)
|
||||
write_record.assert_not_called()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user