2026-07-28 02:12:46 +08:00
|
|
|
|
# ============================================================
|
|
|
|
|
|
# 工程目录深度解析 + 按目录排产意图
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
|
|
from server.agent_core.intent import parse_fast
|
|
|
|
|
|
from server.aps_domain.folder_pack import analyze_work_dir
|
|
|
|
|
|
from server.state.seed import seed_world
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_analyze_work_dir_reads_xlsx(tmp_path: Path, monkeypatch):
|
|
|
|
|
|
# 最小可解析 CSV 订单表
|
|
|
|
|
|
orders = tmp_path / "订单.csv"
|
|
|
|
|
|
orders.write_text(
|
|
|
|
|
|
"订单号,产品编码,数量,交期,客户\n"
|
|
|
|
|
|
"SO001,P-1,10,2026-08-01,甲\n",
|
|
|
|
|
|
encoding="utf-8-sig",
|
|
|
|
|
|
)
|
|
|
|
|
|
mats = tmp_path / "物料.csv"
|
|
|
|
|
|
mats.write_text(
|
|
|
|
|
|
"编码,名称,类型,单位,库存\n"
|
|
|
|
|
|
"P-1,成品A,FINISHED_PRODUCT,件,100\n",
|
|
|
|
|
|
encoding="utf-8-sig",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
class FakePS:
|
|
|
|
|
|
def snapshot(self, include_messages=False):
|
|
|
|
|
|
return {
|
|
|
|
|
|
"projects": [{"id": "p1", "name": "测", "workDir": str(tmp_path)}],
|
|
|
|
|
|
"sessions": [{"id": "s1", "projectId": "p1"}],
|
|
|
|
|
|
"files": [],
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr("server.state.projects.get_project_store", lambda: FakePS())
|
|
|
|
|
|
world = seed_world()
|
|
|
|
|
|
report = analyze_work_dir(world, "s1")
|
|
|
|
|
|
assert report["ok"] is True
|
|
|
|
|
|
assert report["totalOk"] >= 1
|
|
|
|
|
|
assert any(f["name"] == "订单.csv" for f in report["files"])
|
|
|
|
|
|
assert "字段" in report["markdown"] or any(f.get("headers") for f in report["files"])
|
|
|
|
|
|
assert "样例" in report["markdown"] or any(f.get("samples") for f in report["files"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_schedule_intent_from_messy_zh():
|
|
|
|
|
|
world = seed_world()
|
|
|
|
|
|
assert parse_fast("以上数据根据排产你要怎么数据", world).intent == "folder.schedule"
|
|
|
|
|
|
assert parse_fast("解析工程目录", world).intent == "folder.analyze"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_kangni_style_headers_soft_preview():
|
|
|
|
|
|
"""康尼现场表头(料号/订单数量/计划结束时间…)应被 soft 识别为有效行。"""
|
|
|
|
|
|
from server.aps_domain.importers import preview_file, detect_field_map
|
|
|
|
|
|
|
|
|
|
|
|
world = seed_world()
|
|
|
|
|
|
csv = (
|
|
|
|
|
|
"订单代码,WBS号,料号,物料描述,计划结束时间,订单数量,项目名称\n"
|
|
|
|
|
|
"102285668,WBS1,28200003654300,承载机构,2026-08-08,1,穗莞深项目\n"
|
|
|
|
|
|
).encode("utf-8-sig")
|
|
|
|
|
|
preview = preview_file("订单.csv", csv, world, soft=True, kind_hint="orders")
|
|
|
|
|
|
assert preview["totalOk"] == 1
|
|
|
|
|
|
row = preview["batches"][0]["okRows"][0]
|
|
|
|
|
|
assert row["productCode"] == "28200003654300"
|
|
|
|
|
|
assert row["quantity"] == 1
|
|
|
|
|
|
assert row["deliveryDate"].startswith("2026-08-08")
|
|
|
|
|
|
assert row["customerName"] == "穗莞深项目"
|
|
|
|
|
|
fmap = detect_field_map("orders", ["订单代码", "料号", "订单数量", "计划结束时间", "项目名称"])
|
|
|
|
|
|
targets = {m["target"] for m in fmap}
|
|
|
|
|
|
assert {"productCode", "quantity", "deliveryDate", "customerName", "orderNo"} <= targets
|
2026-08-11 00:54:05 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _run_folder_analyze(store, session_id: str):
|
|
|
|
|
|
from server.agent_core.intent import IntentResult
|
|
|
|
|
|
from server.aps_domain.workflow import handle_intent
|
|
|
|
|
|
|
|
|
|
|
|
return await handle_intent(
|
|
|
|
|
|
store,
|
|
|
|
|
|
session_id,
|
|
|
|
|
|
IntentResult(
|
|
|
|
|
|
intent="folder.analyze",
|
|
|
|
|
|
params={"query": "分析文件夹"},
|
|
|
|
|
|
confidence=1.0,
|
|
|
|
|
|
source="RULE_FAST",
|
|
|
|
|
|
),
|
|
|
|
|
|
actor="test",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_folder_analyze_writes_master_data_and_knowledge(tmp_path: Path, monkeypatch):
|
|
|
|
|
|
import asyncio
|
|
|
|
|
|
import uuid
|
|
|
|
|
|
|
|
|
|
|
|
from server.auth.context import IdentityContext, bind_identity, reset_identity
|
|
|
|
|
|
from server.knowledge.assets import KnowledgeStore
|
|
|
|
|
|
from server.state.seed import empty_world
|
|
|
|
|
|
|
|
|
|
|
|
(tmp_path / "订单.csv").write_text(
|
|
|
|
|
|
"订单号,产品编码,数量,交期,客户\n"
|
|
|
|
|
|
"SO-FOLDER-1,P-FOLDER-1,12,2026-08-15,锐扬\n",
|
|
|
|
|
|
encoding="utf-8-sig",
|
|
|
|
|
|
)
|
|
|
|
|
|
(tmp_path / "物料.csv").write_text(
|
|
|
|
|
|
"编码,名称,类型,单位,库存\n"
|
|
|
|
|
|
"P-FOLDER-1,文件夹成品,FINISHED_PRODUCT,件,30\n",
|
|
|
|
|
|
encoding="utf-8-sig",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
class FakePS:
|
|
|
|
|
|
def snapshot(self, include_messages=False):
|
|
|
|
|
|
return {
|
|
|
|
|
|
"projects": [{"id": "p-folder", "name": "锐扬目录项目", "workDir": str(tmp_path)}],
|
|
|
|
|
|
"sessions": [{"id": "s-folder", "projectId": "p-folder"}],
|
|
|
|
|
|
"files": [],
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class FakeStore:
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
|
self.data = empty_world()
|
|
|
|
|
|
self.saved = False
|
|
|
|
|
|
self._ids: dict[str, int] = {}
|
|
|
|
|
|
|
|
|
|
|
|
def next_id(self, kind: str) -> int:
|
|
|
|
|
|
self._ids[kind] = self._ids.get(kind, 0) + 1
|
|
|
|
|
|
return self._ids[kind]
|
|
|
|
|
|
|
|
|
|
|
|
def save(self) -> None:
|
|
|
|
|
|
self.saved = True
|
|
|
|
|
|
|
|
|
|
|
|
identity = IdentityContext(
|
|
|
|
|
|
user_id=77,
|
|
|
|
|
|
username="folder_owner",
|
|
|
|
|
|
fullname="目录项目用户",
|
|
|
|
|
|
tenant_uuid=f"tenant-folder-{uuid.uuid4().hex[:10]}",
|
|
|
|
|
|
roles=("planner",),
|
|
|
|
|
|
)
|
|
|
|
|
|
token = bind_identity(identity)
|
|
|
|
|
|
try:
|
|
|
|
|
|
kb = KnowledgeStore(path=str(tmp_path / "knowledge.json"))
|
|
|
|
|
|
kb.assets = []
|
|
|
|
|
|
kb._write()
|
|
|
|
|
|
monkeypatch.setattr("server.state.projects.get_project_store", lambda: FakePS())
|
|
|
|
|
|
monkeypatch.setattr("server.knowledge.assets.get_knowledge", lambda: kb)
|
|
|
|
|
|
|
|
|
|
|
|
store = FakeStore()
|
|
|
|
|
|
reply = asyncio.run(_run_folder_analyze(store, "s-folder"))
|
|
|
|
|
|
|
|
|
|
|
|
assert store.saved is True
|
|
|
|
|
|
assert any(m.get("code") == "P-FOLDER-1" for m in store.data.get("flexMaterials") or [])
|
|
|
|
|
|
assert any(o.get("orderNo") == "SO-FOLDER-1" for o in store.data.get("flexOrders") or [])
|
|
|
|
|
|
assert store.data["meta"]["ownerUserId"] == 77
|
|
|
|
|
|
assert {b.type for b in reply.blocks} == {"folder-pack", "project-analyze"}
|
|
|
|
|
|
project_block = next(b for b in reply.blocks if b.type == "project-analyze")
|
|
|
|
|
|
assert project_block.props["knowledgeIngest"].get("assetId")
|
|
|
|
|
|
assert kb.assets
|
|
|
|
|
|
finally:
|
|
|
|
|
|
reset_identity(token)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_explicit_csv_maps_annotated_headers_and_preserves_unknown_fields(tmp_path: Path, monkeypatch):
|
|
|
|
|
|
import uuid
|
|
|
|
|
|
|
|
|
|
|
|
from server.aps_domain.project_analyze import analyze_project_deep
|
|
|
|
|
|
from server.auth.context import IdentityContext, bind_identity, reset_identity
|
|
|
|
|
|
from server.knowledge.assets import KnowledgeStore
|
|
|
|
|
|
from server.state.seed import empty_world
|
|
|
|
|
|
|
|
|
|
|
|
path = tmp_path / "供应商杂项字段.csv"
|
|
|
|
|
|
path.write_text(
|
|
|
|
|
|
"物料编码(必填),物料名称*,物料分类,计量单位,当前库存,供应商颜色\n"
|
|
|
|
|
|
"MAT-VARY-001,通用铜排,RAW_MATERIAL,件,18,深绿色\n",
|
|
|
|
|
|
encoding="utf-8-sig",
|
|
|
|
|
|
)
|
|
|
|
|
|
kb = KnowledgeStore(path=str(tmp_path / "vary-knowledge.json"))
|
|
|
|
|
|
kb.assets = []
|
|
|
|
|
|
kb._write()
|
|
|
|
|
|
monkeypatch.setattr("server.aps_domain.project_analyze._project_ctx", lambda _sid: (None, ""))
|
|
|
|
|
|
monkeypatch.setattr("server.knowledge.assets.get_knowledge", lambda: kb)
|
|
|
|
|
|
monkeypatch.setattr("server.knowledge.embedding.index_units", lambda _units: None)
|
|
|
|
|
|
|
|
|
|
|
|
identity = IdentityContext(
|
|
|
|
|
|
user_id=90,
|
|
|
|
|
|
username="vary_owner",
|
|
|
|
|
|
fullname="多字段导入用户",
|
|
|
|
|
|
tenant_uuid=f"tenant-vary-{uuid.uuid4().hex[:10]}",
|
|
|
|
|
|
roles=("planner",),
|
|
|
|
|
|
)
|
|
|
|
|
|
token = bind_identity(identity)
|
|
|
|
|
|
try:
|
|
|
|
|
|
world = empty_world()
|
|
|
|
|
|
result = analyze_project_deep(
|
|
|
|
|
|
world,
|
|
|
|
|
|
"missing-project-session",
|
|
|
|
|
|
query=f'解析 "{path}"',
|
|
|
|
|
|
next_id=lambda _kind: 1,
|
|
|
|
|
|
)
|
|
|
|
|
|
finally:
|
|
|
|
|
|
reset_identity(token)
|
|
|
|
|
|
|
|
|
|
|
|
material = next(row for row in world["flexMaterials"] if row.get("code") == "MAT-VARY-001")
|
|
|
|
|
|
assert material["name"] == "通用铜排"
|
|
|
|
|
|
assert material["unit"] == "件"
|
|
|
|
|
|
assert material["stock"] == 18
|
|
|
|
|
|
assert result["knowledgeIngest"].get("assetId")
|
|
|
|
|
|
raw_text = "\n".join(
|
|
|
|
|
|
chunk.get("text", "")
|
|
|
|
|
|
for asset in kb.assets
|
|
|
|
|
|
for chunk in (asset.get("chunks") or [])
|
|
|
|
|
|
)
|
|
|
|
|
|
assert "供应商颜色" in raw_text
|
|
|
|
|
|
assert "深绿色" in raw_text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_analyze_work_dir_separates_total_errors_from_skipped_rows(tmp_path: Path, monkeypatch):
|
|
|
|
|
|
"""round-63 E-TEST: aggregate diagnostics while preserving legacy fields."""
|
|
|
|
|
|
sheet_routing = "\u0030\u0032-\u5de5\u827a\u6a21\u578b"
|
|
|
|
|
|
sheet_equipment = "\u0030\u0037-\u8bbe\u5907\u6a21\u578b"
|
|
|
|
|
|
sheet_quality = "\u0030\u0033-\u8d28\u68c0\u65b9\u6848"
|
|
|
|
|
|
workbook = tmp_path / "ruiyang-mom.xlsx"
|
|
|
|
|
|
workbook.write_bytes(b"round-63 diagnostics fixture")
|
|
|
|
|
|
|
|
|
|
|
|
class FakePS:
|
|
|
|
|
|
def snapshot(self, include_messages=False):
|
|
|
|
|
|
return {
|
|
|
|
|
|
"projects": [{"id": "p-r63", "name": "Ruiyang", "workDir": str(tmp_path)}],
|
|
|
|
|
|
"sessions": [{"id": "s-r63", "projectId": "p-r63"}],
|
|
|
|
|
|
"files": [],
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
warning_diagnostics = [
|
|
|
|
|
|
{
|
|
|
|
|
|
"sheet": sheet_equipment,
|
|
|
|
|
|
"excelRow": row_no,
|
|
|
|
|
|
"kind": "equipment",
|
|
|
|
|
|
"code": "EQUIPMENT_CAPABILITIES_DEFAULTED",
|
|
|
|
|
|
"severity": "warning",
|
|
|
|
|
|
"message": "Equipment capabilities defaulted to GENERAL",
|
|
|
|
|
|
"rawSummary": {"financialCode": f"EQ-{row_no:03d}", "internalCode": ""},
|
|
|
|
|
|
}
|
|
|
|
|
|
for row_no in range(10, 57)
|
|
|
|
|
|
]
|
|
|
|
|
|
embedded_ignored = [
|
|
|
|
|
|
{
|
|
|
|
|
|
"sheet": sheet_routing,
|
|
|
|
|
|
"excelRow": 5,
|
|
|
|
|
|
"kind": "routing",
|
|
|
|
|
|
"code": "SECONDARY_SEQUENCE_HEADER",
|
|
|
|
|
|
"severity": "ignored",
|
|
|
|
|
|
"message": "Secondary routing sequence header",
|
|
|
|
|
|
"rawSummary": {"routingSequence": "01 / 02 / 03"},
|
|
|
|
|
|
},
|
|
|
|
|
|
*[
|
|
|
|
|
|
{
|
|
|
|
|
|
"sheet": sheet_equipment,
|
|
|
|
|
|
"excelRow": row_no,
|
|
|
|
|
|
"kind": "equipment",
|
|
|
|
|
|
"code": "EMBEDDED_SECTION_ROW",
|
|
|
|
|
|
"severity": "ignored",
|
|
|
|
|
|
"message": "Embedded subsection or dictionary row",
|
|
|
|
|
|
"rawSummary": {"firstCell": "embedded section"},
|
|
|
|
|
|
}
|
|
|
|
|
|
for row_no in (137, 142, 151, 152)
|
|
|
|
|
|
],
|
|
|
|
|
|
]
|
|
|
|
|
|
blocking_diagnostic = {
|
|
|
|
|
|
"sheet": sheet_equipment,
|
|
|
|
|
|
"excelRow": 130,
|
|
|
|
|
|
"kind": "equipment",
|
|
|
|
|
|
"code": "EQUIPMENT_CODE_MISSING",
|
|
|
|
|
|
"severity": "blocking",
|
|
|
|
|
|
"message": "All allowed equipment code fields are empty",
|
|
|
|
|
|
"rawSummary": {
|
|
|
|
|
|
"equipmentName": "real equipment",
|
|
|
|
|
|
"financialCode": "",
|
|
|
|
|
|
"fixedAssetCode": "",
|
|
|
|
|
|
"internalCode": "",
|
|
|
|
|
|
"factoryNo": "FACTORY-NO-MUST-NOT-BECOME-CODE",
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
non_scheduling_diagnostics = [
|
|
|
|
|
|
{
|
|
|
|
|
|
"sheet": sheet_quality,
|
|
|
|
|
|
"excelRow": row_no,
|
|
|
|
|
|
"kind": "materials",
|
|
|
|
|
|
"code": "REQUIRED_FIELDS_MISSING",
|
|
|
|
|
|
"severity": "blocking",
|
|
|
|
|
|
"message": "Folder aggregation must reclassify this non-scheduling row",
|
|
|
|
|
|
"rawSummary": {"inspectionItem": f"item-{row_no}"},
|
|
|
|
|
|
}
|
|
|
|
|
|
for row_no in range(1, 975)
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
def fake_preview_file(*_args, **_kwargs):
|
|
|
|
|
|
diagnostics = [
|
|
|
|
|
|
*warning_diagnostics,
|
|
|
|
|
|
*embedded_ignored,
|
|
|
|
|
|
blocking_diagnostic,
|
|
|
|
|
|
*non_scheduling_diagnostics,
|
|
|
|
|
|
]
|
|
|
|
|
|
return {
|
|
|
|
|
|
"totalOk": 1404,
|
|
|
|
|
|
"totalErrors": 975,
|
|
|
|
|
|
"diagnostics": diagnostics,
|
|
|
|
|
|
"diagnosticCounts": {"ignored": 5, "warning": 47, "blocking": 975},
|
|
|
|
|
|
"batches": [
|
|
|
|
|
|
{
|
|
|
|
|
|
"sheet": sheet_equipment,
|
|
|
|
|
|
"kind": "equipment",
|
|
|
|
|
|
"okCount": 1404,
|
|
|
|
|
|
"errorCount": 0,
|
|
|
|
|
|
"okRows": [{"code": "EQ-R63", "name": "valid equipment", "capabilities": ["GENERAL"]}],
|
|
|
|
|
|
"errors": [],
|
|
|
|
|
|
"warnings": ["Equipment capabilities defaulted to GENERAL"],
|
|
|
|
|
|
"diagnostics": warning_diagnostics,
|
|
|
|
|
|
"diagnosticCounts": {"ignored": 0, "warning": 47, "blocking": 0},
|
|
|
|
|
|
"fieldMap": [],
|
|
|
|
|
|
"headersRaw": ["financialCode", "internalCode", "equipmentName"],
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"sheet": f"{sheet_routing}#diagnostics",
|
|
|
|
|
|
"kind": "routing",
|
|
|
|
|
|
"okCount": 0,
|
|
|
|
|
|
"errorCount": 0,
|
|
|
|
|
|
"okRows": [],
|
|
|
|
|
|
"errors": [],
|
|
|
|
|
|
"warnings": [],
|
|
|
|
|
|
"diagnostics": embedded_ignored[:1],
|
|
|
|
|
|
"diagnosticCounts": {"ignored": 1, "warning": 0, "blocking": 0},
|
|
|
|
|
|
"fieldMap": [],
|
|
|
|
|
|
"headersRaw": ["routingSequence"],
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"sheet": f"{sheet_equipment}#diagnostics",
|
|
|
|
|
|
"kind": "equipment",
|
|
|
|
|
|
"okCount": 0,
|
|
|
|
|
|
"errorCount": 1,
|
|
|
|
|
|
"okRows": [],
|
|
|
|
|
|
"errors": ["row 130: equipment code missing"],
|
|
|
|
|
|
"warnings": [],
|
|
|
|
|
|
"diagnostics": [*embedded_ignored[1:], blocking_diagnostic],
|
|
|
|
|
|
"diagnosticCounts": {"ignored": 4, "warning": 0, "blocking": 1},
|
|
|
|
|
|
"fieldMap": [],
|
|
|
|
|
|
"headersRaw": ["financialCode", "fixedAssetCode", "internalCode", "factoryNo"],
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"sheet": sheet_quality,
|
|
|
|
|
|
"kind": "materials",
|
|
|
|
|
|
"okCount": 0,
|
|
|
|
|
|
"errorCount": 974,
|
|
|
|
|
|
"okRows": [],
|
|
|
|
|
|
"errors": ["non-scheduling block must not count as errors"],
|
|
|
|
|
|
"warnings": [],
|
|
|
|
|
|
"diagnostics": non_scheduling_diagnostics,
|
|
|
|
|
|
"diagnosticCounts": {"ignored": 0, "warning": 0, "blocking": 974},
|
|
|
|
|
|
"fieldMap": [],
|
|
|
|
|
|
"headersRaw": ["inspectionItem"],
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr("server.state.projects.get_project_store", lambda: FakePS())
|
|
|
|
|
|
monkeypatch.setattr("server.aps_domain.folder_pack.preview_file", fake_preview_file)
|
|
|
|
|
|
|
|
|
|
|
|
report = analyze_work_dir(seed_world(), "s-r63")
|
|
|
|
|
|
|
|
|
|
|
|
# Legacy API fields and commit-batch semantics remain unchanged.
|
|
|
|
|
|
assert report["totalOk"] == 1404
|
|
|
|
|
|
assert report["totalErrors"] == 1
|
|
|
|
|
|
assert report["skippedRows"] == 974
|
|
|
|
|
|
assert isinstance(report["batches"], list)
|
|
|
|
|
|
assert all(batch.get("okRows") for batch in report["batches"])
|
|
|
|
|
|
assert len(report["batches"]) == 1
|
|
|
|
|
|
assert report["batches"][0]["okRows"][0]["code"] == "EQ-R63"
|
|
|
|
|
|
|
|
|
|
|
|
# Warning and ignored diagnostics never inflate the blocking-only error count.
|
|
|
|
|
|
assert report["diagnosticCounts"] == {"ignored": 979, "warning": 47, "blocking": 1}
|
|
|
|
|
|
diagnostics = report["diagnostics"]
|
|
|
|
|
|
assert sum(d["severity"] == "warning" for d in diagnostics) == 47
|
|
|
|
|
|
assert sum(d["severity"] == "ignored" for d in diagnostics) == 979
|
|
|
|
|
|
assert sum(d["severity"] == "blocking" for d in diagnostics) == 1
|
|
|
|
|
|
|
|
|
|
|
|
# Diagnostics from zero-okRows batches remain visible at the report top level.
|
|
|
|
|
|
assert any(
|
|
|
|
|
|
d["sheet"] == sheet_routing
|
|
|
|
|
|
and d["excelRow"] == 5
|
|
|
|
|
|
and d["code"] == "SECONDARY_SEQUENCE_HEADER"
|
|
|
|
|
|
and d["severity"] == "ignored"
|
|
|
|
|
|
for d in diagnostics
|
|
|
|
|
|
)
|
|
|
|
|
|
for row_no in (137, 142, 151, 152):
|
|
|
|
|
|
assert any(
|
|
|
|
|
|
d["sheet"] == sheet_equipment
|
|
|
|
|
|
and d["excelRow"] == row_no
|
|
|
|
|
|
and d["code"] == "EMBEDDED_SECTION_ROW"
|
|
|
|
|
|
and d["severity"] == "ignored"
|
|
|
|
|
|
for d in diagnostics
|
|
|
|
|
|
)
|
|
|
|
|
|
blocking = [d for d in diagnostics if d["severity"] == "blocking"]
|
|
|
|
|
|
assert [(d["sheet"], d["excelRow"], d["code"]) for d in blocking] == [
|
|
|
|
|
|
(sheet_equipment, 130, "EQUIPMENT_CODE_MISSING")
|
|
|
|
|
|
]
|
|
|
|
|
|
assert blocking[0]["rawSummary"]["factoryNo"] == "FACTORY-NO-MUST-NOT-BECOME-CODE"
|
|
|
|
|
|
|
|
|
|
|
|
# Legacy skippedRows remains exclusive to the complete non-scheduling block.
|
|
|
|
|
|
skipped = [d for d in diagnostics if d["code"] == "NON_SCHEDULING_BLOCK"]
|
|
|
|
|
|
assert len(skipped) == 974
|
|
|
|
|
|
assert all(d["severity"] == "ignored" and d["sheet"] == sheet_quality for d in skipped)
|
|
|
|
|
|
skipped_sheet = next(sheet for sheet in report["files"][0]["sheets"] if sheet["sheet"] == sheet_quality)
|
|
|
|
|
|
assert skipped_sheet["skip"] is True
|
|
|
|
|
|
assert skipped_sheet["errorCount"] == 0
|
|
|
|
|
|
assert report["files"][0]["errorCount"] == 1
|
|
|
|
|
|
assert report["files"][0]["skipRows"] == 974
|
|
|
|
|
|
assert "\u53e6\u6709 974 \u884c\u6765\u81ea\u975e\u6392\u4ea7\u5757" in report["markdown"]
|
|
|
|
|
|
assert "\u5df2\u8df3\u8fc7\uff0c\u4e0d\u8ba1\u5165\u95ee\u9898\u884c" in report["markdown"]
|
2026-08-11 19:01:05 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_analyze_work_dir_excludes_ok_rows_from_non_scheduling_blocks(tmp_path: Path, monkeypatch):
|
|
|
|
|
|
"""带有效行的非排产 sheet 也不能进 batches/kindCounts(锐扬精简演示 xlsx)。"""
|
|
|
|
|
|
workbook = tmp_path / "ruiyang.xlsx"
|
|
|
|
|
|
workbook.write_bytes(b"folder-skip fixture")
|
|
|
|
|
|
|
|
|
|
|
|
class FakePS:
|
|
|
|
|
|
def snapshot(self, include_messages=False):
|
|
|
|
|
|
return {
|
|
|
|
|
|
"projects": [{"id": "p-skip", "name": "Ruiyang", "workDir": str(tmp_path)}],
|
|
|
|
|
|
"sessions": [{"id": "s-skip", "projectId": "p-skip"}],
|
|
|
|
|
|
"files": [],
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def fake_preview_file(*_args, **_kwargs):
|
|
|
|
|
|
return {
|
|
|
|
|
|
"totalOk": 4,
|
|
|
|
|
|
"totalErrors": 0,
|
|
|
|
|
|
"diagnostics": [],
|
|
|
|
|
|
"diagnosticCounts": {"ignored": 0, "warning": 0, "blocking": 0},
|
|
|
|
|
|
"batches": [
|
|
|
|
|
|
{
|
|
|
|
|
|
"sheet": "工厂资源", "kind": "materials", "okCount": 2, "errorCount": 0,
|
|
|
|
|
|
"okRows": [
|
|
|
|
|
|
{"code": "BU4", "name": "工厂", "type": "RAW_MATERIAL"},
|
|
|
|
|
|
{"code": "BJCJ", "name": "车间", "type": "RAW_MATERIAL"},
|
|
|
|
|
|
],
|
|
|
|
|
|
"errors": [], "warnings": [], "diagnostics": [],
|
|
|
|
|
|
"diagnosticCounts": {"ignored": 0, "warning": 0, "blocking": 0},
|
|
|
|
|
|
"fieldMap": [], "headersRaw": ["资源类型", "编码", "名称"],
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"sheet": "设备", "kind": "equipment", "okCount": 1, "errorCount": 0,
|
|
|
|
|
|
"okRows": [{"code": "EQ-1", "name": "切割机", "capabilities": ["CUT"], "status": "RUNNING"}],
|
|
|
|
|
|
"errors": [], "warnings": [], "diagnostics": [],
|
|
|
|
|
|
"diagnosticCounts": {"ignored": 0, "warning": 0, "blocking": 0},
|
|
|
|
|
|
"fieldMap": [], "headersRaw": ["设备编码", "设备名称", "能力"],
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"sheet": "销售订单", "kind": "orders", "okCount": 1, "errorCount": 0,
|
|
|
|
|
|
"okRows": [{"orderNo": "SO-1", "productCode": "P-1", "quantity": 1,
|
|
|
|
|
|
"deliveryDate": "2026-08-01"}],
|
|
|
|
|
|
"errors": [], "warnings": [], "diagnostics": [],
|
|
|
|
|
|
"diagnosticCounts": {"ignored": 0, "warning": 0, "blocking": 0},
|
|
|
|
|
|
"fieldMap": [], "headersRaw": ["订单号", "产品编码", "数量", "交期"],
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr("server.state.projects.get_project_store", lambda: FakePS())
|
|
|
|
|
|
monkeypatch.setattr("server.aps_domain.folder_pack.preview_file", fake_preview_file)
|
|
|
|
|
|
report = analyze_work_dir(seed_world(), "s-skip")
|
|
|
|
|
|
|
|
|
|
|
|
assert report["totalOk"] == 2
|
|
|
|
|
|
assert report["skippedRows"] == 2
|
|
|
|
|
|
assert report["kindCounts"].get("materials", 0) == 0
|
|
|
|
|
|
assert all(batch.get("sheet") != "工厂资源" for batch in report["batches"])
|
|
|
|
|
|
assert [batch["kind"] for batch in report["batches"]] == ["equipment", "orders"]
|