fix: use API workdays and final delivery date in bot replies

This commit is contained in:
ar51
2026-09-20 14:28:51 +08:00
parent 24469372fb
commit 1c25c9e1d9
6 changed files with 139 additions and 207 deletions

View File

@@ -5,7 +5,7 @@
**日期**:2026-09-02 **日期**:2026-09-02
**目的**:将 AM516 交期预测输出的自然日转换为工作日,并据此得到最终交货日期。 **目的**:将 AM516 交期预测输出的自然日转换为工作日,并据此得到最终交货日期。
**当前临时处理**:因 AR836 排期较晚,AM516 飞书机器人先在 API 返回后的输出层执行同一换算;不修改 AR836 API 原始数据。待 API 正式提供工作日口径后,应移除本地临时换算,避免重复转换。 **2026-09-20 实现更新**:API 已返回 `AM516工作日交期` 和 `AM516最终交货日期`。本仓库机器人回复已改为直接使用这两个字段,移除本地临时换算及日期推算;运行端需同步本次改动后生效。下文保留为原始需求与验证记录。
## 一、需求背景 ## 一、需求背景

View File

@@ -40,17 +40,18 @@ Expected:
- record-table columns follow this order: 序号、记录时间、型号、推荐类型、差异项、API状态、AM516预测交期、可用库存、主导因素; - 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 `—`; - 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: Rules and expected results:
- treat `AM516总交期`, or numeric `总交期` when the AM516 field is absent, as the original natural-day lead time `N`; - read `data.交期汇总.AM516工作日交期` and `data.交期汇总.AM516最终交货日期` directly;
- convert `N` with the approved quotient-and-remainder rule, including `r = 4` as one rest day and `r = 0` as no remainder workday; - when the API returns `8` and `2026-10-08`, output `AM516预测交期:8个工作日,最终交货日期:2026-10-08`;
- exclude the local query date itself, then count the converted workdays using the configured China statutory workday calendar; - use the same workdays-and-date form in the fastest-candidate summary, initial and continuation candidate details, and newly appended record rows;
- statutory holidays are excluded and official makeup weekend workdays are included; - ignore natural-day totals and the old `AM516预测交期` field when selecting the displayed result; do not recalculate workdays or dates locally;
- for a query date of `2026-09-02`, `31` natural days convert to `22` workdays and produce `2026-10-09`; - a valid API date in another calendar year remains usable without a local holiday calendar;
- output `2026-10-09(原始31个自然日,折算22个工作日)` instead of adding a separate National Day exception; - accept zero workdays; reject negative, fractional, boolean, or non-numeric workday values and invalid dates;
- when counting reaches an unconfigured calendar year, do not silently fall back to a Monday-Friday estimate. - 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 ## Sample 1A | Continue With Other Candidates

View File

@@ -10,8 +10,8 @@
6. Use model recommendation rules to generate the complete ordered internal candidate pool. 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. 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. 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. 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, 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. 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. 11. Append a non-secret record.
12. Return Feishu-friendly text with the fixed disclaimer. 12. Return Feishu-friendly text with the fixed disclaimer.

View File

@@ -93,12 +93,10 @@ Do not use temporary curl, WebFetch, or any API method outside the controlled sc
## Output Selection ## 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`. - Read the workday count directly from `data.交期汇总.AM516工作日交期` and the corresponding final delivery date from `data.交期汇总.AM516最终交货日期`.
- 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`. - 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.
- 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. - 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.
- 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. - 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.
- 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.
## Candidate Batch Rule ## Candidate Batch Rule
@@ -163,13 +161,13 @@ Use this field order for a successful Feishu report:
总体判断: 总体判断:
- 现货候选:{fully covered candidate or none} - 现货候选:{fully covered candidate or none}
- 最快候选型号:{fastest candidate} - 最快候选型号:{fastest candidate}
- 最快候选交期:{calculated delivery date, original natural days, and converted workdays} - 最快候选交期:{API workdays}个工作日,最终交货日期:{API final delivery date}
- 是否完全覆盖数量:{yes / no / unavailable} - 是否完全覆盖数量:{yes / no / unavailable}
- 主导因素:{main factor} - 主导因素:{main factor}
1. {model}({candidate type and reducer brand}) 1. {model}({candidate type and reducer brand})
- 数量:{quantity}台 - 数量:{quantity}台
- AM516预测交期:{calculated delivery date}(原始{N}个自然日,折算{W}个工作日) - AM516预测交期:{API workdays}个工作日,最终交货日期:{API final delivery date}
- 可用库存/覆盖情况:{summary-level inventory and coverage only} - 可用库存/覆盖情况:{summary-level inventory and coverage only}
- 主导因素:{main factor} - 主导因素:{main factor}
- 推荐理由:{rule-based recommendation reason} - 推荐理由:{rule-based recommendation reason}

View File

@@ -12,7 +12,7 @@ import threading
import time import time
import unicodedata import unicodedata
from dataclasses import dataclass from dataclasses import dataclass
from datetime import date, datetime, timedelta from datetime import date, datetime
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -82,40 +82,6 @@ CANDIDATE_CONTINUATION_TTL_SECONDS = 6 * 60 * 60
RECEIVED_REACTION_TYPE = "OnIt" 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 = { CANDIDATE_CONTINUATION_SHORT_PHRASES = {
"还有吗", "还有吗",
"还有呢", "还有呢",
@@ -209,12 +175,6 @@ class CandidateUnavailableError(RuntimeError):
pass pass
class WorkdayCalendarUnavailableError(ValueError):
def __init__(self, year: int) -> None:
self.year = year
super().__init__(f"{year}年国家法定工作日历未配置")
_candidate_continuations: dict[str, CandidateContinuation] = {} _candidate_continuations: dict[str, CandidateContinuation] = {}
_candidate_continuation_lock = threading.Lock() _candidate_continuation_lock = threading.Lock()
@@ -536,52 +496,7 @@ def run_controlled_query(request: RequestInput) -> dict[str, Any]:
return payload return payload
def natural_days_to_workdays(natural_days: int) -> int: def whole_day_count(value: Any) -> int | None:
"""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:
if isinstance(value, bool) or not isinstance(value, (int, float)): if isinstance(value, bool) or not isinstance(value, (int, float)):
return None return None
if isinstance(value, float) and not value.is_integer(): 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( def lead_time_details(
payload: dict[str, Any], payload: dict[str, Any],
requested_quantity: int, requested_quantity: int,
calculation_date: date | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
calculation_date = calculation_date or datetime.now().astimezone().date()
data = payload.get("data") data = payload.get("data")
if not isinstance(data, dict): if not isinstance(data, dict):
return { return {
@@ -635,31 +548,23 @@ def lead_time_details(
if isinstance(main_factor, (str, int, float)) if isinstance(main_factor, (str, int, float))
else "接口未返回,需人工复核" else "接口未返回,需人工复核"
) )
predicted_date = summary.get("AM516预测交期") total_days = whole_day_count(summary.get("AM516总交期"))
total_days = whole_natural_day_count(summary.get("AM516总交期"))
if total_days is None: if total_days is None:
total_days = whole_natural_day_count(summary.get("总交期")) total_days = whole_day_count(summary.get("总交期"))
workdays: int | None = None workdays = whole_day_count(summary.get("AM516工作日交期"))
delivery_date: date | None = None delivery_date = summary.get("AM516最终交货日期")
if total_days is not None: if isinstance(delivery_date, str) and re.fullmatch(r"[0-9]{4}-[0-9]{2}-[0-9]{2}", delivery_date):
workdays = natural_days_to_workdays(total_days)
try: try:
delivery_date = add_china_official_workdays(calculation_date, workdays) date.fromisoformat(delivery_date)
except WorkdayCalendarUnavailableError as exc: except ValueError:
prediction_value = ( delivery_date = None
f"【{exc}】(原始{total_days}个自然日,折算{workdays}个工作日)"
)
else: else:
prediction_value = ( delivery_date = None
f"{delivery_date.isoformat()}" if workdays is not None and delivery_date is not None:
f"(原始{total_days}个自然日,折算{workdays}个工作日)" prediction_value = f"{workdays}个工作日,最终交货日期:{delivery_date}"
)
prediction = f"AM516预测交期:{prediction_value}"
elif isinstance(predicted_date, (str, int, float)):
prediction_value = f"{predicted_date}(接口未返回自然日交期,未换算)"
prediction = f"AM516预测交期:{prediction_value}" prediction = f"AM516预测交期:{prediction_value}"
else: else:
prediction = "接口未返回可用交期汇总,需人工复核。" prediction = "接口未返回有效工作日交期和最终交货日期,需人工复核。"
prediction_value = None prediction_value = None
inventory_info = data.get("库存信息") inventory_info = data.get("库存信息")
@@ -738,7 +643,7 @@ def lead_time_details(
"prediction_value": prediction_value, "prediction_value": prediction_value,
"total_days": total_days, "total_days": total_days,
"workdays": workdays, "workdays": workdays,
"delivery_date": delivery_date.isoformat() if delivery_date else None, "delivery_date": delivery_date,
"main_factor": main_factor_text, "main_factor": main_factor_text,
"inventory": inventory_text, "inventory": inventory_text,
"available_inventory": ( "available_inventory": (
@@ -936,7 +841,6 @@ def build_report(request: RequestInput, candidate_offset: int = 0) -> str:
if candidate_offset < 0: if candidate_offset < 0:
raise InputError("候选批次位置无效,请重新发起型号和数量查询。") raise InputError("候选批次位置无效,请重新发起型号和数量查询。")
calculation_date = datetime.now().astimezone().date()
all_candidate_specs = generate_candidate_specs(request.model) all_candidate_specs = generate_candidate_specs(request.model)
candidate_specs = all_candidate_specs[ candidate_specs = all_candidate_specs[
candidate_offset : candidate_offset + CANDIDATE_BATCH_SIZE 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 = lead_time_details(
baseline_payload, baseline_payload,
request.quantity, request.quantity,
calculation_date=calculation_date,
) )
if not baseline["has_lead_time"]: if not baseline["has_lead_time"]:
raise RuntimeError("原型号接口未返回可用交期汇总,未生成候选结果。") raise RuntimeError("原型号接口未返回有效工作日交期和最终交货日期,未生成候选结果。")
candidates.append( candidates.append(
{ {
"index": 1, "index": 1,
@@ -979,7 +882,6 @@ def build_report(request: RequestInput, candidate_offset: int = 0) -> str:
details = lead_time_details( details = lead_time_details(
payload, payload,
request.quantity, request.quantity,
calculation_date=calculation_date,
) )
return spec, details if details["has_lead_time"] else None return spec, details if details["has_lead_time"] else None

View File

@@ -3,7 +3,6 @@ from __future__ import annotations
import sys import sys
import types import types
import unittest import unittest
from datetime import date
from types import SimpleNamespace from types import SimpleNamespace
from unittest import mock from unittest import mock
@@ -34,8 +33,9 @@ def successful_payload() -> dict[str, object]:
"data": { "data": {
"时间戳": "2026-08-31 12:00:00", "时间戳": "2026-08-31 12:00:00",
"交期汇总": { "交期汇总": {
"AM516预测交期": "2026-09-01",
"AM516总交期": 1, "AM516总交期": 1,
"AM516工作日交期": 1,
"AM516最终交货日期": "2026-09-01",
"AM516主导因素": "现货", "AM516主导因素": "现货",
}, },
"库存信息": {"可用库存(扣除了待产数量)": 100}, "库存信息": {"可用库存(扣除了待产数量)": 100},
@@ -437,89 +437,120 @@ class CandidateBatchTests(unittest.TestCase):
self.assertNotIn(model, report) self.assertNotIn(model, report)
class WorkdayConversionTests(unittest.TestCase): class ApiWorkdayOutputTests(unittest.TestCase):
def test_natural_day_conversion_rule(self) -> None: def test_api_workdays_and_date_override_legacy_fields(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:
payload = successful_payload() payload = successful_payload()
summary = payload["data"]["交期汇总"] summary = payload["data"]["交期汇总"]
summary["AM516预测交期"] = "2026-10-03" summary["AM516预测交期"] = "2026-10-03"
summary["AM516总交期"] = 31 summary["AM516总交期"] = 31
summary["AM516工作日交期"] = 8
summary["AM516最终交货日期"] = "2026-10-08"
details = bridge.lead_time_details( details = bridge.lead_time_details(payload, requested_quantity=10)
payload,
requested_quantity=10,
calculation_date=date(2026, 9, 2),
)
self.assertEqual(details["total_days"], 31) self.assertEqual(details["total_days"], 31)
self.assertEqual(details["workdays"], 22) self.assertEqual(details["workdays"], 8)
self.assertEqual(details["delivery_date"], "2026-10-09") self.assertEqual(details["delivery_date"], "2026-10-08")
self.assertEqual( self.assertEqual(
details["prediction"], details["prediction"],
"AM516预测交期:2026-10-09(原始31个自然日,折算22个工作日)", "AM516预测交期:8个工作日,最终交货日期:2026-10-08",
) )
def test_unconfigured_calendar_year_is_not_silently_estimated(self) -> None: def test_new_fields_work_without_natural_days_or_local_calendar(self) -> None:
with self.assertRaisesRegex( payload = successful_payload()
bridge.WorkdayCalendarUnavailableError, summary = payload["data"]["交期汇总"]
"2027年国家法定工作日历未配置", 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__": if __name__ == "__main__":