feat: add Fujian M5 match and source-priority fixtures
Offline confirmation for 哈药牌钙铁锌口服液 uses recorded GOODSID 4000109983 and PRIORITY=1 source rows. M5 host/SID are configured; credentials stay local.
This commit is contained in:
parent
63eec40334
commit
29df92ee17
|
|
@ -33,6 +33,12 @@ rpa-chrome-profile/
|
||||||
# Duplicate / scratch
|
# Duplicate / scratch
|
||||||
state_center_副本/
|
state_center_副本/
|
||||||
|
|
||||||
|
# Local M5 credentials
|
||||||
|
state_center/fixtures/m5/connection.local.json
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
# Media (keep docs, drop recordings and extracted frames)
|
# Media (keep docs, drop recordings and extracted frames)
|
||||||
*.mp4
|
*.mp4
|
||||||
video_frames_v2/
|
video_frames_v2/
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,34 @@ def run(node_code: str, job: dict[str, Any], client: Any) -> dict[str, Any]:
|
||||||
|
|
||||||
|
|
||||||
def run_order_fetch(job: dict[str, Any], client: Any) -> dict[str, Any]:
|
def run_order_fetch(job: dict[str, Any], client: Any) -> dict[str, Any]:
|
||||||
|
from pathlib import Path
|
||||||
|
import json
|
||||||
|
|
||||||
|
fixture = (
|
||||||
|
Path(__file__).resolve().parents[2]
|
||||||
|
/ "state_center"
|
||||||
|
/ "fixtures"
|
||||||
|
/ "orders"
|
||||||
|
/ "yyc-fujian-20260804.json"
|
||||||
|
)
|
||||||
order_date = str(job.get("order_date") or "2026-08-12")
|
order_date = str(job.get("order_date") or "2026-08-12")
|
||||||
|
if fixture.is_file():
|
||||||
|
data = json.loads(fixture.read_text(encoding="utf-8"))
|
||||||
|
row = data["row"]
|
||||||
|
order_no = row.get("orderNo") or f"DD-{order_date.replace('-', '')}-001"
|
||||||
|
rows = [
|
||||||
|
{
|
||||||
|
"orderNo": order_no,
|
||||||
|
"rawProductName": row["rawProductName"],
|
||||||
|
"platformProductCode": row.get("platformProductCode"),
|
||||||
|
"spec": row.get("spec"),
|
||||||
|
"manufacturer": row.get("manufacturer"),
|
||||||
|
"approvalNo": row.get("approvalNo"),
|
||||||
|
"qty": row.get("qty") or 1,
|
||||||
|
"sourceRowNo": row.get("sourceRowNo"),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
else:
|
||||||
order_no = f"DD-{order_date.replace('-', '')}-001"
|
order_no = f"DD-{order_date.replace('-', '')}-001"
|
||||||
rows = [
|
rows = [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,14 @@ class Settings:
|
||||||
ORACLE_USER = os.getenv("ORACLE_USER", "")
|
ORACLE_USER = os.getenv("ORACLE_USER", "")
|
||||||
ORACLE_PASSWORD = os.getenv("ORACLE_PASSWORD", "")
|
ORACLE_PASSWORD = os.getenv("ORACLE_PASSWORD", "")
|
||||||
|
|
||||||
|
# M5 货品视图(福建货主对码)。账号密码只走环境变量,不进仓库。
|
||||||
|
M5_HOST = os.getenv("M5_HOST", "173.1.1.215")
|
||||||
|
M5_PORT = int(os.getenv("M5_PORT", "1521"))
|
||||||
|
M5_SERVICE = os.getenv("M5_SERVICE", "m5")
|
||||||
|
M5_USER = os.getenv("M5_USER", "")
|
||||||
|
M5_PASSWORD = os.getenv("M5_PASSWORD", "")
|
||||||
|
M5_SCHEMA = os.getenv("M5_SCHEMA", "m5")
|
||||||
|
|
||||||
# Oracle 货品主数据表(真实表名/列名拿到后改这里,先按设计文档 A~G 列占位)
|
# Oracle 货品主数据表(真实表名/列名拿到后改这里,先按设计文档 A~G 列占位)
|
||||||
ORACLE_GOODS_TABLE = os.getenv("ORACLE_GOODS_TABLE", "GOODS_MASTER")
|
ORACLE_GOODS_TABLE = os.getenv("ORACLE_GOODS_TABLE", "GOODS_MASTER")
|
||||||
ORACLE_COL_COMMON_NAME = os.getenv("ORACLE_COL_COMMON_NAME", "COMMON_NAME") # 通用名
|
ORACLE_COL_COMMON_NAME = os.getenv("ORACLE_COL_COMMON_NAME", "COMMON_NAME") # 通用名
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,174 @@
|
||||||
|
"""M5 货品视图只读查询。账号密码来自环境变量或 connection.local.json。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from .config import settings
|
||||||
|
|
||||||
|
FIXTURE_DIR = Path(__file__).resolve().parents[1] / "fixtures"
|
||||||
|
M5_DIR = FIXTURE_DIR / "m5"
|
||||||
|
ORDER_PATH = FIXTURE_DIR / "orders" / "yyc-fujian-20260804.json"
|
||||||
|
FUJIAN_PATH = M5_DIR / "fujian.json"
|
||||||
|
LOCAL_CONN_PATH = M5_DIR / "connection.local.json"
|
||||||
|
|
||||||
|
|
||||||
|
def _load_json(path: Path) -> dict[str, Any]:
|
||||||
|
return json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def load_connection() -> dict[str, Any]:
|
||||||
|
local: dict[str, Any] = {}
|
||||||
|
if LOCAL_CONN_PATH.is_file():
|
||||||
|
local = _load_json(LOCAL_CONN_PATH)
|
||||||
|
host = os.getenv("M5_HOST") or local.get("host") or settings.M5_HOST
|
||||||
|
port = int(os.getenv("M5_PORT") or local.get("port") or settings.M5_PORT)
|
||||||
|
service = os.getenv("M5_SERVICE") or local.get("serviceName") or settings.M5_SERVICE
|
||||||
|
user = os.getenv("M5_USER") or local.get("user") or settings.M5_USER
|
||||||
|
password = os.getenv("M5_PASSWORD") or local.get("password") or settings.M5_PASSWORD
|
||||||
|
schema = os.getenv("M5_SCHEMA") or local.get("schema") or settings.M5_SCHEMA
|
||||||
|
return {
|
||||||
|
"host": host,
|
||||||
|
"port": port,
|
||||||
|
"serviceName": service,
|
||||||
|
"user": user,
|
||||||
|
"password": password,
|
||||||
|
"schema": schema,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def recorded_source_rows(goods_id: str) -> list[dict[str, Any]] | None:
|
||||||
|
"""离线夹具:哈药牌这条 SQL 的 P1 结果,不连 M5。"""
|
||||||
|
params = lookup_params()
|
||||||
|
if str(goods_id) != params["expectedGoodsId"]:
|
||||||
|
return None
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"ENTRYID": params["entryId"],
|
||||||
|
"PRIORITY": 1,
|
||||||
|
"GOODSTYPE": params["spec"],
|
||||||
|
"FACTORYNAME": params["manufacturer"],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate_priority_source(
|
||||||
|
rows: list[dict[str, Any]],
|
||||||
|
*,
|
||||||
|
entry_id: int,
|
||||||
|
spec: str,
|
||||||
|
manufacturer: str,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""PRIORITY=1 且货主/规格/厂家对上,才算货源齐全。"""
|
||||||
|
priority_rows = [row for row in rows if str(row.get("PRIORITY") or "") == "1"]
|
||||||
|
hit = next(
|
||||||
|
(
|
||||||
|
row
|
||||||
|
for row in priority_rows
|
||||||
|
if str(row.get("ENTRYID") or "") == str(entry_id)
|
||||||
|
and str(row.get("GOODSTYPE") or "").strip() == spec.strip()
|
||||||
|
and str(row.get("FACTORYNAME") or "").strip() == manufacturer.strip()
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"source_add_needed": hit is None,
|
||||||
|
"hit": hit,
|
||||||
|
"priorityRows": priority_rows,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def lookup_params(order: dict[str, Any] | None = None, fujian: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||||
|
order = order or _load_json(ORDER_PATH)
|
||||||
|
fujian = fujian or _load_json(FUJIAN_PATH)
|
||||||
|
row = order["row"]
|
||||||
|
return {
|
||||||
|
"goodsNameLike": f"%{row['goodsNameLike']}%",
|
||||||
|
"approvalNo": row["approvalNo"],
|
||||||
|
"spec": row["spec"],
|
||||||
|
"manufacturer": row["manufacturer"],
|
||||||
|
"entryId": int(order["m5"]["entryId"] or fujian["entryId"]),
|
||||||
|
"expectedGoodsId": str(row["expectedGoodsId"]),
|
||||||
|
"sql": fujian["sql"],
|
||||||
|
"sourceSql": fujian["sourceSql"],
|
||||||
|
"expectedSourceAddNeeded": bool(row.get("expectedSourceAddNeeded", True)),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def connect(conn_cfg: dict[str, Any] | None = None):
|
||||||
|
cfg = conn_cfg or load_connection()
|
||||||
|
missing = [key for key in ("user", "password") if not cfg.get(key)]
|
||||||
|
if missing:
|
||||||
|
raise RuntimeError(
|
||||||
|
"M5 连接缺少 " + ", ".join(missing) + "。设 M5_USER/M5_PASSWORD,或写 fixtures/m5/connection.local.json"
|
||||||
|
)
|
||||||
|
import oracledb
|
||||||
|
|
||||||
|
dsn = f"{cfg['host']}:{cfg['port']}/{cfg['serviceName']}"
|
||||||
|
return oracledb.connect(user=cfg["user"], password=cfg["password"], dsn=dsn)
|
||||||
|
|
||||||
|
|
||||||
|
def lookup_one(conn_cfg: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||||
|
params = lookup_params()
|
||||||
|
conn = connect(conn_cfg)
|
||||||
|
try:
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute(
|
||||||
|
params["sql"],
|
||||||
|
goodsNameLike=params["goodsNameLike"],
|
||||||
|
approvalNo=params["approvalNo"],
|
||||||
|
spec=params["spec"],
|
||||||
|
manufacturer=params["manufacturer"],
|
||||||
|
entryId=params["entryId"],
|
||||||
|
)
|
||||||
|
rows = _rows_from_cursor(cur)
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
goods_ids = [str(row.get("GOODSID") or "") for row in rows]
|
||||||
|
return {
|
||||||
|
"ok": len(rows) == 1 and goods_ids[0] == params["expectedGoodsId"],
|
||||||
|
"expectedGoodsId": params["expectedGoodsId"],
|
||||||
|
"hits": len(rows),
|
||||||
|
"goodsIds": goods_ids,
|
||||||
|
"rows": rows,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _rows_from_cursor(cur) -> list[dict[str, Any]]:
|
||||||
|
columns = [col[0] for col in cur.description]
|
||||||
|
return [dict(zip(columns, row)) for row in cur.fetchall()]
|
||||||
|
|
||||||
|
|
||||||
|
def query_priority_source(goods_id: str, conn_cfg: dict[str, Any] | None = None) -> list[dict[str, Any]]:
|
||||||
|
fujian = _load_json(FUJIAN_PATH)
|
||||||
|
conn = connect(conn_cfg)
|
||||||
|
try:
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute(fujian["sourceSql"], goodsId=int(goods_id))
|
||||||
|
return _rows_from_cursor(cur)
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def check_priority_source(conn_cfg: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||||
|
params = lookup_params()
|
||||||
|
rows = query_priority_source(params["expectedGoodsId"], conn_cfg)
|
||||||
|
judged = evaluate_priority_source(
|
||||||
|
rows,
|
||||||
|
entry_id=params["entryId"],
|
||||||
|
spec=params["spec"],
|
||||||
|
manufacturer=params["manufacturer"],
|
||||||
|
)
|
||||||
|
judged["ok"] = judged["source_add_needed"] == params["expectedSourceAddNeeded"]
|
||||||
|
judged["expectedSourceAddNeeded"] = params["expectedSourceAddNeeded"]
|
||||||
|
judged["rows"] = rows
|
||||||
|
return judged
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
match = lookup_one()
|
||||||
|
source = check_priority_source() if match.get("ok") else {"skipped": True, "reason": "对码未命中,不查货源"}
|
||||||
|
print(json.dumps({"match": match, "source": source}, ensure_ascii=False, default=str, indent=2))
|
||||||
|
|
@ -353,9 +353,51 @@ def _supplier_has_source(cur, supplier_name: str, goods_id: str, current_job_id:
|
||||||
return cur.fetchone() is not None
|
return cur.fetchone() is not None
|
||||||
|
|
||||||
|
|
||||||
def _source_add_needed(cur, job: dict, goods_id: Optional[str]) -> int:
|
def _source_add_needed(
|
||||||
|
cur,
|
||||||
|
job: dict,
|
||||||
|
goods_id: Optional[str],
|
||||||
|
spec: str = "",
|
||||||
|
manufacturer: str = "",
|
||||||
|
) -> int:
|
||||||
if not goods_id:
|
if not goods_id:
|
||||||
return 0
|
return 0
|
||||||
|
from .config import settings
|
||||||
|
from .m5_client import (
|
||||||
|
FUJIAN_PATH,
|
||||||
|
_load_json,
|
||||||
|
evaluate_priority_source,
|
||||||
|
load_connection,
|
||||||
|
query_priority_source,
|
||||||
|
recorded_source_rows,
|
||||||
|
)
|
||||||
|
|
||||||
|
if spec and manufacturer:
|
||||||
|
recorded = recorded_source_rows(str(goods_id))
|
||||||
|
if recorded is not None and settings.DEV_MODE:
|
||||||
|
entry_id = int(_load_json(FUJIAN_PATH)["entryId"])
|
||||||
|
judged = evaluate_priority_source(
|
||||||
|
recorded,
|
||||||
|
entry_id=entry_id,
|
||||||
|
spec=spec,
|
||||||
|
manufacturer=manufacturer,
|
||||||
|
)
|
||||||
|
return 1 if judged["source_add_needed"] else 0
|
||||||
|
|
||||||
|
cfg = load_connection()
|
||||||
|
if cfg.get("user") and cfg.get("password") and spec and manufacturer:
|
||||||
|
try:
|
||||||
|
entry_id = int(_load_json(FUJIAN_PATH)["entryId"])
|
||||||
|
rows = query_priority_source(str(goods_id), cfg)
|
||||||
|
judged = evaluate_priority_source(
|
||||||
|
rows,
|
||||||
|
entry_id=entry_id,
|
||||||
|
spec=spec,
|
||||||
|
manufacturer=manufacturer,
|
||||||
|
)
|
||||||
|
return 1 if judged["source_add_needed"] else 0
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
return 0 if _supplier_has_source(cur, job["supplier_name"], goods_id, job["job_id"]) else 1
|
return 0 if _supplier_has_source(cur, job["supplier_name"], goods_id, job["job_id"]) else 1
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -934,6 +976,15 @@ def load_fixture_goods(x_agent_token: Optional[str] = Header(default=None)):
|
||||||
GoodsRecord("阿卡波糖片", "国药准字H00000001", "50mg*30片", "盒", "示例制药", "G-001", "YP-001"),
|
GoodsRecord("阿卡波糖片", "国药准字H00000001", "50mg*30片", "盒", "示例制药", "G-001", "YP-001"),
|
||||||
GoodsRecord("阿莫西林胶囊", "国药准字H00000002", "0.25g*24粒", "盒", "示例医药", "G-002", "YP-002"),
|
GoodsRecord("阿莫西林胶囊", "国药准字H00000002", "0.25g*24粒", "盒", "示例医药", "G-002", "YP-002"),
|
||||||
GoodsRecord("氯化钠注射液", "国药准字H00000003", "100ml", "瓶", "示例药业", "G-003", "YP-003"),
|
GoodsRecord("氯化钠注射液", "国药准字H00000003", "100ml", "瓶", "示例药业", "G-003", "YP-003"),
|
||||||
|
GoodsRecord(
|
||||||
|
"哈药牌钙铁锌口服液",
|
||||||
|
"食健备G201923000558",
|
||||||
|
"10ml*90支",
|
||||||
|
"盒",
|
||||||
|
"哈药集团三精制药有限公司",
|
||||||
|
"4000109983",
|
||||||
|
"4000109983",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
global _ENGINE_LOADED
|
global _ENGINE_LOADED
|
||||||
_ENGINE.load_goods(records)
|
_ENGINE.load_goods(records)
|
||||||
|
|
@ -1003,7 +1054,9 @@ def match_run(job_id: str, x_agent_token: Optional[str] = Header(default=None)):
|
||||||
r = _ENGINE.match_one(it["id"], it["raw_product_name"], it["approval_no"] or "",
|
r = _ENGINE.match_one(it["id"], it["raw_product_name"], it["approval_no"] or "",
|
||||||
it["spec"] or "", it["manufacturer"] or "")
|
it["spec"] or "", it["manufacturer"] or "")
|
||||||
stat[r.match_status] = stat.get(r.match_status, 0) + 1
|
stat[r.match_status] = stat.get(r.match_status, 0) + 1
|
||||||
source_add_needed = _source_add_needed(cur, job, r.goods_id)
|
source_add_needed = _source_add_needed(
|
||||||
|
cur, job, r.goods_id, it.get("spec") or "", it.get("manufacturer") or ""
|
||||||
|
)
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"INSERT INTO product_match(job_id,order_item_id,match_key,match_status,"
|
"INSERT INTO product_match(job_id,order_item_id,match_key,match_status,"
|
||||||
"goods_id,goods_code,base_unit,candidates_json,source_add_needed,created_at)"
|
"goods_id,goods_code,base_unit,candidates_json,source_add_needed,created_at)"
|
||||||
|
|
@ -1094,7 +1147,20 @@ def match_confirm(match_id: int, req: MatchConfirmReq,
|
||||||
raise HTTPException(status_code=404, detail="product_match not found")
|
raise HTTPException(status_code=404, detail="product_match not found")
|
||||||
job_id = pm["job_id"]
|
job_id = pm["job_id"]
|
||||||
job = _load_job(cur, job_id)
|
job = _load_job(cur, job_id)
|
||||||
source_add_needed = _source_add_needed(cur, job, req.goods_id)
|
oi = None
|
||||||
|
if pm.get("order_item_id"):
|
||||||
|
cur.execute(
|
||||||
|
"SELECT spec,manufacturer FROM order_item WHERE id=%s",
|
||||||
|
(pm["order_item_id"],),
|
||||||
|
)
|
||||||
|
oi = cur.fetchone()
|
||||||
|
source_add_needed = _source_add_needed(
|
||||||
|
cur,
|
||||||
|
job,
|
||||||
|
req.goods_id,
|
||||||
|
(oi or {}).get("spec") or "",
|
||||||
|
(oi or {}).get("manufacturer") or "",
|
||||||
|
)
|
||||||
|
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"UPDATE product_match SET match_status='ALIAS',goods_id=%s,goods_code=%s,base_unit=%s,"
|
"UPDATE product_match SET match_status='ALIAS',goods_id=%s,goods_code=%s,base_unit=%s,"
|
||||||
|
|
@ -1188,7 +1254,20 @@ def _submit_candidate_selection(cur, task: dict, job: dict, payload: dict, opera
|
||||||
if not pm or pm.get("job_id") != task["job_id"]:
|
if not pm or pm.get("job_id") != task["job_id"]:
|
||||||
raise HTTPException(status_code=404, detail="对码结果不存在")
|
raise HTTPException(status_code=404, detail="对码结果不存在")
|
||||||
|
|
||||||
source_add_needed = _source_add_needed(cur, job, goods_id)
|
oi = None
|
||||||
|
if pm.get("order_item_id"):
|
||||||
|
cur.execute(
|
||||||
|
"SELECT spec,manufacturer FROM order_item WHERE id=%s",
|
||||||
|
(pm["order_item_id"],),
|
||||||
|
)
|
||||||
|
oi = cur.fetchone()
|
||||||
|
source_add_needed = _source_add_needed(
|
||||||
|
cur,
|
||||||
|
job,
|
||||||
|
goods_id,
|
||||||
|
(oi or {}).get("spec") or "",
|
||||||
|
(oi or {}).get("manufacturer") or "",
|
||||||
|
)
|
||||||
goods_code = payload.get("goodsCode")
|
goods_code = payload.get("goodsCode")
|
||||||
base_unit = payload.get("baseUnit")
|
base_unit = payload.get("baseUnit")
|
||||||
cur.execute(
|
cur.execute(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
# 对码入参底座(一条,福建货主)
|
||||||
|
|
||||||
|
订单明细走这里,不走证据包。本批只有一条:重庆亿昊 / 哈药牌钙铁锌口服液。
|
||||||
|
|
||||||
|
## M5
|
||||||
|
|
||||||
|
- 地址默认 `173.1.1.215:1521`,Service Name `m5`
|
||||||
|
- 账号密码可配,不进 git:环境变量 `M5_USER` `M5_PASSWORD`,或复制 `m5/connection.example.json` → `m5/connection.local.json`
|
||||||
|
- 查询:品名 LIKE + 文号 + 规格 + 厂家 + `ENTRYID=907`
|
||||||
|
- 产地空或「无」不参与过滤
|
||||||
|
- 回写 `goods_id` = `GOODSID`(`4000109983`)
|
||||||
|
- 对码命中后再查货源:`bms_su_supplygoods_doc_v` × `bms_su_supplygoods_dtl_v`,`PRIORITY` 必须是 1,且 `ENTRYID/GOODSTYPE/FACTORYNAME` 对上这条规格。对上则跳过新增货源。
|
||||||
|
|
||||||
|
查这一条:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export M5_USER=...
|
||||||
|
export M5_PASSWORD=...
|
||||||
|
python -m state_center.app.m5_client
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"comment": "Excel 列 → 订单入参。本批只收一条。条码/医保不进 order_item,也不拿去查 M5。账号密码不写在这份文件里。",
|
||||||
|
"sheet": "订单明细",
|
||||||
|
"skipStatuses": ["已取消"],
|
||||||
|
"jobGroupBy": ["supplierName", "orderDate", "sourceType"],
|
||||||
|
"columns": {
|
||||||
|
"父订单号": "parentOrderNo",
|
||||||
|
"商家": "supplierName",
|
||||||
|
"订单状态": "orderStatus",
|
||||||
|
"品名": "rawProductName",
|
||||||
|
"产品编码": "platformProductCode",
|
||||||
|
"产品条码": "barcode",
|
||||||
|
"医保编码": "insuranceCode",
|
||||||
|
"批准文号": "approvalNo"
|
||||||
|
},
|
||||||
|
"orderItem": [
|
||||||
|
"orderNo",
|
||||||
|
"rawProductName",
|
||||||
|
"platformProductCode",
|
||||||
|
"spec",
|
||||||
|
"manufacturer",
|
||||||
|
"approvalNo",
|
||||||
|
"qty",
|
||||||
|
"sourceRowNo"
|
||||||
|
],
|
||||||
|
"m5Lookup": {
|
||||||
|
"view": "PUB_ENTRY_GOODS_V",
|
||||||
|
"goodsIdField": "GOODSID",
|
||||||
|
"filters": {
|
||||||
|
"GOODSNAME": { "from": "rawProductName", "op": "like" },
|
||||||
|
"APPROVEDOCNO": { "from": "approvalNo", "op": "eq" },
|
||||||
|
"GOODSTYPE": { "from": "spec", "op": "eq" },
|
||||||
|
"FACTORYNAME": { "from": "manufacturer", "op": "eq" },
|
||||||
|
"ENTRYID": { "from": "entryId", "op": "eq" }
|
||||||
|
},
|
||||||
|
"optionalFilters": {
|
||||||
|
"PRODAREA": { "from": "prodArea", "skipValues": ["", "无", null] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"host": "173.1.1.215",
|
||||||
|
"port": 1521,
|
||||||
|
"serviceName": "m5",
|
||||||
|
"user": "",
|
||||||
|
"password": "",
|
||||||
|
"schema": "m5",
|
||||||
|
"comment": "复制为 connection.local.json 填账号密码。local 文件不进 git。也可用环境变量 M5_USER / M5_PASSWORD / M5_SERVICE。"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"entryName": "福建",
|
||||||
|
"entryId": 907,
|
||||||
|
"host": "173.1.1.215",
|
||||||
|
"port": 1521,
|
||||||
|
"serviceName": "m5",
|
||||||
|
"schema": "m5",
|
||||||
|
"view": "PUB_ENTRY_GOODS_V",
|
||||||
|
"goodsIdField": "GOODSID",
|
||||||
|
"requireProdArea": false,
|
||||||
|
"sql": "SELECT t.FACTORYNAME, t.APPROVEDOCNO, t.GOODSTYPE, t.PRODAREA, t.ENTRYID, t.GOODSID, t.GOODSNAME, t.GOODSUNIT, t.ENTRYGOODSID FROM PUB_ENTRY_GOODS_V t WHERE t.GOODSNAME LIKE :goodsNameLike AND t.APPROVEDOCNO = :approvalNo AND t.GOODSTYPE = :spec AND t.FACTORYNAME = :manufacturer AND t.ENTRYID = :entryId",
|
||||||
|
"sourceSql": "SELECT t.ENTRYID, a.PRIORITY, t.GOODSTYPE, t.FACTORYNAME FROM bms_su_supplygoods_doc_v t, bms_su_supplygoods_dtl_v a WHERE t.GOODSID = :goodsId AND a.PRIORITY = 1 AND t.SUPPLYGOODSDOCID = a.SUPPLYGOODSDOCID"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"sourceType": "YYC",
|
||||||
|
"orderDate": "2026-08-04",
|
||||||
|
"supplierName": "重庆亿昊药业有限公司",
|
||||||
|
"m5": {
|
||||||
|
"entryName": "福建",
|
||||||
|
"entryId": 907
|
||||||
|
},
|
||||||
|
"row": {
|
||||||
|
"orderNo": "YYC-ROW-2",
|
||||||
|
"parentOrderNo": "",
|
||||||
|
"rawProductName": "哈药牌钙铁锌口服液",
|
||||||
|
"goodsNameLike": "哈药牌",
|
||||||
|
"platformProductCode": "2919482534",
|
||||||
|
"barcode": "6938780031263",
|
||||||
|
"approvalNo": "食健备G201923000558",
|
||||||
|
"spec": "10ml*90支",
|
||||||
|
"manufacturer": "哈药集团三精制药有限公司",
|
||||||
|
"prodArea": "无",
|
||||||
|
"qty": 83,
|
||||||
|
"orderStatus": "待付款",
|
||||||
|
"sourceRowNo": 2,
|
||||||
|
"expectedGoodsId": "4000109983",
|
||||||
|
"expectedSourceAddNeeded": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
from state_center.app.m5_client import (
|
||||||
|
evaluate_priority_source,
|
||||||
|
load_connection,
|
||||||
|
lookup_params,
|
||||||
|
recorded_source_rows,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_lookup_params_use_the_single_fujian_row():
|
||||||
|
params = lookup_params()
|
||||||
|
assert params["goodsNameLike"] == "%哈药牌%"
|
||||||
|
assert params["approvalNo"] == "食健备G201923000558"
|
||||||
|
assert params["spec"] == "10ml*90支"
|
||||||
|
assert params["manufacturer"] == "哈药集团三精制药有限公司"
|
||||||
|
assert params["entryId"] == 907
|
||||||
|
assert params["expectedGoodsId"] == "4000109983"
|
||||||
|
assert "PUB_ENTRY_GOODS_V" in params["sql"]
|
||||||
|
assert "PRODAREA" not in params["sql"].split("WHERE", 1)[1]
|
||||||
|
|
||||||
|
|
||||||
|
def test_m5_host_defaults_and_password_stays_out_of_repo(monkeypatch):
|
||||||
|
monkeypatch.delenv("M5_USER", raising=False)
|
||||||
|
monkeypatch.delenv("M5_PASSWORD", raising=False)
|
||||||
|
monkeypatch.delenv("M5_SERVICE", raising=False)
|
||||||
|
cfg = load_connection()
|
||||||
|
assert cfg["host"] == "173.1.1.215"
|
||||||
|
assert cfg["port"] == 1521
|
||||||
|
assert cfg["serviceName"] == "m5"
|
||||||
|
assert cfg["password"] == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_priority_1_matching_spec_skips_source_add():
|
||||||
|
judged = evaluate_priority_source(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"ENTRYID": 907,
|
||||||
|
"PRIORITY": 1,
|
||||||
|
"GOODSTYPE": "10ml*90支",
|
||||||
|
"FACTORYNAME": "哈药集团三精制药有限公司",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
entry_id=907,
|
||||||
|
spec="10ml*90支",
|
||||||
|
manufacturer="哈药集团三精制药有限公司",
|
||||||
|
)
|
||||||
|
assert judged["source_add_needed"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_priority_1_other_spec_needs_source_add():
|
||||||
|
judged = evaluate_priority_source(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"ENTRYID": 907,
|
||||||
|
"PRIORITY": 1,
|
||||||
|
"GOODSTYPE": "10ml*10支",
|
||||||
|
"FACTORYNAME": "哈药集团三精制药有限公司",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
entry_id=907,
|
||||||
|
spec="10ml*90支",
|
||||||
|
manufacturer="哈药集团三精制药有限公司",
|
||||||
|
)
|
||||||
|
assert judged["source_add_needed"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_lookup_params_include_source_sql():
|
||||||
|
params = lookup_params()
|
||||||
|
assert "bms_su_supplygoods_doc_v" in params["sourceSql"]
|
||||||
|
assert params["expectedSourceAddNeeded"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_recorded_source_rows_cover_hayao_without_m5():
|
||||||
|
rows = recorded_source_rows("4000109983")
|
||||||
|
assert rows is not None
|
||||||
|
judged = evaluate_priority_source(
|
||||||
|
rows,
|
||||||
|
entry_id=907,
|
||||||
|
spec="10ml*90支",
|
||||||
|
manufacturer="哈药集团三精制药有限公司",
|
||||||
|
)
|
||||||
|
assert judged["source_add_needed"] is False
|
||||||
|
assert recorded_source_rows("other") is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_hayao_priority1_sql_row_skips_source_add():
|
||||||
|
"""第二条 SQL 的真实结果:907 / 1 / 10ml*90支 / 哈药集团三精制药有限公司。"""
|
||||||
|
judged = evaluate_priority_source(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"ENTRYID": 907,
|
||||||
|
"PRIORITY": 1,
|
||||||
|
"GOODSTYPE": "10ml*90支",
|
||||||
|
"FACTORYNAME": "哈药集团三精制药有限公司",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
entry_id=907,
|
||||||
|
spec="10ml*90支",
|
||||||
|
manufacturer="哈药集团三精制药有限公司",
|
||||||
|
)
|
||||||
|
assert judged["source_add_needed"] is False
|
||||||
|
assert judged["hit"]["ENTRYID"] == 907
|
||||||
|
|
@ -50,6 +50,32 @@ def test_none():
|
||||||
assert r.match_status == "NONE"
|
assert r.match_status == "NONE"
|
||||||
|
|
||||||
|
|
||||||
|
def test_fujian_hayao_exact_from_m5_row():
|
||||||
|
eng = MatchEngine()
|
||||||
|
eng.load_goods(
|
||||||
|
[
|
||||||
|
GoodsRecord(
|
||||||
|
"哈药牌钙铁锌口服液",
|
||||||
|
"食健备G201923000558",
|
||||||
|
"10ml*90支",
|
||||||
|
"盒",
|
||||||
|
"哈药集团三精制药有限公司",
|
||||||
|
"4000109983",
|
||||||
|
"4000109983",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
r = eng.match_one(
|
||||||
|
1,
|
||||||
|
"哈药牌钙铁锌口服液",
|
||||||
|
"食健备G201923000558",
|
||||||
|
"10ml*90支",
|
||||||
|
"哈药集团三精制药有限公司",
|
||||||
|
)
|
||||||
|
assert r.match_status == "EXACT"
|
||||||
|
assert r.goods_id == "4000109983"
|
||||||
|
|
||||||
|
|
||||||
def test_alias_priority():
|
def test_alias_priority():
|
||||||
eng = _engine()
|
eng = _engine()
|
||||||
key = build_match_key("阿卡波糖", "", "30片", "") # 平台奇葩写法
|
key = build_match_key("阿卡波糖", "", "30片", "") # 平台奇葩写法
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue