提交
This commit is contained in:
1 parent
c3bd2c00ae
commit
a52fe77e34
39 files changed
+609647
-9238
No files matched your search
@@ -0,0 +1 @@
|
||||
41606
|
||||
@@ -1,6 +1,10 @@
|
||||
import io
|
||||
from datetime import date as date_cls
|
||||
from typing import List, Optional
|
||||
from urllib.parse import quote
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy import or_
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -21,6 +25,13 @@ from app.schemas.bx.reimburse import (
|
||||
)
|
||||
from app.services.bx.expense_item import ExpenseItemService
|
||||
from app.services.bx.reimburse import ReimburseService
|
||||
from app.services.bx.reimburse_import import (
|
||||
build_applicant_label,
|
||||
build_import_template_bytes,
|
||||
import_reimburse_workbook,
|
||||
load_applicant_options,
|
||||
load_expense_nodes_flat,
|
||||
)
|
||||
from app.utils.bx_codegen import generate_apply_code, generate_title, parse_optional_date
|
||||
from app.utils.bx_date_range import append_create_time_filters
|
||||
|
||||
@@ -84,6 +95,54 @@ def get_generated_title(
|
||||
return ApiResponse(data=title)
|
||||
|
||||
|
||||
@router.get("/import-template")
|
||||
def download_reimburse_import_template(
|
||||
db: Session = Depends(get_db),
|
||||
user: BxUserContext = Depends(get_bx_current_user),
|
||||
):
|
||||
tenant_no = _tenant(db, user)
|
||||
nodes = load_expense_nodes_flat(db, tenant_no)
|
||||
applicants = load_applicant_options(db, user=user, tenant_no=tenant_no)
|
||||
me = next((a for a in applicants if a["id"] == user.id), None)
|
||||
if me:
|
||||
cur_label = build_applicant_label(me["nick_name"], me["user_no"], me["id"])
|
||||
else:
|
||||
cur_label = build_applicant_label(user.name or "", "", user.id)
|
||||
applicants = [
|
||||
{"id": user.id, "nick_name": user.name or "", "user_no": ""}
|
||||
] + applicants
|
||||
raw = build_import_template_bytes(
|
||||
expense_nodes=nodes,
|
||||
applicants=applicants,
|
||||
current_applicant_label=cur_label,
|
||||
today=date_cls.today(),
|
||||
)
|
||||
filename = quote("报销申请导入模板.xlsx")
|
||||
return StreamingResponse(
|
||||
io.BytesIO(raw),
|
||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
headers={
|
||||
"Content-Disposition": f"attachment; filename={filename}; filename*=UTF-8''{filename}"
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/import", response_model=ApiResponse[dict])
|
||||
def import_reimburses(
|
||||
file: UploadFile = File(...),
|
||||
db: Session = Depends(get_db),
|
||||
user: BxUserContext = Depends(get_bx_current_user),
|
||||
):
|
||||
content = file.file.read()
|
||||
if not content:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="文件为空")
|
||||
try:
|
||||
result = import_reimburse_workbook(db, file_bytes=content, user=user)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
||||
return ApiResponse(data=result)
|
||||
|
||||
|
||||
@router.get("/", response_model=ApiResponse[PagedData[ReimburseApplyOut]])
|
||||
def list_reimburses(
|
||||
page: int = 1,
|
||||
|
||||
@@ -0,0 +1,614 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
from datetime import date, datetime, timedelta
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from io import BytesIO
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from openpyxl import Workbook, load_workbook
|
||||
from openpyxl.styles import PatternFill
|
||||
from openpyxl.utils import get_column_letter
|
||||
from openpyxl.worksheet.worksheet import Worksheet
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.bx_auth import BxUserContext
|
||||
from app.models.bx_models import BxExpenseItem, BxWorkflowDef
|
||||
from app.models.models import SysUser
|
||||
from app.services.bx.expense_item import ExpenseItemService
|
||||
from app.services.bx.reimburse import ReimburseService
|
||||
from app.utils.bx_codegen import generate_apply_code, parse_optional_date, parse_reimburse_date
|
||||
from app.utils.excel_list_validation import add_list_validation, write_option_sheet
|
||||
|
||||
EXAMPLE_REMARK_PREFIX = "【示例】"
|
||||
PATH_SEP = " / "
|
||||
MAX_IMPORT_ROWS = 500
|
||||
|
||||
IMPORT_SHEET = "报销导入"
|
||||
GUIDE_SHEET = "填写说明"
|
||||
EXPENSE_OPT_SHEET = "_选项_费用项目"
|
||||
APPLICANT_OPT_SHEET = "_选项_申请人"
|
||||
|
||||
HEADERS = [
|
||||
"分组号",
|
||||
"申请人",
|
||||
"报销日期",
|
||||
"报销标题",
|
||||
"费用项目",
|
||||
"金额(元)",
|
||||
"报销事由",
|
||||
"发生时间",
|
||||
]
|
||||
|
||||
_GUIDE_LINES = [
|
||||
"1. 请使用本系统「下载模板」获取最新文件;费用项/人员变更后请重新下载。",
|
||||
"2. 推荐 Excel 或 WPS 桌面版;在线表格可能丢失下拉。",
|
||||
"3. 一行一条费用明细;相同「分组号」合成一张报销单。",
|
||||
"4. 分组号可空:空则该行单独成单。",
|
||||
"5. 同一分组内:申请人、报销日期、报销标题必须每行都填且完全一致(标题可都留空以自动生成)。",
|
||||
"6. 费用项目、申请人请用下拉选择,勿手改名称。费用项目下拉与系统费用树一致:仅启用项,不含已删除/已停用,也不含父级已删的孤儿节点。",
|
||||
"7. 灰色示例行仅供参考,导入时自动跳过(报销事由以【示例】开头),请从示例下方空行开始填写。",
|
||||
"8. 导入结果为「待提交」草稿;发票请导入后在系统内关联。",
|
||||
"9. 请勿修改或删除隐藏的 _选项_* 工作表。",
|
||||
"10. Excel 行号从 1 起计(第 1 行为表头);错误清单中的行号与打开文件所见一致。",
|
||||
]
|
||||
|
||||
_EXAMPLE_FILL = PatternFill(start_color="F1F5F9", end_color="F1F5F9", fill_type="solid")
|
||||
|
||||
|
||||
def build_applicant_label(nick_name: str, user_no: str, user_id: str) -> str:
|
||||
name = (nick_name or user_no or user_id or "").strip() or user_id
|
||||
suffix = (user_no or "").strip() or str(user_id)[-6:]
|
||||
return f"{name}({suffix})"
|
||||
|
||||
|
||||
def _ids_visible_in_web_tree(nodes: List[Dict[str, Any]]) -> set:
|
||||
"""与 ExpenseItemService._build_tree 一致:父级不存在的孤儿节点不会出现在 Web 树中。"""
|
||||
by_id = {str(n["id"]): n for n in nodes}
|
||||
visible: set = set()
|
||||
|
||||
def walk(nid: str) -> None:
|
||||
if nid in visible:
|
||||
return
|
||||
if nid not in by_id:
|
||||
return
|
||||
visible.add(nid)
|
||||
for cid, c in by_id.items():
|
||||
if str(c.get("parent_id") or "0") == nid:
|
||||
walk(cid)
|
||||
|
||||
for n in nodes:
|
||||
pid = str(n.get("parent_id") or "0")
|
||||
if not pid or pid == "0":
|
||||
walk(str(n["id"]))
|
||||
return visible
|
||||
|
||||
|
||||
def build_expense_path_labels(nodes: List[Dict[str, Any]]) -> Dict[str, str]:
|
||||
"""返回 {id: 展示文案}。
|
||||
|
||||
与 Web 费用树对齐:仅启用项,且排除父级已删导致的孤儿节点。
|
||||
冲突时追加 (编码:xxx)。
|
||||
"""
|
||||
by_id = {str(n["id"]): n for n in nodes}
|
||||
visible = _ids_visible_in_web_tree(nodes)
|
||||
enabled = [
|
||||
n
|
||||
for n in nodes
|
||||
if int(n.get("status") or 0) == 1 and str(n["id"]) in visible
|
||||
]
|
||||
|
||||
def path_of(nid: str) -> str:
|
||||
parts: List[str] = []
|
||||
cur = by_id.get(nid)
|
||||
guard = 0
|
||||
while cur and guard < 20:
|
||||
parts.append(str(cur.get("item_name") or "").strip())
|
||||
pid = str(cur.get("parent_id") or "0")
|
||||
if not pid or pid == "0":
|
||||
break
|
||||
cur = by_id.get(pid)
|
||||
guard += 1
|
||||
return PATH_SEP.join(reversed([p for p in parts if p]))
|
||||
|
||||
raw: Dict[str, str] = {str(n["id"]): path_of(str(n["id"])) for n in enabled}
|
||||
path_to_ids: Dict[str, List[str]] = {}
|
||||
for iid, p in raw.items():
|
||||
path_to_ids.setdefault(p, []).append(iid)
|
||||
out: Dict[str, str] = {}
|
||||
for p, ids in path_to_ids.items():
|
||||
if len(ids) == 1:
|
||||
out[ids[0]] = p
|
||||
continue
|
||||
for iid in ids:
|
||||
code = (by_id[iid].get("item_code") or "").strip() or iid[-6:]
|
||||
out[iid] = f"{p}(编码:{code})"
|
||||
return out
|
||||
|
||||
|
||||
def is_example_row(row: Dict[str, Any]) -> bool:
|
||||
remark = str(row.get("报销事由") or "").strip()
|
||||
return remark.startswith(EXAMPLE_REMARK_PREFIX)
|
||||
|
||||
|
||||
def partition_import_groups(rows: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
ordered: List[Dict[str, Any]] = []
|
||||
index: Dict[str, int] = {}
|
||||
for row in rows:
|
||||
g = str(row.get("分组号") or "").strip()
|
||||
if not g:
|
||||
ordered.append({"group_no": "", "rows": [row]})
|
||||
continue
|
||||
if g not in index:
|
||||
index[g] = len(ordered)
|
||||
ordered.append({"group_no": g, "rows": [row]})
|
||||
else:
|
||||
ordered[index[g]]["rows"].append(row)
|
||||
return ordered
|
||||
|
||||
|
||||
def validate_group_headers(rows: List[Dict[str, Any]]) -> Optional[str]:
|
||||
if not rows:
|
||||
return "分组无有效行"
|
||||
apps = {str(r.get("申请人") or "").strip() for r in rows}
|
||||
dates = {str(r.get("报销日期") or "").strip() for r in rows}
|
||||
titles = [str(r.get("报销标题") or "").strip() for r in rows]
|
||||
if len(apps) != 1 or not next(iter(apps)):
|
||||
return "同组申请人不一致或为空"
|
||||
if len(dates) != 1 or not next(iter(dates)):
|
||||
return "同组报销日期不一致或为空"
|
||||
empty_n = sum(1 for t in titles if not t)
|
||||
if 0 < empty_n < len(titles):
|
||||
return "同组报销标题不能部分为空"
|
||||
if empty_n == 0 and len(set(titles)) != 1:
|
||||
return "同组报销标题不一致"
|
||||
return None
|
||||
|
||||
|
||||
def cell_str(v: Any) -> str:
|
||||
if v is None:
|
||||
return ""
|
||||
if isinstance(v, datetime):
|
||||
return v.date().isoformat()
|
||||
if isinstance(v, date):
|
||||
return v.isoformat()
|
||||
# Excel 常把纯数字单元格读成 float(如 29.0),需归一成 "29"
|
||||
if isinstance(v, float) and v.is_integer():
|
||||
return str(int(v))
|
||||
if isinstance(v, int) and not isinstance(v, bool):
|
||||
return str(v)
|
||||
return str(v).strip()
|
||||
|
||||
|
||||
def parse_amount(v: Any) -> Tuple[Optional[Decimal], Optional[str]]:
|
||||
if v is None or cell_str(v) == "":
|
||||
return None, "金额不能为空"
|
||||
try:
|
||||
amt = Decimal(str(v).strip())
|
||||
except (InvalidOperation, ValueError):
|
||||
return None, "金额格式无效"
|
||||
if amt <= 0:
|
||||
return None, "金额必须大于 0"
|
||||
return amt, None
|
||||
|
||||
|
||||
def load_expense_nodes_flat(db: Session, tenant_no: str) -> List[Dict[str, Any]]:
|
||||
rows = (
|
||||
db.query(BxExpenseItem)
|
||||
.filter(BxExpenseItem.del_flag == 0, BxExpenseItem.tenant_no == tenant_no)
|
||||
.order_by(BxExpenseItem.sort_order.asc(), BxExpenseItem.create_time.desc())
|
||||
.all()
|
||||
)
|
||||
return [
|
||||
{
|
||||
"id": r.id,
|
||||
"parent_id": r.parent_id or "0",
|
||||
"item_name": r.item_name,
|
||||
"status": r.status,
|
||||
"item_code": r.item_code,
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
|
||||
|
||||
def load_applicant_options(
|
||||
db: Session, *, user: BxUserContext, tenant_no: str
|
||||
) -> List[Dict[str, Any]]:
|
||||
q = db.query(SysUser).filter(
|
||||
SysUser.del_flag == 0,
|
||||
SysUser.status == "1",
|
||||
SysUser.tenant_no == tenant_no,
|
||||
)
|
||||
if not ReimburseService.can_change_applicant(db, user):
|
||||
q = q.filter(SysUser.id == user.id)
|
||||
users = q.order_by(SysUser.create_time.desc()).all()
|
||||
return [
|
||||
{"id": u.id, "nick_name": u.nick_name or "", "user_no": u.user_no or ""}
|
||||
for u in users
|
||||
]
|
||||
|
||||
|
||||
def build_import_template_bytes(
|
||||
*,
|
||||
expense_nodes: List[Dict[str, Any]],
|
||||
applicants: List[Dict[str, Any]],
|
||||
current_applicant_label: str,
|
||||
today: date,
|
||||
) -> bytes:
|
||||
wb = Workbook()
|
||||
|
||||
guide = wb.active
|
||||
guide.title = GUIDE_SHEET
|
||||
for line in _GUIDE_LINES:
|
||||
guide.append([line])
|
||||
if not expense_nodes or not build_expense_path_labels(expense_nodes):
|
||||
guide.append(["注意:当前租户暂无启用费用项目,请先在系统中配置后再填写费用项目列。"])
|
||||
|
||||
ws = wb.create_sheet(IMPORT_SHEET, 0)
|
||||
ws.append(list(HEADERS))
|
||||
|
||||
expense_labels = build_expense_path_labels(expense_nodes)
|
||||
expense_items = sorted(expense_labels.items(), key=lambda x: x[1])
|
||||
exp_ids = [iid for iid, _ in expense_items]
|
||||
exp_labs = [lab for _, lab in expense_items]
|
||||
write_option_sheet(wb, EXPENSE_OPT_SHEET, exp_labs, exp_ids, hidden=True)
|
||||
|
||||
app_labs: List[str] = []
|
||||
app_ids: List[str] = []
|
||||
for a in applicants:
|
||||
lab = build_applicant_label(
|
||||
str(a.get("nick_name") or ""),
|
||||
str(a.get("user_no") or ""),
|
||||
str(a.get("id") or ""),
|
||||
)
|
||||
app_labs.append(lab)
|
||||
app_ids.append(str(a.get("id") or ""))
|
||||
write_option_sheet(wb, APPLICANT_OPT_SHEET, app_labs, app_ids, hidden=True)
|
||||
|
||||
fee1 = exp_labs[0] if exp_labs else ""
|
||||
fee2 = exp_labs[1] if len(exp_labs) > 1 else fee1
|
||||
yesterday = (today - timedelta(days=1)).isoformat()
|
||||
today_s = today.isoformat()
|
||||
|
||||
examples = [
|
||||
["G1", current_applicant_label, today_s, "", fee1, 50.00, f"{EXAMPLE_REMARK_PREFIX}地铁通勤,请删除本行", ""],
|
||||
["G1", current_applicant_label, today_s, "", fee2, 300.00, f"{EXAMPLE_REMARK_PREFIX}住宿,请删除本行", yesterday],
|
||||
["", current_applicant_label, today_s, "", fee1, 20.00, f"{EXAMPLE_REMARK_PREFIX}空分组号单独成单", ""],
|
||||
]
|
||||
for row in examples:
|
||||
ws.append(row)
|
||||
for col in range(1, len(HEADERS) + 1):
|
||||
ws.cell(row=ws.max_row, column=col).fill = _EXAMPLE_FILL
|
||||
|
||||
data_end = 1 + MAX_IMPORT_ROWS
|
||||
if app_labs:
|
||||
add_list_validation(
|
||||
ws,
|
||||
option_sheet=APPLICANT_OPT_SHEET,
|
||||
option_rows=len(app_labs),
|
||||
target_col=2,
|
||||
start_row=2,
|
||||
end_row=data_end,
|
||||
)
|
||||
if exp_labs:
|
||||
add_list_validation(
|
||||
ws,
|
||||
option_sheet=EXPENSE_OPT_SHEET,
|
||||
option_rows=len(exp_labs),
|
||||
target_col=5,
|
||||
start_row=2,
|
||||
end_row=data_end,
|
||||
)
|
||||
|
||||
buf = BytesIO()
|
||||
wb.save(buf)
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
RESULT_STATUS_COL = "导入结果"
|
||||
RESULT_MSG_COL = "失败原因"
|
||||
|
||||
|
||||
def _read_sheet_header(ws: Worksheet) -> Tuple[List[str], Dict[str, int]]:
|
||||
header_cells = [cell_str(c.value) for c in next(ws.iter_rows(min_row=1, max_row=1))]
|
||||
missing = [h for h in HEADERS if h not in header_cells]
|
||||
if missing:
|
||||
raise ValueError(f"表头缺少列:{', '.join(missing)}")
|
||||
col_index = {name: header_cells.index(name) for name in HEADERS}
|
||||
return header_cells, col_index
|
||||
|
||||
|
||||
def scan_sheet_rows(ws: Worksheet) -> List[Dict[str, Any]]:
|
||||
"""扫描非空行。含示例行(_kind=example)与可导入行(_kind=data)。"""
|
||||
_header_cells, col_index = _read_sheet_header(ws)
|
||||
rows: List[Dict[str, Any]] = []
|
||||
for excel_row, cells in enumerate(ws.iter_rows(min_row=2, values_only=True), start=2):
|
||||
values = list(cells) if cells else []
|
||||
row: Dict[str, Any] = {"_row": excel_row}
|
||||
empty = True
|
||||
for name, idx in col_index.items():
|
||||
raw = values[idx] if idx < len(values) else None
|
||||
if name in ("报销日期", "发生时间"):
|
||||
row[name] = cell_str(raw)
|
||||
elif name == "金额(元)":
|
||||
row[name] = raw
|
||||
else:
|
||||
row[name] = cell_str(raw)
|
||||
if cell_str(raw) != "" and raw is not None:
|
||||
empty = False
|
||||
if empty:
|
||||
continue
|
||||
row["_kind"] = "example" if is_example_row(row) else "data"
|
||||
rows.append(row)
|
||||
return rows
|
||||
|
||||
|
||||
def iter_data_rows(ws: Worksheet) -> List[Dict[str, Any]]:
|
||||
"""仅返回可导入数据行(跳过示例),兼容旧测试。"""
|
||||
return [r for r in scan_sheet_rows(ws) if r.get("_kind") == "data"]
|
||||
|
||||
|
||||
def build_annotated_result_bytes(
|
||||
file_bytes: bytes,
|
||||
outcomes: Dict[int, Dict[str, str]],
|
||||
) -> bytes:
|
||||
"""在原表「报销导入」末尾追加「导入结果」「失败原因」列。"""
|
||||
wb = load_workbook(BytesIO(file_bytes))
|
||||
if IMPORT_SHEET not in wb.sheetnames:
|
||||
raise ValueError(f"缺少工作表「{IMPORT_SHEET}」")
|
||||
ws = wb[IMPORT_SHEET]
|
||||
header_cells = [cell_str(c.value) for c in next(ws.iter_rows(min_row=1, max_row=1))]
|
||||
# 若已存在结果列则复用,否则追加
|
||||
if RESULT_STATUS_COL in header_cells:
|
||||
status_col = header_cells.index(RESULT_STATUS_COL) + 1
|
||||
else:
|
||||
status_col = len(header_cells) + 1
|
||||
ws.cell(row=1, column=status_col, value=RESULT_STATUS_COL)
|
||||
header_cells.append(RESULT_STATUS_COL)
|
||||
if RESULT_MSG_COL in header_cells:
|
||||
msg_col = header_cells.index(RESULT_MSG_COL) + 1
|
||||
else:
|
||||
msg_col = len(header_cells) + 1
|
||||
ws.cell(row=1, column=msg_col, value=RESULT_MSG_COL)
|
||||
|
||||
max_row = max(ws.max_row or 1, max(outcomes.keys(), default=1))
|
||||
for r in range(2, max_row + 1):
|
||||
oc = outcomes.get(r)
|
||||
if not oc:
|
||||
continue
|
||||
ws.cell(row=r, column=status_col, value=oc.get("status") or "")
|
||||
ws.cell(row=r, column=msg_col, value=oc.get("message") or "")
|
||||
|
||||
# 略加宽结果列
|
||||
ws.column_dimensions[get_column_letter(status_col)].width = 12
|
||||
ws.column_dimensions[get_column_letter(msg_col)].width = 36
|
||||
|
||||
buf = BytesIO()
|
||||
wb.save(buf)
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def _mark_rows(
|
||||
outcomes: Dict[int, Dict[str, str]],
|
||||
row_nums: List[int],
|
||||
*,
|
||||
status: str,
|
||||
message: str = "",
|
||||
group_no: str = "",
|
||||
) -> None:
|
||||
for rn in row_nums:
|
||||
outcomes[rn] = {
|
||||
"status": status,
|
||||
"message": message,
|
||||
"groupNo": group_no,
|
||||
"excelRow": str(rn),
|
||||
}
|
||||
|
||||
|
||||
def import_reimburse_workbook(
|
||||
db: Session,
|
||||
*,
|
||||
file_bytes: bytes,
|
||||
user: BxUserContext,
|
||||
) -> Dict[str, Any]:
|
||||
try:
|
||||
wb = load_workbook(BytesIO(file_bytes), data_only=True)
|
||||
except Exception as exc:
|
||||
raise ValueError("无法解析 Excel 文件,请上传有效的 .xlsx") from exc
|
||||
|
||||
if IMPORT_SHEET not in wb.sheetnames:
|
||||
raise ValueError(f"缺少工作表「{IMPORT_SHEET}」")
|
||||
|
||||
scanned = scan_sheet_rows(wb[IMPORT_SHEET])
|
||||
outcomes: Dict[int, Dict[str, str]] = {}
|
||||
for r in scanned:
|
||||
if r.get("_kind") == "example":
|
||||
_mark_rows(
|
||||
outcomes,
|
||||
[int(r["_row"])],
|
||||
status="跳过",
|
||||
message="示例行,未导入",
|
||||
group_no=cell_str(r.get("分组号")),
|
||||
)
|
||||
|
||||
rows = [r for r in scanned if r.get("_kind") == "data"]
|
||||
if len(rows) > MAX_IMPORT_ROWS:
|
||||
raise ValueError(f"有效数据超过 {MAX_IMPORT_ROWS} 行,请拆分后导入")
|
||||
|
||||
tenant_no = ReimburseService._tenant_no(db, user)
|
||||
expense_nodes = load_expense_nodes_flat(db, tenant_no)
|
||||
expense_label_to_id = {lab: iid for iid, lab in build_expense_path_labels(expense_nodes).items()}
|
||||
|
||||
applicants = load_applicant_options(db, user=user, tenant_no=tenant_no)
|
||||
applicant_label_to_id = {
|
||||
build_applicant_label(a["nick_name"], a["user_no"], a["id"]): a["id"] for a in applicants
|
||||
}
|
||||
|
||||
wf = (
|
||||
db.query(BxWorkflowDef)
|
||||
.filter(
|
||||
BxWorkflowDef.biz_type == "reimburse",
|
||||
BxWorkflowDef.tenant_no == tenant_no,
|
||||
BxWorkflowDef.del_flag == 0,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
created: List[Dict[str, Any]] = []
|
||||
errors: List[Dict[str, Any]] = []
|
||||
success_rows = 0
|
||||
fail_rows = 0
|
||||
|
||||
def fail_group(group_no: str, grow: List[Dict[str, Any]], message: str) -> None:
|
||||
nonlocal fail_rows
|
||||
row_nums = [int(r["_row"]) for r in grow]
|
||||
errors.append({"groupNo": group_no, "rows": row_nums, "message": message})
|
||||
_mark_rows(outcomes, row_nums, status="失败", message=message, group_no=group_no)
|
||||
fail_rows += len(grow)
|
||||
|
||||
for group in partition_import_groups(rows):
|
||||
group_no = group["group_no"]
|
||||
grow: List[Dict[str, Any]] = group["rows"]
|
||||
row_nums = [int(r["_row"]) for r in grow]
|
||||
|
||||
header_err = validate_group_headers(grow)
|
||||
if header_err:
|
||||
fail_group(group_no, grow, header_err)
|
||||
continue
|
||||
|
||||
applicant_label = cell_str(grow[0].get("申请人"))
|
||||
applicant_id = applicant_label_to_id.get(applicant_label)
|
||||
if not applicant_id:
|
||||
fail_group(group_no, grow, f"申请人「{applicant_label}」不存在或不在可选范围")
|
||||
continue
|
||||
|
||||
try:
|
||||
resolved_id, resolved_name = ReimburseService.resolve_applicant(
|
||||
db, user, applicant_id, tenant_no=tenant_no
|
||||
)
|
||||
except Exception as exc:
|
||||
detail = getattr(exc, "detail", None) or str(exc)
|
||||
fail_group(group_no, grow, str(detail))
|
||||
continue
|
||||
|
||||
date_text = cell_str(grow[0].get("报销日期"))
|
||||
try:
|
||||
reimburse_date = parse_reimburse_date(date_text)
|
||||
except ValueError:
|
||||
fail_group(group_no, grow, "报销日期格式无效,请使用 YYYY-MM-DD")
|
||||
continue
|
||||
|
||||
title = cell_str(grow[0].get("报销标题"))
|
||||
details_data: List[dict] = []
|
||||
row_fail: Optional[str] = None
|
||||
for r in grow:
|
||||
expense_label = cell_str(r.get("费用项目"))
|
||||
expense_id = expense_label_to_id.get(expense_label)
|
||||
if not expense_id:
|
||||
row_fail = f"费用项目「{expense_label}」不存在或已停用"
|
||||
break
|
||||
item = ExpenseItemService.get_by_id(
|
||||
db, expense_id, tenant_no, user=user, skip_scope=True
|
||||
)
|
||||
if not item or int(item.status or 0) != 1:
|
||||
row_fail = f"费用项目「{expense_label}」不存在或已停用"
|
||||
break
|
||||
amount, amount_err = parse_amount(r.get("金额(元)"))
|
||||
if amount_err:
|
||||
row_fail = amount_err
|
||||
break
|
||||
remark = cell_str(r.get("报销事由"))
|
||||
if not remark:
|
||||
row_fail = "报销事由不能为空"
|
||||
break
|
||||
occur_raw = r.get("发生时间")
|
||||
try:
|
||||
occur_date = parse_optional_date(occur_raw if occur_raw not in ("", None) else None)
|
||||
except ValueError:
|
||||
row_fail = "发生时间格式无效,请使用 YYYY-MM-DD"
|
||||
break
|
||||
details_data.append(
|
||||
{
|
||||
"expense_id": expense_id,
|
||||
"expense_name": item.item_name,
|
||||
"amount": amount,
|
||||
"remark": remark,
|
||||
"occur_date": occur_date,
|
||||
}
|
||||
)
|
||||
|
||||
if row_fail:
|
||||
fail_group(group_no, grow, row_fail)
|
||||
continue
|
||||
|
||||
data: Dict[str, Any] = {
|
||||
"apply_code": generate_apply_code(db, "BX", tenant_no),
|
||||
"status": "draft",
|
||||
"remark": "",
|
||||
"reimburse_date": reimburse_date,
|
||||
}
|
||||
if title:
|
||||
data["title"] = title
|
||||
if wf:
|
||||
data["wf_id"] = wf.id
|
||||
|
||||
try:
|
||||
apply_obj = ReimburseService.create_with_details(
|
||||
db,
|
||||
data,
|
||||
details_data,
|
||||
user=user,
|
||||
applicant_id=resolved_id,
|
||||
applicant_name=resolved_name,
|
||||
)
|
||||
db.commit()
|
||||
created.append(
|
||||
{
|
||||
"groupNo": group_no,
|
||||
"applyId": apply_obj.id,
|
||||
"applyCode": apply_obj.apply_code,
|
||||
}
|
||||
)
|
||||
ok_msg = f"成功,单号 {apply_obj.apply_code}"
|
||||
_mark_rows(outcomes, row_nums, status="成功", message=ok_msg, group_no=group_no)
|
||||
success_rows += len(grow)
|
||||
except Exception as exc:
|
||||
db.rollback()
|
||||
fail_group(group_no, grow, f"写入失败:{exc}")
|
||||
|
||||
tip = ""
|
||||
if not rows and scanned:
|
||||
tip = "没有可导入的有效数据。模板中的【示例】行已自动跳过,请填写正式数据(报销事由不要带【示例】)后再导入。"
|
||||
elif not rows and not scanned:
|
||||
tip = "表格中没有可识别的数据行,请检查是否填写了「报销导入」工作表。"
|
||||
|
||||
row_results = [
|
||||
{
|
||||
"excelRow": int(k),
|
||||
"groupNo": v.get("groupNo") or "",
|
||||
"status": v.get("status") or "",
|
||||
"message": v.get("message") or "",
|
||||
}
|
||||
for k, v in sorted(outcomes.items(), key=lambda x: x[0])
|
||||
]
|
||||
|
||||
result_file_base64 = ""
|
||||
try:
|
||||
annotated = build_annotated_result_bytes(file_bytes, outcomes)
|
||||
result_file_base64 = base64.b64encode(annotated).decode("ascii")
|
||||
except Exception:
|
||||
result_file_base64 = ""
|
||||
|
||||
return {
|
||||
"successGroups": len(created),
|
||||
"successRows": success_rows,
|
||||
"failGroups": len(errors),
|
||||
"failRows": fail_rows,
|
||||
"skippedRows": sum(1 for r in row_results if r["status"] == "跳过"),
|
||||
"created": created,
|
||||
"errors": errors,
|
||||
"rowResults": row_results,
|
||||
"resultFileBase64": result_file_base64,
|
||||
"resultFileName": "报销申请导入结果.xlsx",
|
||||
"tip": tip,
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Sequence
|
||||
|
||||
from openpyxl.utils import get_column_letter
|
||||
from openpyxl.workbook.workbook import Workbook
|
||||
from openpyxl.worksheet.datavalidation import DataValidation
|
||||
from openpyxl.worksheet.worksheet import Worksheet
|
||||
|
||||
|
||||
def write_option_sheet(
|
||||
wb: Workbook,
|
||||
sheet_title: str,
|
||||
labels: Sequence[str],
|
||||
ids: Sequence[str] | None = None,
|
||||
*,
|
||||
hidden: bool = True,
|
||||
) -> str:
|
||||
"""写入选项表:A=label,可选 B=id。返回 sheet_title。"""
|
||||
if sheet_title in wb.sheetnames:
|
||||
ws = wb[sheet_title]
|
||||
wb.remove(ws)
|
||||
ws = wb.create_sheet(sheet_title)
|
||||
ws.append(["label", "id"] if ids is not None else ["label"])
|
||||
for i, lab in enumerate(labels):
|
||||
if ids is not None:
|
||||
ws.append([lab, ids[i] if i < len(ids) else ""])
|
||||
else:
|
||||
ws.append([lab])
|
||||
if hidden:
|
||||
ws.sheet_state = "hidden"
|
||||
return sheet_title
|
||||
|
||||
|
||||
def add_list_validation(
|
||||
ws: Worksheet,
|
||||
*,
|
||||
option_sheet: str,
|
||||
option_rows: int,
|
||||
target_col: int,
|
||||
start_row: int,
|
||||
end_row: int,
|
||||
allow_blank: bool = True,
|
||||
) -> None:
|
||||
"""option_rows 为选项数据行数(不含表头)。option 在 A2:A{1+option_rows}。"""
|
||||
if option_rows <= 0:
|
||||
return
|
||||
last = 1 + option_rows
|
||||
formula = f"='{option_sheet}'!$A$2:$A${last}"
|
||||
col = get_column_letter(target_col)
|
||||
dv = DataValidation(
|
||||
type="list",
|
||||
formula1=formula,
|
||||
allow_blank=allow_blank,
|
||||
showErrorMessage=True,
|
||||
showInputMessage=True,
|
||||
)
|
||||
dv.error = "请从下拉列表选择"
|
||||
dv.errorTitle = "无效值"
|
||||
dv.add(f"{col}{start_row}:{col}{end_row}")
|
||||
ws.add_data_validation(dv)
|
||||
@@ -74,40 +74,24 @@
|
||||
"72": "Community 72",
|
||||
"73": "Community 73",
|
||||
"74": "Community 74",
|
||||
"75": "Community 75",
|
||||
"76": "Community 76",
|
||||
"77": "Community 77",
|
||||
"78": "Community 78",
|
||||
"79": "Community 79",
|
||||
"80": "Community 80",
|
||||
"81": "Community 81",
|
||||
"82": "Community 82",
|
||||
"83": "Community 83",
|
||||
"84": "Community 84",
|
||||
"85": "Community 85",
|
||||
"86": "Community 86",
|
||||
"87": "Community 87",
|
||||
"88": "Any",
|
||||
"89": "pl_ocr_service.py",
|
||||
"90": "Community 90",
|
||||
"91": "pl_vehicle_recharge_service.py",
|
||||
"92": "Community 92",
|
||||
"93": "_invoice_info_by_customer_impl",
|
||||
"94": "response.py",
|
||||
"95": "Community 95",
|
||||
"96": "Community 96",
|
||||
"97": "_ensure_subpack_slip_no",
|
||||
"98": "Community 98",
|
||||
"99": "Community 99",
|
||||
"100": "price_letter_detail",
|
||||
"101": ".to_tenant_user_dict",
|
||||
"102": "dept.py",
|
||||
"103": "Community 103",
|
||||
"104": "apply_crm_create_by_scope",
|
||||
"105": "list_customers",
|
||||
"106": "Community 106",
|
||||
"107": "apply_pl_scope",
|
||||
"108": "Community 108",
|
||||
"109": "Community 109",
|
||||
"110": "Base",
|
||||
"111": "Community 111",
|
||||
@@ -116,153 +100,59 @@
|
||||
"114": "db_models.py",
|
||||
"115": "site.py",
|
||||
"116": "Community 116",
|
||||
"117": "Community 117",
|
||||
"118": "Community 118",
|
||||
"119": "Community 119",
|
||||
"120": "Community 120",
|
||||
"121": "Community 121",
|
||||
"122": "Community 122",
|
||||
"123": "Community 123",
|
||||
"124": "Community 124",
|
||||
"125": "Community 125",
|
||||
"126": "customer_pool.py",
|
||||
"127": "Community 127",
|
||||
"128": "plan.py",
|
||||
"129": "BaseStorage",
|
||||
"130": "datetime_parse.py",
|
||||
"131": "LocalStorage",
|
||||
"132": "crm_supplier_hazard.py",
|
||||
"133": "Community 133",
|
||||
"134": "Community 134",
|
||||
"135": "unsettled_plans_q_filter",
|
||||
"136": "Community 136",
|
||||
"137": "statistics.py",
|
||||
"138": "Community 138",
|
||||
"139": "get_current_user_by_token",
|
||||
"140": "Community 140",
|
||||
"141": "PlTransport",
|
||||
"142": "Community 142",
|
||||
"143": "SysEthnic",
|
||||
"144": "dict_data.py",
|
||||
"145": "resolve_crm_product",
|
||||
"146": "__init__.py",
|
||||
"147": "Community 147",
|
||||
"148": "storage.py",
|
||||
"149": "Community 149",
|
||||
"150": "apply_site_feedback",
|
||||
"151": "Community 151",
|
||||
"152": "response.py",
|
||||
"153": "Community 153",
|
||||
"154": "response.py",
|
||||
"155": "Community 155",
|
||||
"156": "utils.py",
|
||||
"157": "Community 157",
|
||||
"158": "Community 158",
|
||||
"159": "customer_update",
|
||||
"160": "register_local_callback",
|
||||
"161": "backfill_crm_contract_attachment_urls.py",
|
||||
"162": ".to_tenant_user_dict",
|
||||
"163": "__init__.py",
|
||||
"164": "get_client_ip",
|
||||
"165": "Community 165",
|
||||
"166": "code:block1 (/iflytek/shanxia2.0/backend)",
|
||||
"167": "Community 167",
|
||||
"168": "vehicle_urea.py",
|
||||
"169": "product.py",
|
||||
"170": "test_quote_approver_setting.py",
|
||||
"171": "minio_url.py",
|
||||
"172": "apply_pl_scope",
|
||||
"173": "resolve_create_time_bounds",
|
||||
"174": "register_local_callback",
|
||||
"175": "vehicle_maintenance.py",
|
||||
"176": "_forbid_legacy_invoice_fields",
|
||||
"177": "vehicle_maintenance.py",
|
||||
"178": ".__init__",
|
||||
"179": "test_pl_vehicle_personnel_photo_schema.py",
|
||||
"180": "OSSStorage",
|
||||
"181": "allocate_next_biz_no",
|
||||
"182": "statistics.py",
|
||||
"183": "resolve_create_time_bounds",
|
||||
"184": "excel_export.py",
|
||||
"185": "vehicle_urea.py",
|
||||
"186": "transport_address.py",
|
||||
"187": "price_letter_detail",
|
||||
"188": "get_client_ip",
|
||||
"189": "create",
|
||||
"190": "menu.py",
|
||||
"191": "context.py",
|
||||
"192": "pool_rule.py",
|
||||
"193": "storage.py",
|
||||
"194": "get_current_user_by_token",
|
||||
"195": "PlFreightSettlementTransport",
|
||||
"196": "crm_contract_sub_routes.py",
|
||||
"197": "_coerce_resource_snapshot",
|
||||
"198": "user.py",
|
||||
"199": "price_letter_detail",
|
||||
"200": "assert_contract_editable_for_update",
|
||||
"202": "statistics.py",
|
||||
"206": "crm_quote_pdf.py",
|
||||
"207": "shanxia2.0 后端骨架(FastAPI)",
|
||||
"211": "leave.py",
|
||||
"216": "io_order.py",
|
||||
"217": "auth.py",
|
||||
"218": "pool_rule.py",
|
||||
"220": "api_crypto_middleware.py",
|
||||
"223": "workflow.py",
|
||||
"224": "supplier_info.py",
|
||||
"226": "get_active",
|
||||
"228": "advance.py",
|
||||
"230": "attachment.py",
|
||||
"232": "transport_address.py",
|
||||
"233": "customer_pool.py",
|
||||
"235": "invoice_info.py",
|
||||
"236": "price_letter.py",
|
||||
"237": "order_status_compat.py",
|
||||
"239": "shanxia2.0 Backend 生产部署说明",
|
||||
"240": "delete",
|
||||
"242": "leave.py",
|
||||
"244": "expense_item.py",
|
||||
"245": "__init__.py",
|
||||
"246": "db_collation.py",
|
||||
"247": "expense_item.py",
|
||||
"249": "__init__.py",
|
||||
"250": "invoice.py",
|
||||
"251": "menu.py",
|
||||
"252": "public_inventory.py",
|
||||
"254": "Activate.ps1",
|
||||
"256": "statistics.py",
|
||||
"259": "一次上传一条发票:多文件挂同一档案,金额为总额,只写一条额度流水。",
|
||||
"262": "reimburse.py",
|
||||
"263": "__init__.py",
|
||||
"264": "__init__.py",
|
||||
"265": "__init__.py",
|
||||
"266": "workflow.py",
|
||||
"269": "__init__.py",
|
||||
"270": "leave.py",
|
||||
"271": "menu.py",
|
||||
"272": "__init__.py",
|
||||
"273": "__init__.py",
|
||||
"274": "__init__.py",
|
||||
"275": "__init__.py",
|
||||
"276": "__init__.py",
|
||||
"278": "__init__.py",
|
||||
"281": "__init__.py",
|
||||
"286": "send_delivery_notice",
|
||||
"287": "__init__.py",
|
||||
"288": "__init__.py",
|
||||
"289": "send_delivery_warning",
|
||||
"292": "send_order_status",
|
||||
"293": "__init__.py",
|
||||
"294": "__init__.py",
|
||||
"295": "__init__.py",
|
||||
"296": "__init__.py",
|
||||
"297": "__init__.py",
|
||||
"298": "__init__.py",
|
||||
"299": "__init__.py",
|
||||
"300": "__init__.py",
|
||||
"301": "__init__.py",
|
||||
"302": "send_payment_success",
|
||||
"303": "send_refund_result",
|
||||
"304": "__init__.py",
|
||||
"305": "__init__.py",
|
||||
"306": "__init__.py",
|
||||
@@ -272,165 +162,6 @@
|
||||
"310": "__init__.py",
|
||||
"311": "__init__.py",
|
||||
"312": "__init__.py",
|
||||
"313": "send_urge_notification",
|
||||
"314": "get",
|
||||
"315": "get_multi_with_filters",
|
||||
"317": "base.py",
|
||||
"318": "clue.py",
|
||||
"320": "customer_pool.py",
|
||||
"321": "customer.py",
|
||||
"323": "invoice_info.py",
|
||||
"326": "price_letter.py",
|
||||
"327": "product.py",
|
||||
"328": "quote.py",
|
||||
"329": "user.py",
|
||||
"330": "base.py",
|
||||
"331": "__tablename__",
|
||||
"332": "__init__.py",
|
||||
"333": "advance.py",
|
||||
"334": "validate_expense_item_id",
|
||||
"335": "attachment.py",
|
||||
"336": "common.py",
|
||||
"337": "validate_parent_id",
|
||||
"338": "__init__.py",
|
||||
"339": "invoice.py",
|
||||
"340": "coerce_applicant_id",
|
||||
"341": "details_must_not_be_empty",
|
||||
"342": "reimburse.py",
|
||||
"343": "validate_advance_id",
|
||||
"344": "validate_expense_item_id",
|
||||
"345": "validate_approver_id",
|
||||
"346": "common.py",
|
||||
"347": "contact.py",
|
||||
"348": "customer_follow.py",
|
||||
"349": "customer.py",
|
||||
"350": "dashboard.py",
|
||||
"351": "__init__.py",
|
||||
"352": "inventory.py",
|
||||
"353": "plans.py",
|
||||
"354": "weighing.py",
|
||||
"355": "__init__.py",
|
||||
"356": "freight.py",
|
||||
"357": "__init__.py",
|
||||
"358": "plan.py",
|
||||
"360": "validate_coordinates",
|
||||
"361": "transport.py",
|
||||
"362": "validate_amount",
|
||||
"363": "validate_amount",
|
||||
"364": "validate_recharge_type",
|
||||
"365": "validate_amount",
|
||||
"366": "validate_apply_amount",
|
||||
"367": "validate_quantity",
|
||||
"368": "approve",
|
||||
"369": "create",
|
||||
"370": "finance_issue_stats",
|
||||
"371": "get_by_id",
|
||||
"372": "get_paged",
|
||||
"373": "global_status_counts",
|
||||
"374": "issue",
|
||||
"375": "_latest_approval_per_node",
|
||||
"376": "_my_apply_base",
|
||||
"377": "my_apply_stats",
|
||||
"378": "offset_amount_map",
|
||||
"379": "pending_approval_stats",
|
||||
"380": "advance.py",
|
||||
"381": "_query_approvals",
|
||||
"382": "reject",
|
||||
"383": "soft_delete",
|
||||
"384": "submit",
|
||||
"385": "_tenant_no",
|
||||
"386": "update",
|
||||
"387": "_workflow_nodes",
|
||||
"389": "create",
|
||||
"390": "get_by_biz",
|
||||
"391": "get_by_id",
|
||||
"392": "attachment.py",
|
||||
"393": "soft_delete",
|
||||
"394": "_tenant_no",
|
||||
"395": "get_by_id",
|
||||
"396": "get_paged",
|
||||
"397": "base.py",
|
||||
"398": "soft_delete",
|
||||
"399": "update",
|
||||
"401": "_build_tree",
|
||||
"402": "create",
|
||||
"403": "get_by_id",
|
||||
"404": "get_item_tree",
|
||||
"405": "expense_item.py",
|
||||
"406": "soft_delete",
|
||||
"407": "_tenant_no",
|
||||
"408": "update",
|
||||
"409": "_count_invoices",
|
||||
"410": "create",
|
||||
"411": "delete",
|
||||
"412": "get_mine_summary",
|
||||
"413": "is_wallet_admin",
|
||||
"414": "add_entry",
|
||||
"415": "assert_can_delete_invoice",
|
||||
"416": "calc_summary",
|
||||
"417": "debit_reimburse",
|
||||
"418": "list_entries",
|
||||
"419": "_lock_summary",
|
||||
"420": "refresh_summary",
|
||||
"421": "release_reimburse",
|
||||
"422": "list_ledgers",
|
||||
"423": "list_wallets",
|
||||
"424": "_lookup_user_name",
|
||||
"425": "out_rows_with_attachments",
|
||||
"426": "invoice.py",
|
||||
"427": "_require_wallet_admin",
|
||||
"428": "_resolve_owner",
|
||||
"429": "_resolve_owner_for_query",
|
||||
"430": "_summary_to_dict",
|
||||
"431": "_tenant_no",
|
||||
"432": "approve",
|
||||
"433": "batch_offset",
|
||||
"434": "build_apply_out_with_details",
|
||||
"435": "build_expense_name_condition",
|
||||
"436": "can_change_applicant",
|
||||
"437": "create_with_details",
|
||||
"438": "finance_payment_stats",
|
||||
"439": "get_by_id",
|
||||
"440": "get_details",
|
||||
"441": "get_details_map_by_reimburse_ids",
|
||||
"442": "get_linked_invoice_ids",
|
||||
"443": "get_linked_invoice_ids_map",
|
||||
"444": "get_linked_invoices_brief",
|
||||
"445": "get_paged",
|
||||
"446": "global_status_counts",
|
||||
"447": "_has_debit_reimburse",
|
||||
"448": "_latest_approval_per_node",
|
||||
"449": "_my_apply_base",
|
||||
"450": "my_apply_stats",
|
||||
"451": "offset",
|
||||
"452": "out_one_with_attachment_counts",
|
||||
"453": "out_rows_with_attachment_counts",
|
||||
"454": "pay",
|
||||
"455": "reimburse.py",
|
||||
"456": "_query_approvals",
|
||||
"457": "reject",
|
||||
"458": "resolve_applicant",
|
||||
"459": "soft_delete",
|
||||
"460": "submit",
|
||||
"461": "sync_reimburse_invoices",
|
||||
"462": "_tenant_no",
|
||||
"463": "update_with_details",
|
||||
"464": "withdraw",
|
||||
"465": "_workflow_nodes",
|
||||
"466": "ensure_by_biz_type",
|
||||
"467": "get_by_biz_type",
|
||||
"468": "get_by_id",
|
||||
"469": "get_paged",
|
||||
"470": "workflow.py",
|
||||
"471": "save_nodes",
|
||||
"472": "soft_delete",
|
||||
"473": "_tenant_no",
|
||||
"474": "update",
|
||||
"487": "_get_variable",
|
||||
"488": "_parse_node_config",
|
||||
"489": "_parse_variables",
|
||||
"490": "customer_pool.py",
|
||||
"491": "__init__.py",
|
||||
"495": "code:bash (cd /path/to/shanxia2.0)",
|
||||
"496": "code:bash (./start-prod.sh status # 查看状态)",
|
||||
"497": "code:bash (curl -s http://127.0.0.1:9111/health)",
|
||||
@@ -440,108 +171,5 @@
|
||||
"501": "code:env (# 数据库配置)",
|
||||
"502": "code:bash (uvicorn app.main:app --reload --port 8000)",
|
||||
"503": "BaseModel",
|
||||
"504": "PlanResourceSnapshotMixin",
|
||||
"510": "advance.py",
|
||||
"511": "attachment.py",
|
||||
"512": "expense_item.py",
|
||||
"514": "workflow.py",
|
||||
"517": "dashboard.py",
|
||||
"518": "__init__.py",
|
||||
"519": "inventory.py",
|
||||
"520": "plans.py",
|
||||
"521": "public_inventory.py",
|
||||
"522": "weighing.py",
|
||||
"523": "__init__.py",
|
||||
"524": "leave.py",
|
||||
"525": "menu.py",
|
||||
"526": "base_data.py",
|
||||
"527": "freight.py",
|
||||
"528": "__init__.py",
|
||||
"529": "inventory.py",
|
||||
"530": "io_order.py",
|
||||
"531": "plan.py",
|
||||
"533": "transport_address.py",
|
||||
"534": "transport.py",
|
||||
"535": "vehicle_maintenance.py",
|
||||
"536": "vehicle_recharge.py",
|
||||
"537": "vehicle_urea.py",
|
||||
"538": "workflow.py",
|
||||
"539": "auth.py",
|
||||
"545": "__init__.py",
|
||||
"548": "storage.py",
|
||||
"551": "base.py",
|
||||
"553": "clue.py",
|
||||
"554": "contact.py",
|
||||
"555": "contract.py",
|
||||
"556": "customer_follow.py",
|
||||
"557": "customer_pool.py",
|
||||
"558": "customer.py",
|
||||
"559": "invoice_info.py",
|
||||
"560": "opportunity.py",
|
||||
"561": "pool_rule.py",
|
||||
"562": "price_letter.py",
|
||||
"563": "product.py",
|
||||
"564": "quote.py",
|
||||
"565": "statistics.py",
|
||||
"566": "supplier_info.py",
|
||||
"569": "__init__.py",
|
||||
"570": "leave.py",
|
||||
"571": "menu.py",
|
||||
"573": "user.py",
|
||||
"574": "workflow.py",
|
||||
"576": "base.py",
|
||||
"577": "__init__.py",
|
||||
"578": "attachment.py",
|
||||
"579": "auth.py",
|
||||
"580": "advance.py",
|
||||
"581": "attachment.py",
|
||||
"582": "common.py",
|
||||
"583": "expense_item.py",
|
||||
"584": "__init__.py",
|
||||
"585": "workflow.py",
|
||||
"586": "common.py",
|
||||
"587": "clue.py",
|
||||
"588": "contact.py",
|
||||
"589": "contract.py",
|
||||
"590": "customer_follow.py",
|
||||
"591": "customer_pool.py",
|
||||
"592": "customer.py",
|
||||
"593": "invoice_info.py",
|
||||
"594": "opportunity.py",
|
||||
"595": "pool_rule.py",
|
||||
"596": "price_letter.py",
|
||||
"597": "product.py",
|
||||
"598": "quote.py",
|
||||
"599": "statistics.py",
|
||||
"600": "supplier_info.py",
|
||||
"601": "dashboard.py",
|
||||
"602": "__init__.py",
|
||||
"603": "inventory.py",
|
||||
"604": "plans.py",
|
||||
"605": "public_inventory.py",
|
||||
"606": "weighing.py",
|
||||
"607": "__init__.py",
|
||||
"608": "leave.py",
|
||||
"609": "menu.py",
|
||||
"610": "base_data.py",
|
||||
"611": "freight.py",
|
||||
"612": "__init__.py",
|
||||
"613": "inventory.py",
|
||||
"614": "io_order.py",
|
||||
"615": "plan.py",
|
||||
"616": "transport_address.py",
|
||||
"617": "transport.py",
|
||||
"618": "vehicle_maintenance.py",
|
||||
"619": "vehicle_recharge.py",
|
||||
"620": "vehicle_urea.py",
|
||||
"621": "user.py",
|
||||
"622": "workflow.py",
|
||||
"623": "advance.py",
|
||||
"624": "attachment.py",
|
||||
"625": "base.py",
|
||||
"626": "expense_item.py",
|
||||
"627": "storage.py",
|
||||
"628": "workflow.py",
|
||||
"636": "customer_pool.py",
|
||||
"638": "__init__.py"
|
||||
"504": "PlanResourceSnapshotMixin"
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
.
|
||||
/Users/wuzilong/work/00code/shanxia2.0/backend
|
||||
@@ -0,0 +1,547 @@
|
||||
{
|
||||
"0": "Community 0",
|
||||
"1": "Community 1",
|
||||
"2": "Community 2",
|
||||
"3": "Community 3",
|
||||
"4": "Community 4",
|
||||
"5": "Community 5",
|
||||
"6": "Community 6",
|
||||
"7": "Community 7",
|
||||
"8": "Community 8",
|
||||
"9": "Community 9",
|
||||
"10": "Community 10",
|
||||
"11": "Community 11",
|
||||
"12": "Community 12",
|
||||
"13": "Community 13",
|
||||
"14": "Community 14",
|
||||
"15": "Community 15",
|
||||
"16": "Community 16",
|
||||
"17": "Community 17",
|
||||
"18": "Community 18",
|
||||
"19": "Community 19",
|
||||
"20": "Community 20",
|
||||
"21": "Community 21",
|
||||
"22": "Community 22",
|
||||
"23": "Community 23",
|
||||
"24": "Community 24",
|
||||
"25": "Community 25",
|
||||
"26": "Community 26",
|
||||
"27": "Community 27",
|
||||
"28": "Community 28",
|
||||
"29": "Community 29",
|
||||
"30": "Community 30",
|
||||
"31": "Community 31",
|
||||
"32": "Community 32",
|
||||
"33": "Community 33",
|
||||
"34": "Community 34",
|
||||
"35": "Community 35",
|
||||
"36": "Community 36",
|
||||
"37": "Community 37",
|
||||
"38": "Community 38",
|
||||
"39": "Community 39",
|
||||
"40": "Community 40",
|
||||
"41": "Community 41",
|
||||
"42": "Community 42",
|
||||
"43": "Community 43",
|
||||
"44": "Community 44",
|
||||
"45": "Community 45",
|
||||
"46": "Community 46",
|
||||
"47": "Community 47",
|
||||
"48": "Community 48",
|
||||
"49": "Community 49",
|
||||
"50": "Community 50",
|
||||
"51": "Community 51",
|
||||
"52": "Community 52",
|
||||
"53": "Community 53",
|
||||
"54": "Community 54",
|
||||
"55": "Community 55",
|
||||
"56": "Community 56",
|
||||
"57": "Community 57",
|
||||
"58": "Community 58",
|
||||
"59": "Community 59",
|
||||
"60": "Community 60",
|
||||
"61": "Community 61",
|
||||
"62": "Community 62",
|
||||
"63": "Community 63",
|
||||
"64": "Community 64",
|
||||
"65": "Community 65",
|
||||
"66": "Community 66",
|
||||
"67": "Community 67",
|
||||
"68": "Community 68",
|
||||
"69": "Community 69",
|
||||
"70": "Community 70",
|
||||
"71": "Community 71",
|
||||
"72": "Community 72",
|
||||
"73": "Community 73",
|
||||
"74": "Community 74",
|
||||
"75": "Community 75",
|
||||
"76": "Community 76",
|
||||
"77": "Community 77",
|
||||
"78": "Community 78",
|
||||
"79": "Community 79",
|
||||
"80": "Community 80",
|
||||
"81": "Community 81",
|
||||
"82": "Community 82",
|
||||
"83": "Community 83",
|
||||
"84": "Community 84",
|
||||
"85": "Community 85",
|
||||
"86": "Community 86",
|
||||
"87": "Community 87",
|
||||
"88": "Any",
|
||||
"89": "pl_ocr_service.py",
|
||||
"90": "Community 90",
|
||||
"91": "pl_vehicle_recharge_service.py",
|
||||
"92": "Community 92",
|
||||
"93": "_invoice_info_by_customer_impl",
|
||||
"94": "response.py",
|
||||
"95": "Community 95",
|
||||
"96": "Community 96",
|
||||
"97": "_ensure_subpack_slip_no",
|
||||
"98": "Community 98",
|
||||
"99": "Community 99",
|
||||
"100": "price_letter_detail",
|
||||
"101": ".to_tenant_user_dict",
|
||||
"102": "dept.py",
|
||||
"103": "Community 103",
|
||||
"104": "apply_crm_create_by_scope",
|
||||
"105": "list_customers",
|
||||
"106": "Community 106",
|
||||
"107": "apply_pl_scope",
|
||||
"108": "Community 108",
|
||||
"109": "Community 109",
|
||||
"110": "Base",
|
||||
"111": "Community 111",
|
||||
"112": "SysApiRequestLog",
|
||||
"113": "quote_share_log.py",
|
||||
"114": "db_models.py",
|
||||
"115": "site.py",
|
||||
"116": "Community 116",
|
||||
"117": "Community 117",
|
||||
"118": "Community 118",
|
||||
"119": "Community 119",
|
||||
"120": "Community 120",
|
||||
"121": "Community 121",
|
||||
"122": "Community 122",
|
||||
"123": "Community 123",
|
||||
"124": "Community 124",
|
||||
"125": "Community 125",
|
||||
"126": "customer_pool.py",
|
||||
"127": "Community 127",
|
||||
"128": "plan.py",
|
||||
"129": "BaseStorage",
|
||||
"130": "datetime_parse.py",
|
||||
"131": "LocalStorage",
|
||||
"132": "crm_supplier_hazard.py",
|
||||
"133": "Community 133",
|
||||
"134": "Community 134",
|
||||
"135": "unsettled_plans_q_filter",
|
||||
"136": "Community 136",
|
||||
"137": "statistics.py",
|
||||
"138": "Community 138",
|
||||
"139": "get_current_user_by_token",
|
||||
"140": "Community 140",
|
||||
"141": "PlTransport",
|
||||
"142": "Community 142",
|
||||
"143": "SysEthnic",
|
||||
"144": "dict_data.py",
|
||||
"145": "resolve_crm_product",
|
||||
"146": "__init__.py",
|
||||
"147": "Community 147",
|
||||
"148": "storage.py",
|
||||
"149": "Community 149",
|
||||
"150": "apply_site_feedback",
|
||||
"151": "Community 151",
|
||||
"152": "response.py",
|
||||
"153": "Community 153",
|
||||
"154": "response.py",
|
||||
"155": "Community 155",
|
||||
"156": "utils.py",
|
||||
"157": "Community 157",
|
||||
"158": "Community 158",
|
||||
"159": "customer_update",
|
||||
"160": "register_local_callback",
|
||||
"161": "backfill_crm_contract_attachment_urls.py",
|
||||
"162": ".to_tenant_user_dict",
|
||||
"163": "__init__.py",
|
||||
"164": "get_client_ip",
|
||||
"165": "Community 165",
|
||||
"166": "code:block1 (/iflytek/shanxia2.0/backend)",
|
||||
"167": "Community 167",
|
||||
"168": "vehicle_urea.py",
|
||||
"169": "product.py",
|
||||
"170": "test_quote_approver_setting.py",
|
||||
"171": "minio_url.py",
|
||||
"172": "apply_pl_scope",
|
||||
"173": "resolve_create_time_bounds",
|
||||
"174": "register_local_callback",
|
||||
"175": "vehicle_maintenance.py",
|
||||
"176": "_forbid_legacy_invoice_fields",
|
||||
"177": "vehicle_maintenance.py",
|
||||
"178": ".__init__",
|
||||
"179": "test_pl_vehicle_personnel_photo_schema.py",
|
||||
"180": "OSSStorage",
|
||||
"181": "allocate_next_biz_no",
|
||||
"182": "statistics.py",
|
||||
"183": "resolve_create_time_bounds",
|
||||
"184": "excel_export.py",
|
||||
"185": "vehicle_urea.py",
|
||||
"186": "transport_address.py",
|
||||
"187": "price_letter_detail",
|
||||
"188": "get_client_ip",
|
||||
"189": "create",
|
||||
"190": "menu.py",
|
||||
"191": "context.py",
|
||||
"192": "pool_rule.py",
|
||||
"193": "storage.py",
|
||||
"194": "get_current_user_by_token",
|
||||
"195": "PlFreightSettlementTransport",
|
||||
"196": "crm_contract_sub_routes.py",
|
||||
"197": "_coerce_resource_snapshot",
|
||||
"198": "user.py",
|
||||
"199": "price_letter_detail",
|
||||
"200": "assert_contract_editable_for_update",
|
||||
"202": "statistics.py",
|
||||
"206": "crm_quote_pdf.py",
|
||||
"207": "shanxia2.0 后端骨架(FastAPI)",
|
||||
"211": "leave.py",
|
||||
"216": "io_order.py",
|
||||
"217": "auth.py",
|
||||
"218": "pool_rule.py",
|
||||
"220": "api_crypto_middleware.py",
|
||||
"223": "workflow.py",
|
||||
"224": "supplier_info.py",
|
||||
"226": "get_active",
|
||||
"228": "advance.py",
|
||||
"230": "attachment.py",
|
||||
"232": "transport_address.py",
|
||||
"233": "customer_pool.py",
|
||||
"235": "invoice_info.py",
|
||||
"236": "price_letter.py",
|
||||
"237": "order_status_compat.py",
|
||||
"239": "shanxia2.0 Backend 生产部署说明",
|
||||
"240": "delete",
|
||||
"242": "leave.py",
|
||||
"244": "expense_item.py",
|
||||
"245": "__init__.py",
|
||||
"246": "db_collation.py",
|
||||
"247": "expense_item.py",
|
||||
"249": "__init__.py",
|
||||
"250": "invoice.py",
|
||||
"251": "menu.py",
|
||||
"252": "public_inventory.py",
|
||||
"254": "Activate.ps1",
|
||||
"256": "statistics.py",
|
||||
"259": "一次上传一条发票:多文件挂同一档案,金额为总额,只写一条额度流水。",
|
||||
"262": "reimburse.py",
|
||||
"263": "__init__.py",
|
||||
"264": "__init__.py",
|
||||
"265": "__init__.py",
|
||||
"266": "workflow.py",
|
||||
"269": "__init__.py",
|
||||
"270": "leave.py",
|
||||
"271": "menu.py",
|
||||
"272": "__init__.py",
|
||||
"273": "__init__.py",
|
||||
"274": "__init__.py",
|
||||
"275": "__init__.py",
|
||||
"276": "__init__.py",
|
||||
"278": "__init__.py",
|
||||
"281": "__init__.py",
|
||||
"286": "send_delivery_notice",
|
||||
"287": "__init__.py",
|
||||
"288": "__init__.py",
|
||||
"289": "send_delivery_warning",
|
||||
"292": "send_order_status",
|
||||
"293": "__init__.py",
|
||||
"294": "__init__.py",
|
||||
"295": "__init__.py",
|
||||
"296": "__init__.py",
|
||||
"297": "__init__.py",
|
||||
"298": "__init__.py",
|
||||
"299": "__init__.py",
|
||||
"300": "__init__.py",
|
||||
"301": "__init__.py",
|
||||
"302": "send_payment_success",
|
||||
"303": "send_refund_result",
|
||||
"304": "__init__.py",
|
||||
"305": "__init__.py",
|
||||
"306": "__init__.py",
|
||||
"307": "__init__.py",
|
||||
"308": "__init__.py",
|
||||
"309": "__init__.py",
|
||||
"310": "__init__.py",
|
||||
"311": "__init__.py",
|
||||
"312": "__init__.py",
|
||||
"313": "send_urge_notification",
|
||||
"314": "get",
|
||||
"315": "get_multi_with_filters",
|
||||
"317": "base.py",
|
||||
"318": "clue.py",
|
||||
"320": "customer_pool.py",
|
||||
"321": "customer.py",
|
||||
"323": "invoice_info.py",
|
||||
"326": "price_letter.py",
|
||||
"327": "product.py",
|
||||
"328": "quote.py",
|
||||
"329": "user.py",
|
||||
"330": "base.py",
|
||||
"331": "__tablename__",
|
||||
"332": "__init__.py",
|
||||
"333": "advance.py",
|
||||
"334": "validate_expense_item_id",
|
||||
"335": "attachment.py",
|
||||
"336": "common.py",
|
||||
"337": "validate_parent_id",
|
||||
"338": "__init__.py",
|
||||
"339": "invoice.py",
|
||||
"340": "coerce_applicant_id",
|
||||
"341": "details_must_not_be_empty",
|
||||
"342": "reimburse.py",
|
||||
"343": "validate_advance_id",
|
||||
"344": "validate_expense_item_id",
|
||||
"345": "validate_approver_id",
|
||||
"346": "common.py",
|
||||
"347": "contact.py",
|
||||
"348": "customer_follow.py",
|
||||
"349": "customer.py",
|
||||
"350": "dashboard.py",
|
||||
"351": "__init__.py",
|
||||
"352": "inventory.py",
|
||||
"353": "plans.py",
|
||||
"354": "weighing.py",
|
||||
"355": "__init__.py",
|
||||
"356": "freight.py",
|
||||
"357": "__init__.py",
|
||||
"358": "plan.py",
|
||||
"360": "validate_coordinates",
|
||||
"361": "transport.py",
|
||||
"362": "validate_amount",
|
||||
"363": "validate_amount",
|
||||
"364": "validate_recharge_type",
|
||||
"365": "validate_amount",
|
||||
"366": "validate_apply_amount",
|
||||
"367": "validate_quantity",
|
||||
"368": "approve",
|
||||
"369": "create",
|
||||
"370": "finance_issue_stats",
|
||||
"371": "get_by_id",
|
||||
"372": "get_paged",
|
||||
"373": "global_status_counts",
|
||||
"374": "issue",
|
||||
"375": "_latest_approval_per_node",
|
||||
"376": "_my_apply_base",
|
||||
"377": "my_apply_stats",
|
||||
"378": "offset_amount_map",
|
||||
"379": "pending_approval_stats",
|
||||
"380": "advance.py",
|
||||
"381": "_query_approvals",
|
||||
"382": "reject",
|
||||
"383": "soft_delete",
|
||||
"384": "submit",
|
||||
"385": "_tenant_no",
|
||||
"386": "update",
|
||||
"387": "_workflow_nodes",
|
||||
"389": "create",
|
||||
"390": "get_by_biz",
|
||||
"391": "get_by_id",
|
||||
"392": "attachment.py",
|
||||
"393": "soft_delete",
|
||||
"394": "_tenant_no",
|
||||
"395": "get_by_id",
|
||||
"396": "get_paged",
|
||||
"397": "base.py",
|
||||
"398": "soft_delete",
|
||||
"399": "update",
|
||||
"401": "_build_tree",
|
||||
"402": "create",
|
||||
"403": "get_by_id",
|
||||
"404": "get_item_tree",
|
||||
"405": "expense_item.py",
|
||||
"406": "soft_delete",
|
||||
"407": "_tenant_no",
|
||||
"408": "update",
|
||||
"409": "_count_invoices",
|
||||
"410": "create",
|
||||
"411": "delete",
|
||||
"412": "get_mine_summary",
|
||||
"413": "is_wallet_admin",
|
||||
"414": "add_entry",
|
||||
"415": "assert_can_delete_invoice",
|
||||
"416": "calc_summary",
|
||||
"417": "debit_reimburse",
|
||||
"418": "list_entries",
|
||||
"419": "_lock_summary",
|
||||
"420": "refresh_summary",
|
||||
"421": "release_reimburse",
|
||||
"422": "list_ledgers",
|
||||
"423": "list_wallets",
|
||||
"424": "_lookup_user_name",
|
||||
"425": "out_rows_with_attachments",
|
||||
"426": "invoice.py",
|
||||
"427": "_require_wallet_admin",
|
||||
"428": "_resolve_owner",
|
||||
"429": "_resolve_owner_for_query",
|
||||
"430": "_summary_to_dict",
|
||||
"431": "_tenant_no",
|
||||
"432": "approve",
|
||||
"433": "batch_offset",
|
||||
"434": "build_apply_out_with_details",
|
||||
"435": "build_expense_name_condition",
|
||||
"436": "can_change_applicant",
|
||||
"437": "create_with_details",
|
||||
"438": "finance_payment_stats",
|
||||
"439": "get_by_id",
|
||||
"440": "get_details",
|
||||
"441": "get_details_map_by_reimburse_ids",
|
||||
"442": "get_linked_invoice_ids",
|
||||
"443": "get_linked_invoice_ids_map",
|
||||
"444": "get_linked_invoices_brief",
|
||||
"445": "get_paged",
|
||||
"446": "global_status_counts",
|
||||
"447": "_has_debit_reimburse",
|
||||
"448": "_latest_approval_per_node",
|
||||
"449": "_my_apply_base",
|
||||
"450": "my_apply_stats",
|
||||
"451": "offset",
|
||||
"452": "out_one_with_attachment_counts",
|
||||
"453": "out_rows_with_attachment_counts",
|
||||
"454": "pay",
|
||||
"455": "reimburse.py",
|
||||
"456": "_query_approvals",
|
||||
"457": "reject",
|
||||
"458": "resolve_applicant",
|
||||
"459": "soft_delete",
|
||||
"460": "submit",
|
||||
"461": "sync_reimburse_invoices",
|
||||
"462": "_tenant_no",
|
||||
"463": "update_with_details",
|
||||
"464": "withdraw",
|
||||
"465": "_workflow_nodes",
|
||||
"466": "ensure_by_biz_type",
|
||||
"467": "get_by_biz_type",
|
||||
"468": "get_by_id",
|
||||
"469": "get_paged",
|
||||
"470": "workflow.py",
|
||||
"471": "save_nodes",
|
||||
"472": "soft_delete",
|
||||
"473": "_tenant_no",
|
||||
"474": "update",
|
||||
"487": "_get_variable",
|
||||
"488": "_parse_node_config",
|
||||
"489": "_parse_variables",
|
||||
"490": "customer_pool.py",
|
||||
"491": "__init__.py",
|
||||
"495": "code:bash (cd /path/to/shanxia2.0)",
|
||||
"496": "code:bash (./start-prod.sh status # 查看状态)",
|
||||
"497": "code:bash (curl -s http://127.0.0.1:9111/health)",
|
||||
"498": "code:bash (cd /iflytek/shanxia2.0/backend)",
|
||||
"499": "code:bash (python -m venv venv)",
|
||||
"500": "code:bash (pip install -r requirements.txt)",
|
||||
"501": "code:env (# 数据库配置)",
|
||||
"502": "code:bash (uvicorn app.main:app --reload --port 8000)",
|
||||
"503": "BaseModel",
|
||||
"504": "PlanResourceSnapshotMixin",
|
||||
"510": "advance.py",
|
||||
"511": "attachment.py",
|
||||
"512": "expense_item.py",
|
||||
"514": "workflow.py",
|
||||
"517": "dashboard.py",
|
||||
"518": "__init__.py",
|
||||
"519": "inventory.py",
|
||||
"520": "plans.py",
|
||||
"521": "public_inventory.py",
|
||||
"522": "weighing.py",
|
||||
"523": "__init__.py",
|
||||
"524": "leave.py",
|
||||
"525": "menu.py",
|
||||
"526": "base_data.py",
|
||||
"527": "freight.py",
|
||||
"528": "__init__.py",
|
||||
"529": "inventory.py",
|
||||
"530": "io_order.py",
|
||||
"531": "plan.py",
|
||||
"533": "transport_address.py",
|
||||
"534": "transport.py",
|
||||
"535": "vehicle_maintenance.py",
|
||||
"536": "vehicle_recharge.py",
|
||||
"537": "vehicle_urea.py",
|
||||
"538": "workflow.py",
|
||||
"539": "auth.py",
|
||||
"545": "__init__.py",
|
||||
"548": "storage.py",
|
||||
"551": "base.py",
|
||||
"553": "clue.py",
|
||||
"554": "contact.py",
|
||||
"555": "contract.py",
|
||||
"556": "customer_follow.py",
|
||||
"557": "customer_pool.py",
|
||||
"558": "customer.py",
|
||||
"559": "invoice_info.py",
|
||||
"560": "opportunity.py",
|
||||
"561": "pool_rule.py",
|
||||
"562": "price_letter.py",
|
||||
"563": "product.py",
|
||||
"564": "quote.py",
|
||||
"565": "statistics.py",
|
||||
"566": "supplier_info.py",
|
||||
"569": "__init__.py",
|
||||
"570": "leave.py",
|
||||
"571": "menu.py",
|
||||
"573": "user.py",
|
||||
"574": "workflow.py",
|
||||
"576": "base.py",
|
||||
"577": "__init__.py",
|
||||
"578": "attachment.py",
|
||||
"579": "auth.py",
|
||||
"580": "advance.py",
|
||||
"581": "attachment.py",
|
||||
"582": "common.py",
|
||||
"583": "expense_item.py",
|
||||
"584": "__init__.py",
|
||||
"585": "workflow.py",
|
||||
"586": "common.py",
|
||||
"587": "clue.py",
|
||||
"588": "contact.py",
|
||||
"589": "contract.py",
|
||||
"590": "customer_follow.py",
|
||||
"591": "customer_pool.py",
|
||||
"592": "customer.py",
|
||||
"593": "invoice_info.py",
|
||||
"594": "opportunity.py",
|
||||
"595": "pool_rule.py",
|
||||
"596": "price_letter.py",
|
||||
"597": "product.py",
|
||||
"598": "quote.py",
|
||||
"599": "statistics.py",
|
||||
"600": "supplier_info.py",
|
||||
"601": "dashboard.py",
|
||||
"602": "__init__.py",
|
||||
"603": "inventory.py",
|
||||
"604": "plans.py",
|
||||
"605": "public_inventory.py",
|
||||
"606": "weighing.py",
|
||||
"607": "__init__.py",
|
||||
"608": "leave.py",
|
||||
"609": "menu.py",
|
||||
"610": "base_data.py",
|
||||
"611": "freight.py",
|
||||
"612": "__init__.py",
|
||||
"613": "inventory.py",
|
||||
"614": "io_order.py",
|
||||
"615": "plan.py",
|
||||
"616": "transport_address.py",
|
||||
"617": "transport.py",
|
||||
"618": "vehicle_maintenance.py",
|
||||
"619": "vehicle_recharge.py",
|
||||
"620": "vehicle_urea.py",
|
||||
"621": "user.py",
|
||||
"622": "workflow.py",
|
||||
"623": "advance.py",
|
||||
"624": "attachment.py",
|
||||
"625": "base.py",
|
||||
"626": "expense_item.py",
|
||||
"627": "storage.py",
|
||||
"628": "workflow.py",
|
||||
"636": "customer_pool.py",
|
||||
"638": "__init__.py"
|
||||
}
|
||||
@@ -0,0 +1,837 @@
|
||||
# Graph Report - backend (2026-08-17)
|
||||
|
||||
## Corpus Check
|
||||
- 326 files · ~215,608 words
|
||||
- Verdict: corpus is large enough that graph structure adds value.
|
||||
|
||||
## Summary
|
||||
- 4819 nodes · 16536 edges · 545 communities (527 shown, 18 thin omitted)
|
||||
- Extraction: 96% EXTRACTED · 4% INFERRED · 0% AMBIGUOUS · INFERRED: 730 edges (avg confidence: 0.62)
|
||||
- Token cost: 0 input · 0 output
|
||||
|
||||
## Graph Freshness
|
||||
- Built from commit: `9345cf4a`
|
||||
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
|
||||
- Run `graphify update .` after code changes (no API cost).
|
||||
|
||||
## Community Hubs (Navigation)
|
||||
- [[_COMMUNITY_Community 0|Community 0]]
|
||||
- [[_COMMUNITY_Community 1|Community 1]]
|
||||
- [[_COMMUNITY_Community 2|Community 2]]
|
||||
- [[_COMMUNITY_Community 3|Community 3]]
|
||||
- [[_COMMUNITY_Community 4|Community 4]]
|
||||
- [[_COMMUNITY_Community 5|Community 5]]
|
||||
- [[_COMMUNITY_Community 6|Community 6]]
|
||||
- [[_COMMUNITY_Community 7|Community 7]]
|
||||
- [[_COMMUNITY_Community 8|Community 8]]
|
||||
- [[_COMMUNITY_Community 11|Community 11]]
|
||||
- [[_COMMUNITY_Community 12|Community 12]]
|
||||
- [[_COMMUNITY_Community 13|Community 13]]
|
||||
- [[_COMMUNITY_Community 14|Community 14]]
|
||||
- [[_COMMUNITY_Community 15|Community 15]]
|
||||
- [[_COMMUNITY_Community 16|Community 16]]
|
||||
- [[_COMMUNITY_Community 18|Community 18]]
|
||||
- [[_COMMUNITY_Community 19|Community 19]]
|
||||
- [[_COMMUNITY_Community 21|Community 21]]
|
||||
- [[_COMMUNITY_Community 22|Community 22]]
|
||||
- [[_COMMUNITY_Community 23|Community 23]]
|
||||
- [[_COMMUNITY_Community 24|Community 24]]
|
||||
- [[_COMMUNITY_Community 25|Community 25]]
|
||||
- [[_COMMUNITY_Community 26|Community 26]]
|
||||
- [[_COMMUNITY_Community 27|Community 27]]
|
||||
- [[_COMMUNITY_Community 28|Community 28]]
|
||||
- [[_COMMUNITY_Community 29|Community 29]]
|
||||
- [[_COMMUNITY_Community 30|Community 30]]
|
||||
- [[_COMMUNITY_Community 33|Community 33]]
|
||||
- [[_COMMUNITY_Community 34|Community 34]]
|
||||
- [[_COMMUNITY_Community 35|Community 35]]
|
||||
- [[_COMMUNITY_Community 36|Community 36]]
|
||||
- [[_COMMUNITY_Community 37|Community 37]]
|
||||
- [[_COMMUNITY_Community 38|Community 38]]
|
||||
- [[_COMMUNITY_Community 39|Community 39]]
|
||||
- [[_COMMUNITY_Community 42|Community 42]]
|
||||
- [[_COMMUNITY_Community 43|Community 43]]
|
||||
- [[_COMMUNITY_Community 44|Community 44]]
|
||||
- [[_COMMUNITY_Community 45|Community 45]]
|
||||
- [[_COMMUNITY_Community 46|Community 46]]
|
||||
- [[_COMMUNITY_Community 48|Community 48]]
|
||||
- [[_COMMUNITY_Community 49|Community 49]]
|
||||
- [[_COMMUNITY_Community 50|Community 50]]
|
||||
- [[_COMMUNITY_Community 51|Community 51]]
|
||||
- [[_COMMUNITY_Community 52|Community 52]]
|
||||
- [[_COMMUNITY_Community 53|Community 53]]
|
||||
- [[_COMMUNITY_Community 54|Community 54]]
|
||||
- [[_COMMUNITY_Community 58|Community 58]]
|
||||
- [[_COMMUNITY_Community 59|Community 59]]
|
||||
- [[_COMMUNITY_Community 60|Community 60]]
|
||||
- [[_COMMUNITY_Community 61|Community 61]]
|
||||
- [[_COMMUNITY_Community 62|Community 62]]
|
||||
- [[_COMMUNITY_Community 63|Community 63]]
|
||||
- [[_COMMUNITY_Community 64|Community 64]]
|
||||
- [[_COMMUNITY_Community 65|Community 65]]
|
||||
- [[_COMMUNITY_Community 66|Community 66]]
|
||||
- [[_COMMUNITY_Community 67|Community 67]]
|
||||
- [[_COMMUNITY_Community 68|Community 68]]
|
||||
- [[_COMMUNITY_Community 69|Community 69]]
|
||||
- [[_COMMUNITY_Community 70|Community 70]]
|
||||
- [[_COMMUNITY_Community 71|Community 71]]
|
||||
- [[_COMMUNITY_Community 72|Community 72]]
|
||||
- [[_COMMUNITY_Community 73|Community 73]]
|
||||
- [[_COMMUNITY_Community 74|Community 74]]
|
||||
- [[_COMMUNITY_Community 77|Community 77]]
|
||||
- [[_COMMUNITY_Community 78|Community 78]]
|
||||
- [[_COMMUNITY_Community 81|Community 81]]
|
||||
- [[_COMMUNITY_Community 83|Community 83]]
|
||||
- [[_COMMUNITY_Community 84|Community 84]]
|
||||
- [[_COMMUNITY_Community 86|Community 86]]
|
||||
- [[_COMMUNITY_Community 87|Community 87]]
|
||||
- [[_COMMUNITY_Any|Any]]
|
||||
- [[_COMMUNITY_pl_ocr_service.py|pl_ocr_service.py]]
|
||||
- [[_COMMUNITY_pl_vehicle_recharge_service.py|pl_vehicle_recharge_service.py]]
|
||||
- [[_COMMUNITY_Community 92|Community 92]]
|
||||
- [[_COMMUNITY__invoice_info_by_customer_impl|_invoice_info_by_customer_impl]]
|
||||
- [[_COMMUNITY_response.py|response.py]]
|
||||
- [[_COMMUNITY_Community 95|Community 95]]
|
||||
- [[_COMMUNITY__ensure_subpack_slip_no|_ensure_subpack_slip_no]]
|
||||
- [[_COMMUNITY_price_letter_detail|price_letter_detail]]
|
||||
- [[_COMMUNITY_.to_tenant_user_dict|.to_tenant_user_dict]]
|
||||
- [[_COMMUNITY_dept.py|dept.py]]
|
||||
- [[_COMMUNITY_apply_crm_create_by_scope|apply_crm_create_by_scope]]
|
||||
- [[_COMMUNITY_list_customers|list_customers]]
|
||||
- [[_COMMUNITY_Community 106|Community 106]]
|
||||
- [[_COMMUNITY_apply_pl_scope|apply_pl_scope]]
|
||||
- [[_COMMUNITY_Community 109|Community 109]]
|
||||
- [[_COMMUNITY_Base|Base]]
|
||||
- [[_COMMUNITY_Community 111|Community 111]]
|
||||
- [[_COMMUNITY_SysApiRequestLog|SysApiRequestLog]]
|
||||
- [[_COMMUNITY_quote_share_log.py|quote_share_log.py]]
|
||||
- [[_COMMUNITY_db_models.py|db_models.py]]
|
||||
- [[_COMMUNITY_site.py|site.py]]
|
||||
- [[_COMMUNITY_Community 116|Community 116]]
|
||||
- [[_COMMUNITY_Community 118|Community 118]]
|
||||
- [[_COMMUNITY_Community 119|Community 119]]
|
||||
- [[_COMMUNITY_Community 122|Community 122]]
|
||||
- [[_COMMUNITY_Community 125|Community 125]]
|
||||
- [[_COMMUNITY_customer_pool.py|customer_pool.py]]
|
||||
- [[_COMMUNITY_Community 127|Community 127]]
|
||||
- [[_COMMUNITY_plan.py|plan.py]]
|
||||
- [[_COMMUNITY_BaseStorage|BaseStorage]]
|
||||
- [[_COMMUNITY_datetime_parse.py|datetime_parse.py]]
|
||||
- [[_COMMUNITY_LocalStorage|LocalStorage]]
|
||||
- [[_COMMUNITY_crm_supplier_hazard.py|crm_supplier_hazard.py]]
|
||||
- [[_COMMUNITY_Community 134|Community 134]]
|
||||
- [[_COMMUNITY_unsettled_plans_q_filter|unsettled_plans_q_filter]]
|
||||
- [[_COMMUNITY_Community 136|Community 136]]
|
||||
- [[_COMMUNITY_statistics.py|statistics.py]]
|
||||
- [[_COMMUNITY_Community 138|Community 138]]
|
||||
- [[_COMMUNITY_get_current_user_by_token|get_current_user_by_token]]
|
||||
- [[_COMMUNITY_PlTransport|PlTransport]]
|
||||
- [[_COMMUNITY_SysEthnic|SysEthnic]]
|
||||
- [[_COMMUNITY_dict_data.py|dict_data.py]]
|
||||
- [[_COMMUNITY_resolve_crm_product|resolve_crm_product]]
|
||||
- [[_COMMUNITY___init__.py|__init__.py]]
|
||||
- [[_COMMUNITY_Community 147|Community 147]]
|
||||
- [[_COMMUNITY_apply_site_feedback|apply_site_feedback]]
|
||||
- [[_COMMUNITY_Community 151|Community 151]]
|
||||
- [[_COMMUNITY_response.py|response.py]]
|
||||
- [[_COMMUNITY_response.py|response.py]]
|
||||
- [[_COMMUNITY_utils.py|utils.py]]
|
||||
- [[_COMMUNITY_customer_update|customer_update]]
|
||||
- [[_COMMUNITY_register_local_callback|register_local_callback]]
|
||||
- [[_COMMUNITY_backfill_crm_contract_attachment_urls.py|backfill_crm_contract_attachment_urls.py]]
|
||||
- [[_COMMUNITY_.to_tenant_user_dict|.to_tenant_user_dict]]
|
||||
- [[_COMMUNITY___init__.py|__init__.py]]
|
||||
- [[_COMMUNITY_get_client_ip|get_client_ip]]
|
||||
- [[_COMMUNITY_codeblock1 (iflytekshanxia2.0backend)|code:block1 (/iflytek/shanxia2.0/backend)]]
|
||||
- [[_COMMUNITY_test_quote_approver_setting.py|test_quote_approver_setting.py]]
|
||||
- [[_COMMUNITY_minio_url.py|minio_url.py]]
|
||||
- [[_COMMUNITY_apply_pl_scope|apply_pl_scope]]
|
||||
- [[_COMMUNITY_resolve_create_time_bounds|resolve_create_time_bounds]]
|
||||
- [[_COMMUNITY_register_local_callback|register_local_callback]]
|
||||
- [[_COMMUNITY_.__init__|.__init__]]
|
||||
- [[_COMMUNITY_test_pl_vehicle_personnel_photo_schema.py|test_pl_vehicle_personnel_photo_schema.py]]
|
||||
- [[_COMMUNITY_OSSStorage|OSSStorage]]
|
||||
- [[_COMMUNITY_allocate_next_biz_no|allocate_next_biz_no]]
|
||||
- [[_COMMUNITY_statistics.py|statistics.py]]
|
||||
- [[_COMMUNITY_resolve_create_time_bounds|resolve_create_time_bounds]]
|
||||
- [[_COMMUNITY_excel_export.py|excel_export.py]]
|
||||
- [[_COMMUNITY_price_letter_detail|price_letter_detail]]
|
||||
- [[_COMMUNITY_get_client_ip|get_client_ip]]
|
||||
- [[_COMMUNITY_context.py|context.py]]
|
||||
- [[_COMMUNITY_storage.py|storage.py]]
|
||||
- [[_COMMUNITY_get_current_user_by_token|get_current_user_by_token]]
|
||||
- [[_COMMUNITY_PlFreightSettlementTransport|PlFreightSettlementTransport]]
|
||||
- [[_COMMUNITY_crm_contract_sub_routes.py|crm_contract_sub_routes.py]]
|
||||
- [[_COMMUNITY_price_letter_detail|price_letter_detail]]
|
||||
- [[_COMMUNITY_assert_contract_editable_for_update|assert_contract_editable_for_update]]
|
||||
- [[_COMMUNITY_crm_quote_pdf.py|crm_quote_pdf.py]]
|
||||
- [[_COMMUNITY_shanxia2.0 后端骨架(FastAPI)|shanxia2.0 后端骨架(FastAPI)]]
|
||||
- [[_COMMUNITY_api_crypto_middleware.py|api_crypto_middleware.py]]
|
||||
- [[_COMMUNITY_order_status_compat.py|order_status_compat.py]]
|
||||
- [[_COMMUNITY_shanxia2.0 Backend 生产部署说明|shanxia2.0 Backend 生产部署说明]]
|
||||
- [[_COMMUNITY_db_collation.py|db_collation.py]]
|
||||
- [[_COMMUNITY_一次上传一条发票:多文件挂同一档案,金额为总额,只写一条额度流水。|一次上传一条发票:多文件挂同一档案,金额为总额,只写一条额度流水。]]
|
||||
- [[_COMMUNITY___init__.py|__init__.py]]
|
||||
- [[_COMMUNITY___init__.py|__init__.py]]
|
||||
- [[_COMMUNITY_codebash (cd pathtoshanxia2.0)|code:bash (cd /path/to/shanxia2.0)]]
|
||||
- [[_COMMUNITY_codebash (.start-prod.sh status 查看状态)|code:bash (./start-prod.sh status # 查看状态)]]
|
||||
- [[_COMMUNITY_codebash (curl -s http127.0.0.19111health)|code:bash (curl -s http://127.0.0.1:9111/health)]]
|
||||
- [[_COMMUNITY_codebash (cd iflytekshanxia2.0backend)|code:bash (cd /iflytek/shanxia2.0/backend)]]
|
||||
- [[_COMMUNITY_codebash (python -m venv venv)|code:bash (python -m venv venv)]]
|
||||
- [[_COMMUNITY_codebash (pip install -r requirements.txt)|code:bash (pip install -r requirements.txt)]]
|
||||
- [[_COMMUNITY_codeenv ( 数据库配置)|code:env (# 数据库配置)]]
|
||||
- [[_COMMUNITY_codebash (uvicorn app.mainapp --reload --port 8000)|code:bash (uvicorn app.main:app --reload --port 8000)]]
|
||||
- [[_COMMUNITY_BaseModel|BaseModel]]
|
||||
- [[_COMMUNITY_PlanResourceSnapshotMixin|PlanResourceSnapshotMixin]]
|
||||
|
||||
## God Nodes (most connected - your core abstractions)
|
||||
1. `BaseModel` - 452 edges
|
||||
2. `get_tenant_no()` - 350 edges
|
||||
3. `success_response()` - 268 edges
|
||||
4. `DbUserContext` - 254 edges
|
||||
5. `error_response()` - 247 edges
|
||||
6. `generate_uuid()` - 194 edges
|
||||
7. `BxUserContext` - 183 edges
|
||||
8. `crm_user_info()` - 137 edges
|
||||
9. `assert_row_deletable_by_creator()` - 83 edges
|
||||
10. `ApiResponse` - 79 edges
|
||||
|
||||
## Surprising Connections (you probably didn't know these)
|
||||
- `MigrationStats` --uses--> `Settings` [INFERRED]
|
||||
scripts/migrate_bx_data.py → app/core/config.py
|
||||
- `StepStats` --uses--> `Settings` [INFERRED]
|
||||
scripts/migrate_bx_data.py → app/core/config.py
|
||||
- `MigrationStats` --uses--> `Settings` [INFERRED]
|
||||
scripts/migrate_crm_clue.py → app/core/config.py
|
||||
- `StepStats` --uses--> `Settings` [INFERRED]
|
||||
scripts/migrate_crm_clue.py → app/core/config.py
|
||||
- `MigrationStats` --uses--> `Settings` [INFERRED]
|
||||
scripts/migrate_crm_contract.py → app/core/config.py
|
||||
|
||||
## Import Cycles
|
||||
- None detected.
|
||||
|
||||
## Communities (545 total, 18 thin omitted)
|
||||
|
||||
### Community 0 - "Community 0"
|
||||
Cohesion: 0.11
|
||||
Nodes (44): check_document_expiry(), document_reminder_stats(), get_inventory_ledger(), get_summary(), list_document_reminders(), list_transactions(), list_warnings(), date (+36 more)
|
||||
|
||||
### Community 1 - "Community 1"
|
||||
Cohesion: 0.07
|
||||
Nodes (40): CRM 合同 API 路由 — GET/PUT/DELETE/POST 对齐原 Java CRM, 运营端角色守卫:admin / super_admin 可访问。, 接口菜单权限守卫。super_admin 直接放行。, require_admin_or_super_admin(), require_menu_code(), ContractApproverSettingSaveBody, ContractApprovalActionBody, ContractApprovalPassBody (+32 more)
|
||||
|
||||
### Community 2 - "Community 2"
|
||||
Cohesion: 0.20
|
||||
Nodes (27): _apply_inventory_product_update(), _apply_inventory_update(), _base_query(), create_inventory(), create_link_for_plan(), delete_inventory(), _generate_batch_no(), _get_inventory() (+19 more)
|
||||
|
||||
### Community 3 - "Community 3"
|
||||
Cohesion: 0.07
|
||||
Nodes (65): attachment_download_url(), attachment_preview_url(), create_dept(), create_dict_data(), create_dict_type(), create_menu(), create_menu_favorite(), create_post() (+57 more)
|
||||
|
||||
### Community 4 - "Community 4"
|
||||
Cohesion: 0.17
|
||||
Nodes (43): _apply_list_filters(), assign_opportunities(), batch_update_expect_date(), batch_update_stage(), change_stage(), create_opportunity(), _decimal_value(), _enrich_user_names() (+35 more)
|
||||
|
||||
### Community 5 - "Community 5"
|
||||
Cohesion: 0.10
|
||||
Nodes (56): _generate_notify_message(), PlPlan, PlPlanProduct, PlVehicle, 计划产品明细表 pl_plan_product, PlanArchiveFreightBlock, PlanArchiveProductItem, PlanProductCreate (+48 more)
|
||||
|
||||
### Community 6 - "Community 6"
|
||||
Cohesion: 0.20
|
||||
Nodes (30): assign_customer(), batch_claim_customers(), check_duplicate_customer(), claim_customer(), create_customer(), deal_customer(), _enrich_user_names(), _ensure_customer_in_scope() (+22 more)
|
||||
|
||||
### Community 7 - "Community 7"
|
||||
Cohesion: 0.07
|
||||
Nodes (96): clue_check_convert(), clue_check_duplicate(), clue_detail(), clue_export(), clue_follow_statistics(), clue_import(), clue_list(), clue_mine() (+88 more)
|
||||
|
||||
### Community 8 - "Community 8"
|
||||
Cohesion: 0.13
|
||||
Nodes (50): register_quote_routes(), accept_quote(), assert_quote_editable_for_update(), calculate_quote_amount(), create_new_version(), create_quote(), _decimal_value(), _enrich_contact_fields() (+42 more)
|
||||
|
||||
### Community 11 - "Community 11"
|
||||
Cohesion: 0.20
|
||||
Nodes (33): PlVehicleUreaApply, PlVehicleUreaDisburse, 车辆尿素申请 pl_vehicle_urea_apply, 车辆尿素发放登记 pl_vehicle_urea_disburse, VehicleUreaApplyRead, VehicleUreaDisburseBoardItem, apply_to_read(), _assert_vehicle_exists() (+25 more)
|
||||
|
||||
### Community 12 - "Community 12"
|
||||
Cohesion: 0.05
|
||||
Nodes (68): contact_add(), contact_delete(), contact_update(), customer_delete(), BaseModel, AttachmentUploadResponse, ChangePasswordRequest, Config (+60 more)
|
||||
|
||||
### Community 13 - "Community 13"
|
||||
Cohesion: 0.09
|
||||
Nodes (58): advance_issue_stats(), batch_offset(), export_advance_issues(), issue_advance(), list_advance_issues(), list_advance_offset_records(), list_pending_payments(), list_processed_payments() (+50 more)
|
||||
|
||||
### Community 14 - "Community 14"
|
||||
Cohesion: 0.30
|
||||
Nodes (12): PlCarrier, assert_carrier_eligible_for_transport(), carrier_agreement_expired(), carrier_audit_approved(), carrier_eligible_for_transport(), carrier_qualification_complete(), date, 资质附件均为选填,保留函数签名供兼容调用。 (+4 more)
|
||||
|
||||
### Community 15 - "Community 15"
|
||||
Cohesion: 0.11
|
||||
Nodes (14): BxReimburseApply, BxReimburseApproval, BxReimburseDetail, BxReimburseOffset, 报销单勾选的发票附件(同一发票可勾选多个附件,各占一条)。, ReimburseInvoiceBriefOut, ReimburseInvoiceLinkIn, Any (+6 more)
|
||||
|
||||
### Community 16 - "Community 16"
|
||||
Cohesion: 0.16
|
||||
Nodes (37): PlVehicleRechargeApply, PlVehicleRechargeRecord, 车辆ETC/加油充值申请 pl_vehicle_recharge_apply, 车辆ETC/加油充值发放登记 pl_vehicle_recharge_record, 发放登记页:申请单 + 关联发放信息(左连接), VehicleRechargeDisburseBoardItem, VehicleRechargeRecordCreate, VehicleRechargeRecordRead (+29 more)
|
||||
|
||||
### Community 18 - "Community 18"
|
||||
Cohesion: 0.11
|
||||
Nodes (39): advance_pending_approval_stats(), advance_status_counts(), approve_advance(), approved_by_me(), create_advance(), delete_advance(), _fill_offset_amounts(), get_advance() (+31 more)
|
||||
|
||||
### Community 19 - "Community 19"
|
||||
Cohesion: 0.34
|
||||
Nodes (30): AttachmentDownloadUrlBody, DeptCreateBody, DeptUpdateBody, DictDataCreateBody, DictDataUpdateBody, DictTypeCreateBody, DictTypeUpdateBody, MenuCreateBody (+22 more)
|
||||
|
||||
### Community 21 - "Community 21"
|
||||
Cohesion: 0.09
|
||||
Nodes (58): _alias_row_to_dict(), _assert_brand_deletable(), barcode_exists(), _brand_row_to_dict(), _build_category_tree(), category_code_exists(), _category_row_to_dict(), _collect_category_with_ancestors() (+50 more)
|
||||
|
||||
### Community 22 - "Community 22"
|
||||
Cohesion: 0.08
|
||||
Nodes (28): parse_admin_request_json(), Any, Request, 运营 Web(/web/admin)JSON 请求体解析。 与 ApiCryptoMiddleware 配合:中间件未改写 body 时,在路由内解密 `{, 读取 JSON body;dict 时走 resolve_request_payload(明文 / 密文 / data 包装)。 Returns:, ApiCryptoError, _b64url_decode(), _b64url_encode() (+20 more)
|
||||
|
||||
### Community 23 - "Community 23"
|
||||
Cohesion: 0.15
|
||||
Nodes (6): convert_heic_to_jpeg(), process_image_content(), QiniuStorage, 将 HEIC/HEIF 格式转换为 JPEG 格式, 处理图片内容,如果是 HEIC 格式则转换为 JPEG, BytesIO
|
||||
|
||||
### Community 24 - "Community 24"
|
||||
Cohesion: 0.13
|
||||
Nodes (44): _apply_plan_product_update(), _apply_plan_update(), AutoGenerateMonthlyResult, _base_query(), create_plan(), delete_plan(), _generate_plan_no(), get_plan() (+36 more)
|
||||
|
||||
### Community 25 - "Community 25"
|
||||
Cohesion: 0.13
|
||||
Nodes (28): generate_uuid(), PlInventoryTransaction, 库存流水表 pl_inventory_transaction, load_brand_qtys_by_plan_product_ids(), persist_plan_product_brand_qtys(), Any, Decimal, Session (+20 more)
|
||||
|
||||
### Community 26 - "Community 26"
|
||||
Cohesion: 0.08
|
||||
Nodes (70): day_detail(), delete_assignment(), duty_user_options(), _enrich_assign_names(), month_summary(), Session, 指定用户时补全 personName(用 nick_name)。, 值班可安排人员:仅「值班人员」角色下的启用用户。 (+62 more)
|
||||
|
||||
### Community 27 - "Community 27"
|
||||
Cohesion: 0.11
|
||||
Nodes (32): CRM 报价单 API 路由 — 挂接到 crm.router, QuoteApproverSettingSaveBody, QuoteAcceptBody, QuoteApprovalApproveBody, QuoteApprovalListBody, QuoteApprovalRefreshNamesBody, QuoteApprovalWithdrawBody, QuoteCreateBody (+24 more)
|
||||
|
||||
### Community 28 - "Community 28"
|
||||
Cohesion: 0.06
|
||||
Nodes (94): approve_settlement_legacy(), cancel_settlement(), create_settlement(), delete_settlement(), disburse_settlement(), _generate_settlement_no(), get_settlement(), get_settlement_approval_flow() (+86 more)
|
||||
|
||||
### Community 29 - "Community 29"
|
||||
Cohesion: 0.08
|
||||
Nodes (96): register_contract_routes(), _apply_contract_entity_fields(), batch_assign_owner(), batch_submit_contract_approval(), _build_contract_filters(), _build_contract_name_from_parties(), _build_customer_address(), _calc_amount_with_tax() (+88 more)
|
||||
|
||||
### Community 30 - "Community 30"
|
||||
Cohesion: 0.16
|
||||
Nodes (26): clear_target_categories(), compute_level(), count_target_rows(), derive_source_url(), fetch_portal_admin_id(), fetch_source_rows(), legacy_id_to_str(), load_runtime_settings() (+18 more)
|
||||
|
||||
### Community 33 - "Community 33"
|
||||
Cohesion: 0.18
|
||||
Nodes (25): CRM 合同子模块路由 — 变更/模板/审批独立页, register_contract_sub_routes(), _change_row_to_dict(), create_contract_change(), execute_change(), export_contract_changes(), _get_change_row(), get_contract_change_detail() (+17 more)
|
||||
|
||||
### Community 34 - "Community 34"
|
||||
Cohesion: 0.26
|
||||
Nodes (6): start_instance(), StartInstanceBody, WfNode, Any, Session, WorkflowEngine
|
||||
|
||||
### Community 35 - "Community 35"
|
||||
Cohesion: 0.18
|
||||
Nodes (36): clear_portal_workflow(), _dec(), derive_source_url(), fetch_portal_admin_id(), fetch_source_rows(), legacy_id_to_str(), load_bx_user_mapping(), load_runtime_settings() (+28 more)
|
||||
|
||||
### Community 36 - "Community 36"
|
||||
Cohesion: 0.15
|
||||
Nodes (27): 验证密码(哈希非 bcrypt 或损坏时返回 False,避免抛错), verify_password(), change_password(), create_user(), get_user(), get_user_with_relations(), _hash_password(), list_post_ids_for_user() (+19 more)
|
||||
|
||||
### Community 37 - "Community 37"
|
||||
Cohesion: 0.20
|
||||
Nodes (24): count_source_rows(), derive_source_url(), fetch_portal_admin_id(), fetch_source_rows(), legacy_id_to_str(), load_runtime_settings(), load_settings_env(), load_user_mapping() (+16 more)
|
||||
|
||||
### Community 38 - "Community 38"
|
||||
Cohesion: 0.19
|
||||
Nodes (14): create_expense_item(), delete_expense_item(), get_expense_item(), get_expense_item_tree(), Session, update_expense_item(), get_bx_current_user(), BxExpenseItem (+6 more)
|
||||
|
||||
### Community 39 - "Community 39"
|
||||
Cohesion: 0.13
|
||||
Nodes (42): clue_follow_detail(), assign_clues(), check_convert(), check_duplicate_clue(), convert_clue(), create_clue(), _enrich_user_names(), _ensure_clue_in_scope() (+34 more)
|
||||
|
||||
### Community 42 - "Community 42"
|
||||
Cohesion: 0.19
|
||||
Nodes (32): _dec(), derive_source_url(), fetch_portal_admin_id(), fetch_source_rows(), legacy_id_to_str(), load_oss_id_to_url(), load_runtime_settings(), load_settings_env() (+24 more)
|
||||
|
||||
### Community 43 - "Community 43"
|
||||
Cohesion: 0.12
|
||||
Nodes (23): bx_health(), 报销(bx)API 路由聚合 — REST 风格,协议对齐 bx 原系统。, P0 健康检查:验证 bx 路由已挂载且响应协议为 code=0。, create_invoice(), delete_invoice(), get_mine_wallet_summary(), list_invoice_ledgers(), list_invoice_wallets() (+15 more)
|
||||
|
||||
### Community 44 - "Community 44"
|
||||
Cohesion: 0.18
|
||||
Nodes (30): _dec(), derive_source_url(), fetch_portal_admin_id(), fetch_source_rows(), legacy_id_to_str(), legacy_parent_id(), load_oss_id_to_url(), load_runtime_settings() (+22 more)
|
||||
|
||||
### Community 45 - "Community 45"
|
||||
Cohesion: 0.09
|
||||
Nodes (70): _build_driver_url(), get_plan_arrange_info(), 获取计划已安排运输后的司机链接与地磅出入库关联(供查看弹窗复制简述/二维码)。, _apply_plan_logistics_from_payload(), _apply_transport_arrange_fields(), arrange_transport(), _assert_vehicle_carrier_eligible(), _build_arrange_result() (+62 more)
|
||||
|
||||
### Community 46 - "Community 46"
|
||||
Cohesion: 0.16
|
||||
Nodes (32): Settings, BaseSettings, apply_biz_backfill(), BackfillRule, BizRef, build_biz_index(), count_target_rows(), derive_source_url() (+24 more)
|
||||
|
||||
### Community 48 - "Community 48"
|
||||
Cohesion: 0.17
|
||||
Nodes (22): cancel_apply(), create_apply(), create_disburse(), delete_applies(), delete_disburses(), get_apply(), get_disburse(), list_applies() (+14 more)
|
||||
|
||||
### Community 49 - "Community 49"
|
||||
Cohesion: 0.49
|
||||
Nodes (10): clue_statistics_level(), _apply_create_range(), _base_query(), get_clue_level_statistics(), get_clue_overview_statistics(), get_clue_owner_statistics(), get_clue_source_statistics(), get_clue_trend_statistics() (+2 more)
|
||||
|
||||
### Community 50 - "Community 50"
|
||||
Cohesion: 0.08
|
||||
Nodes (25): clue_assign(), clue_convert(), clue_create(), clue_delete(), clue_follow_create(), clue_follow_delete(), clue_follow_list(), clue_follow_update() (+17 more)
|
||||
|
||||
### Community 51 - "Community 51"
|
||||
Cohesion: 0.25
|
||||
Nodes (17): _cargo_stats(), get_dashboard_stats(), get_inbound_by_cargo(), get_inbound_by_partner_cargo(), get_outbound_by_cargo(), get_outbound_by_partner_cargo(), _partner_cargo_stats(), Session (+9 more)
|
||||
|
||||
### Community 52 - "Community 52"
|
||||
Cohesion: 0.18
|
||||
Nodes (29): _dec(), derive_source_url(), fetch_portal_admin_id(), fetch_source_rows(), legacy_id_to_str(), load_oss_id_to_url(), load_runtime_settings(), load_settings_env() (+21 more)
|
||||
|
||||
### Community 53 - "Community 53"
|
||||
Cohesion: 0.14
|
||||
Nodes (24): PlTransportProductBrandQty, PlTransportProductFeedback, 运输单按产品回传 pl_transport_product_feedback, 运输单产品回传品牌数量 pl_transport_product_brand_qty, _dict_field(), format_brand_qty_summary(), load_transport_driver_product_feedbacks(), persist_io_item_brand_qtys() (+16 more)
|
||||
|
||||
### Community 54 - "Community 54"
|
||||
Cohesion: 0.19
|
||||
Nodes (28): _get_by_plan(), get_inbound(), get_inbound_by_plan(), _get_order(), get_outbound(), get_outbound_by_plan(), inbound_site_url(), list_inbound() (+20 more)
|
||||
|
||||
### Community 58 - "Community 58"
|
||||
Cohesion: 0.28
|
||||
Nodes (17): _address_to_read(), _build_address_query(), create_transport_address(), delete_transport_address(), _get_address(), get_transport_address(), list_all_transport_addresses(), list_transport_addresses() (+9 more)
|
||||
|
||||
### Community 59 - "Community 59"
|
||||
Cohesion: 0.20
|
||||
Nodes (28): _dec(), derive_source_url(), fetch_portal_admin_id(), fetch_source_rows(), legacy_id_to_str(), load_oss_id_to_url(), load_runtime_settings(), load_settings_env() (+20 more)
|
||||
|
||||
### Community 60 - "Community 60"
|
||||
Cohesion: 0.12
|
||||
Nodes (16): 供方全量列表(下拉)— Java: CrmSupplierInfoController.listAll, 供方详情 — Java: CrmSupplierInfoController.getInfo, 新增供方 — Java: CrmSupplierInfoController.add, 修改供方 — Java: CrmSupplierInfoController.edit, 删除供方 — Java: CrmSupplierInfoController.remove, supplier_info_add(), supplier_info_delete(), supplier_info_detail() (+8 more)
|
||||
|
||||
### Community 61 - "Community 61"
|
||||
Cohesion: 0.11
|
||||
Nodes (42): PlTransportDbLink, 运输-地磅出入库关联 pl_transport_db_link, _apply_plan_quantity_delta(), _create_db_inventory(), _create_db_plan(), DibangInventoryLinkResult, find_monthly_db_plan(), _generate_batch_no() (+34 more)
|
||||
|
||||
### Community 62 - "Community 62"
|
||||
Cohesion: 0.16
|
||||
Nodes (27): _apply_weighing_product_update(), _apply_weighing_update(), _base_query(), create_weighing(), delete_weighing(), _get_weighing(), get_weighing_related(), list_weighing() (+19 more)
|
||||
|
||||
### Community 63 - "Community 63"
|
||||
Cohesion: 0.24
|
||||
Nodes (13): BxInvoiceLedger, BxInvoiceWalletSummary, InvoiceLedgerService, Any, Decimal, Session, should_skip_release(), summarize_entries() (+5 more)
|
||||
|
||||
### Community 64 - "Community 64"
|
||||
Cohesion: 0.30
|
||||
Nodes (15): paste_recognize_commit(), ConflictAction, PasteCommitBody, PasteCommitResult, PastePersonnelIn, PasteVehicleIn, 运输计划 - 三方粘贴识别提交 Schema, commit_paste_recognize() (+7 more)
|
||||
|
||||
### Community 65 - "Community 65"
|
||||
Cohesion: 0.16
|
||||
Nodes (40): PlVehicleMaintenanceApply, PlVehicleMaintenanceProcess, 车辆维保申请 pl_vehicle_maintenance_apply, 车辆维保处理登记 pl_vehicle_maintenance_process, Decimal, VehicleMaintenanceApplyCreate, VehicleMaintenanceApplyRead, VehicleMaintenanceApplyUpdate (+32 more)
|
||||
|
||||
### Community 66 - "Community 66"
|
||||
Cohesion: 0.14
|
||||
Nodes (21): create_workflow(), delete_workflow(), get_workflow(), get_workflow_by_biz_type(), list_workflows(), Session, save_workflow_nodes(), update_workflow() (+13 more)
|
||||
|
||||
### Community 67 - "Community 67"
|
||||
Cohesion: 0.12
|
||||
Nodes (36): get_password_hash(), Namespace, build_merged_users(), bx_role_labels(), check_minio(), check_mysql(), check_redis(), _clip() (+28 more)
|
||||
|
||||
### Community 68 - "Community 68"
|
||||
Cohesion: 0.33
|
||||
Nodes (13): _build_tree(), _cascade_soft_delete(), _collect_with_ancestors(), create_dept(), get_dept(), list_depts(), list_depts_tree(), Any (+5 more)
|
||||
|
||||
### Community 69 - "Community 69"
|
||||
Cohesion: 0.15
|
||||
Nodes (25): _append_user_scope_conditions(), apply_data_scope(), assert_row_in_data_scope(), _collect_scope_ids(), _get_child_dept_ids(), get_user_dept_id(), Session, 数据权限过滤(V0.3:基于角色 data_scope 的查询级行级过滤) data_scope 值: 1 = 全部数据 2 = 本部门数据 3 (+17 more)
|
||||
|
||||
### Community 70 - "Community 70"
|
||||
Cohesion: 0.19
|
||||
Nodes (16): align_contract_no_to_sign_date(), date, 标准编号替换日期段,保留序号;非标准返回 None。, 将标准合同编号日期段对齐签订日;撞号则序号顺延(含软删查重)。 非标准编号原样返回。无签订日则原样返回。, replace_contract_no_date(), _mock_last_contract(), 软删占用的编号仍占唯一索引,生成时须跳过(对齐 uk_contract_tenant_no)。, 取最大流水号须含软删,避免复用已软删的最大编号。 (+8 more)
|
||||
|
||||
### Community 71 - "Community 71"
|
||||
Cohesion: 0.13
|
||||
Nodes (26): PlCert, CertRead, parse_resource_snapshot(), Any, 运输计划 - 安排运输时车辆/人员/证件资源快照 Schema, 从 ORM 文本列或 dict 解析快照。, SnapshotCertRead, SnapshotPersonnelRow (+18 more)
|
||||
|
||||
### Community 72 - "Community 72"
|
||||
Cohesion: 0.20
|
||||
Nodes (23): bind_vehicle_driver(), create_driver(), create_personnel(), delete_carrier(), delete_driver(), delete_personnel(), delete_vehicle(), get_driver() (+15 more)
|
||||
|
||||
### Community 73 - "Community 73"
|
||||
Cohesion: 0.20
|
||||
Nodes (25): derive_source_url(), fetch_portal_admin_id(), fetch_source_rows(), legacy_id_to_str(), load_oss_id_to_url(), load_runtime_settings(), load_settings_env(), load_user_mapping() (+17 more)
|
||||
|
||||
### Community 74 - "Community 74"
|
||||
Cohesion: 0.12
|
||||
Nodes (21): list_public_vehicle_plates(), Session, 地磅公共只读:免登车牌 options(现场填写页), 免登:按出入库/计划解析 tenant_no,返回 pl_vehicle 车牌(self + carrier)。, _resolve_tenant_no(), menu_badge_stats(), Session, Session (+13 more)
|
||||
|
||||
### Community 77 - "Community 77"
|
||||
Cohesion: 0.11
|
||||
Nodes (12): get_wechat_pay_client(), get_wechat_pay_v2(), Any, 微信支付工具模块 支持微信支付 V3 API 文档参考: https://pay.weixin.qq.com/docs/v3/apis/index.html, 统一下单接口 (V3) Args: out_trade_no: 商户订单号 total_amo, 申请退款 Args: out_trade_no: 商户订单号 total_amount: 订单, 小程序调起支付的二次签名参数(与公众号 JSAPI 规则一致), 统一下单 (V2 API) Args: out_trade_no: 商户订单号 total_f (+4 more)
|
||||
|
||||
### Community 78 - "Community 78"
|
||||
Cohesion: 0.24
|
||||
Nodes (18): _actor_ids(), _actor_names(), assert_row_deletable_by_creator(), assert_rows_deletable_by_creator(), can_delete_by_creator(), is_super_admin(), normalize_user_info(), Any (+10 more)
|
||||
|
||||
### Community 81 - "Community 81"
|
||||
Cohesion: 0.29
|
||||
Nodes (12): build_name_pinyin_abbr(), normalize_name_pinyin_abbr(), 名称拼音首字母缩写:库内存小写,检索时大小写不敏感。, 保存时解析拼音字段。 - submitted_provided=True:用提交值(空则按 name 生成) - 否则:已有非空 existi, resolve_name_pinyin_abbr(), test_chinese_and_alnum(), test_empty_and_whitespace(), test_resolve_not_submitted_empty_existing_generates() (+4 more)
|
||||
|
||||
### Community 83 - "Community 83"
|
||||
Cohesion: 0.14
|
||||
Nodes (32): _advance_contract_after_approval(), approve_contract(), approve_contract_by_contract_id(), approve_pass(), approve_reject(), approve_transfer(), _build_approval_list_filters(), can_user_approve() (+24 more)
|
||||
|
||||
### Community 84 - "Community 84"
|
||||
Cohesion: 0.23
|
||||
Nodes (23): build_id_maps(), clip_text(), count_rows(), export_mapping(), fetch_rows(), legacy_id_to_str(), main(), map_id() (+15 more)
|
||||
|
||||
### Community 86 - "Community 86"
|
||||
Cohesion: 0.21
|
||||
Nodes (15): create_leave(), get_leave(), list_leaves(), Any, Session, _row_to_dict(), soft_delete_leave(), update_leave() (+7 more)
|
||||
|
||||
### Community 87 - "Community 87"
|
||||
Cohesion: 0.11
|
||||
Nodes (32): approve_quote(), can_user_approve(), create_approval_records(), delete_pending_approvals(), get_quote_approval(), invalidate_all_approvals(), list_quote_approvals(), Any (+24 more)
|
||||
|
||||
### Community 88 - "Any"
|
||||
Cohesion: 0.34
|
||||
Nodes (18): batch_add_opportunity_products(), _calc_subtotal(), create_opportunity_product(), _decimal_value(), _fill_product_redundant(), _get_opportunity_for_product(), get_opportunity_product(), list_opportunity_products() (+10 more)
|
||||
|
||||
### Community 89 - "pl_ocr_service.py"
|
||||
Cohesion: 0.19
|
||||
Nodes (22): PlOcrUsageMonth, 阿里云 OCR 月度调用计数 pl_ocr_usage_month(按自然月全局统计), assert_quota_available(), _build_ocr_client(), compress_image_bytes(), current_usage_month(), _first_attachment_url(), get_or_create_usage_row() (+14 more)
|
||||
|
||||
### Community 91 - "pl_vehicle_recharge_service.py"
|
||||
Cohesion: 0.13
|
||||
Nodes (34): apply_crm_create_by_scope(), ensure_crm_create_by_row_in_scope(), ensure_crm_customer_linked_row_in_scope(), ensure_crm_owner_row_in_scope(), Session, CRM 数据权限辅助(负责人 owner_user_id + 标准 create_by/create_dept_id)。, 客户子表(开票信息等):先校验关联客户 owner scope,否则校验 create_by。, _clear_default_for_customer() (+26 more)
|
||||
|
||||
### Community 92 - "Community 92"
|
||||
Cohesion: 0.20
|
||||
Nodes (21): _assert_users_exist(), get_setting(), Any, Session, _row_to_dict(), save_setting(), _assert_users_exist(), get_setting() (+13 more)
|
||||
|
||||
### Community 93 - "_invoice_info_by_customer_impl"
|
||||
Cohesion: 0.19
|
||||
Nodes (11): get_current_time(), parse_datetime(), datetime, allocate_next_biz_no(), _format_biz_no(), _prefix_for(), datetime, Session (+3 more)
|
||||
|
||||
### Community 94 - "response.py"
|
||||
Cohesion: 0.18
|
||||
Nodes (17): get_user_menu_codes(), Session, 读取用户授权的菜单权限标识集合(与前端 meta.menuCode / perms 对齐)。, _expand_with_ancestor_menu_ids(), _granted_menu_ids_for_user(), iter_user_menu_perm_codes(), _map_role_name_to_web(), Session (+9 more)
|
||||
|
||||
### Community 95 - "Community 95"
|
||||
Cohesion: 0.35
|
||||
Nodes (11): create_tenant(), get_tenant(), list_tenants(), Any, Session, _row_to_dict(), soft_delete_tenant(), tenant_no_exists() (+3 more)
|
||||
|
||||
### Community 97 - "_ensure_subpack_slip_no"
|
||||
Cohesion: 0.21
|
||||
Nodes (22): clear_target_products(), count_target_rows(), _dec(), derive_source_url(), fetch_portal_admin_id(), fetch_source_rows(), legacy_id_to_str(), load_runtime_settings() (+14 more)
|
||||
|
||||
### Community 100 - "price_letter_detail"
|
||||
Cohesion: 0.31
|
||||
Nodes (13): apply_renumber(), build_mapping(), fetch_products(), format_new_product_no(), load_settings_env(), main(), product_id_sort_key(), Engine (+5 more)
|
||||
|
||||
### Community 101 - ".to_tenant_user_dict"
|
||||
Cohesion: 0.16
|
||||
Nodes (18): _build_quote_filters(), _fill_contract_linked_count(), list_quotes(), _parse_optional_datetime(), _period_key(), Any, datetime, Query (+10 more)
|
||||
|
||||
### Community 102 - "dept.py"
|
||||
Cohesion: 0.25
|
||||
Nodes (10): _cert_url_blank(), plan_has_hazard_product_exists(), plan_has_incomplete_hazard_cert_exists(), 证书 URL 为空(NULL / 空串 / 纯空白)。, 列表筛选:计划下存在 CRM 标识为易制毒/易制爆的产品。, 列表筛选:存在危化产品且至少一张必需证书未上传。, _compile(), 危化品证上传列表筛选:EXISTS 子查询应覆盖 CRM 危化标识与证书缺口。 (+2 more)
|
||||
|
||||
### Community 104 - "apply_crm_create_by_scope"
|
||||
Cohesion: 0.29
|
||||
Nodes (15): create_price_letter(), get_price_letter(), list_price_letters(), _parse_date(), Any, date, Session, _row_to_dict() (+7 more)
|
||||
|
||||
### Community 105 - "list_customers"
|
||||
Cohesion: 0.15
|
||||
Nodes (9): 微信消息通知服务 支持微信小程序模板消息推送 使用方式: from app.core.wechat_message import WechatMes, 发送订单状态通知 Args: openid: 用户openid order_n, 发送支付成功通知 Args: openid: 用户openid order_n, 发送配送通知 Args: openid: 配送员openid task_no:, 发送配送时效预警 Args: openid: 配送员openid task_n, 发送退款结果通知 Args: openid: 用户openid order_n, 发送催单通知给配送员 Args: deliver_openid: 配送员openid, 发送模板消息 Args: openid: 用户openid template_ (+1 more)
|
||||
|
||||
### Community 106 - "Community 106"
|
||||
Cohesion: 0.12
|
||||
Nodes (15): pool_rule_add(), pool_rule_delete(), pool_rule_detail(), pool_rule_execute(), pool_rule_list(), pool_rule_update(), pool_rule_update_status(), PoolRuleAddBody (+7 more)
|
||||
|
||||
### Community 107 - "apply_pl_scope"
|
||||
Cohesion: 0.15
|
||||
Nodes (32): create_instance(), create_process_def(), create_task(), get_all_tasks_for_business(), get_instance(), get_latest_version(), get_nodes_by_version(), get_pending_tasks_by_node() (+24 more)
|
||||
|
||||
### Community 109 - "Community 109"
|
||||
Cohesion: 0.46
|
||||
Nodes (7): VehiclePersonnelRow, load_vehicle_personnel_rows(), Session, 运输计划 - 人员 upsert 与车辆-人员关联保存, 按 personnelId → phone → name 弱匹配 upsert,返回 personnel_id。, save_vehicle_personnel(), upsert_personnel()
|
||||
|
||||
### Community 110 - "Base"
|
||||
Cohesion: 0.31
|
||||
Nodes (9): decrypt_login_payload(), ensure_initialized(), extract_login_credentials(), get_public_pem(), Any, 登录凭据 RSA 解密(与前端 JSEncrypt PKCS#1 v1.5 兼容)。, Base64 密文 -> JSON 对象(须含 username、password、ts 毫秒时间戳)。, 从请求体解析用户名、密码。 返回 (username, password, error_message);error_message 非空时表示失败。 (+1 more)
|
||||
|
||||
### Community 111 - "Community 111"
|
||||
Cohesion: 0.28
|
||||
Nodes (14): _assert_editable(), _build_public_page(), _format_file_size(), _get_inventory_by_token(), get_public_inventory(), _parse_feedback_images(), Session, UploadFile (+6 more)
|
||||
|
||||
### Community 112 - "SysApiRequestLog"
|
||||
Cohesion: 0.47
|
||||
Nodes (5): list_ethnics(), Any, Session, 民族码表 CRUD(全局 sys_ethnic), _row_to_dict()
|
||||
|
||||
### Community 113 - "quote_share_log.py"
|
||||
Cohesion: 0.26
|
||||
Nodes (12): list_share_logs(), _parse_user_agent(), Any, Session, CRM 报价单分享访问日志 CRUD — 对照 CrmQuoteShareLogServiceImpl.java, Java: statisticsVisits, 简易 User-Agent 解析(替代 Hutool UserAgentUtil), Java: queryLogsByQuoteId (+4 more)
|
||||
|
||||
### Community 114 - "db_models.py"
|
||||
Cohesion: 0.24
|
||||
Nodes (12): DbSequence, 地磅(db)模块数据模型 — 对齐 docs/sql/db_tables.sql, _allocate_inbound_outbound_id(), _ensure_sequence_row(), max_numeric_inbound_outbound_id(), next_inbound_outbound_id(), Session, 出入库单 id 生成 — 租户内纯数字自增,并发安全。 (+4 more)
|
||||
|
||||
### Community 115 - "site.py"
|
||||
Cohesion: 0.10
|
||||
Nodes (45): approve_carrier(), create_carrier(), create_vehicle(), get_carrier(), get_vehicle(), list_carriers(), list_drivers(), list_vehicles() (+37 more)
|
||||
|
||||
### Community 116 - "Community 116"
|
||||
Cohesion: 0.17
|
||||
Nodes (28): batch_insert_quote_products(), calculate_subtotal(), _decimal_value(), delete_products_by_quote_id(), _ensure_quote_parent_scope(), _get_field(), get_quote_product(), insert_quote_product() (+20 more)
|
||||
|
||||
### Community 118 - "Community 118"
|
||||
Cohesion: 0.15
|
||||
Nodes (29): batch_insert_contract_products(), calculate_subtotal(), _decimal_value(), delete_price_tiers_by_contract_id(), delete_products_by_contract_id(), _get_field(), insert_contract_product(), _letter_oss_ids_to_json() (+21 more)
|
||||
|
||||
### Community 119 - "Community 119"
|
||||
Cohesion: 0.16
|
||||
Nodes (28): CertRow, _cert_row_complete(), collect_required_cert_errors(), _find_cert_for_required(), get_required_personnel_cert_names(), match_required_cert_name(), _missing_field_labels(), Session (+20 more)
|
||||
|
||||
### Community 122 - "Community 122"
|
||||
Cohesion: 0.39
|
||||
Nodes (10): create_post(), get_post(), list_posts(), post_code_exists(), Any, Session, _row_to_dict(), soft_delete_post() (+2 more)
|
||||
|
||||
### Community 125 - "Community 125"
|
||||
Cohesion: 0.24
|
||||
Nodes (7): camel_to_snake(), CRUDBase, get_model_attr(), HistoryCRUD, Any, Session, ModelType
|
||||
|
||||
### Community 126 - "customer_pool.py"
|
||||
Cohesion: 0.36
|
||||
Nodes (13): _build_tree(), _cascade_soft_delete(), create_menu(), get_menu(), list_menus(), list_menus_tree(), menu_name_exists(), Any (+5 more)
|
||||
|
||||
### Community 127 - "Community 127"
|
||||
Cohesion: 0.37
|
||||
Nodes (14): create_opportunity_follow(), _ensure_parent_opportunity(), list_follows_by_opportunity(), list_opportunity_follows(), _parse_optional_datetime(), Any, datetime, Session (+6 more)
|
||||
|
||||
### Community 128 - "plan.py"
|
||||
Cohesion: 0.39
|
||||
Nodes (10): create_site(), get_site(), list_sites(), Any, Session, _row_to_dict(), site_code_exists(), soft_delete_site() (+2 more)
|
||||
|
||||
### Community 129 - "BaseStorage"
|
||||
Cohesion: 0.12
|
||||
Nodes (24): _auto_contract_name_body(), generate_contract_name(), 按面板单位全称精确匹配交易主体/客户主数据,取 shortName;对不上则用全称。, 前端预览误用单位全称时,名称片段会等于面板/客户/开票全称。, 按面板供方(partyA)-需方(partyB)生成:{供方简称}-{需方简称}({合同编号})。 - 名称为空:按主数据简称自动生成 - 名, resolve_party_short_name(), _strip_contract_name_no_suffix(), _user_contract_name_uses_party_full_name() (+16 more)
|
||||
|
||||
### Community 130 - "datetime_parse.py"
|
||||
Cohesion: 0.06
|
||||
Nodes (56): operlog_detail(), operlog_list(), Session, 操作日志分页列表(基于 API 请求日志适配 RuoYi OperLog 结构), _user_info(), _claims_from_auth(), _format_body_bytes(), _persist_log() (+48 more)
|
||||
|
||||
### Community 131 - "LocalStorage"
|
||||
Cohesion: 0.18
|
||||
Nodes (22): cancel_apply(), create_apply(), create_process(), delete_applies(), delete_processes(), get_apply(), get_process(), list_applies() (+14 more)
|
||||
|
||||
### Community 132 - "crm_supplier_hazard.py"
|
||||
Cohesion: 0.15
|
||||
Nodes (14): invoice_info_add(), invoice_info_by_customer(), _invoice_info_by_customer_impl(), invoice_info_delete(), invoice_info_list(), invoice_info_set_default(), invoice_info_update(), 按客户ID查询开票信息(与 by-customer 等价) (+6 more)
|
||||
|
||||
### Community 134 - "Community 134"
|
||||
Cohesion: 0.25
|
||||
Nodes (18): batch_generate_sample_codes(), batch_issue_sample_codes(), batch_revoke_sample_codes(), create_sample_code(), get_sample_code(), list_sample_codes(), _parse_issued_time(), Any (+10 more)
|
||||
|
||||
### Community 135 - "unsettled_plans_q_filter"
|
||||
Cohesion: 0.17
|
||||
Nodes (23): cancel_apply(), create_apply(), create_record(), delete_applies(), delete_records(), get_apply(), get_record(), list_applies() (+15 more)
|
||||
|
||||
### Community 136 - "Community 136"
|
||||
Cohesion: 0.19
|
||||
Nodes (18): create_role(), get_role(), get_role_data_scope(), get_role_menu_ids(), list_roles(), Any, Session, 角色 CRUD(sys_role / sys_role_menu / sys_role_data_scope_detail) (+10 more)
|
||||
|
||||
### Community 137 - "statistics.py"
|
||||
Cohesion: 0.35
|
||||
Nodes (11): create_dict_type(), dict_type_code_exists(), get_dict_type(), list_dict_types(), Any, Session, 字典类型 CRUD(sys_dict_type), _row_to_dict() (+3 more)
|
||||
|
||||
### Community 138 - "Community 138"
|
||||
Cohesion: 0.33
|
||||
Nodes (19): check_python(), create_venv(), ensure_dirs(), ensure_valid_venv(), ensure_venv_and_runtime(), install_dependencies(), is_running(), log_error() (+11 more)
|
||||
|
||||
### Community 139 - "get_current_user_by_token"
|
||||
Cohesion: 0.22
|
||||
Nodes (15): get_storage(), download_file_bytes(), file_stream(), get_access_url(), get_download_url(), get_preview_url(), normalize_storage_path(), 报销附件存储适配 — 复用门户 app.core.storage,对外保持 bx 协议。 (+7 more)
|
||||
|
||||
### Community 141 - "PlTransport"
|
||||
Cohesion: 0.17
|
||||
Nodes (27): approve_task(), _assert_wf_instance_in_scope(), _business_type_label(), create_process_def(), _enrich_task_list(), get_business_all_tasks(), _get_business_form_fields(), get_designer_data() (+19 more)
|
||||
|
||||
### Community 143 - "SysEthnic"
|
||||
Cohesion: 0.34
|
||||
Nodes (13): create_contact(), get_contact(), _parse_optional_date(), date, Session, set_primary_contact(), soft_delete_contact(), total_pages() (+5 more)
|
||||
|
||||
### Community 144 - "dict_data.py"
|
||||
Cohesion: 0.27
|
||||
Nodes (11): create_dict_data(), get_dict_data(), list_dict_data(), list_dict_data_options(), Any, Session, 字典数据 CRUD(sys_dict_data), 字典下拉选项:租户主数据,忽略数据范围(user_info 保留兼容调用方,不参与过滤)。 (+3 more)
|
||||
|
||||
### Community 145 - "resolve_crm_product"
|
||||
Cohesion: 0.51
|
||||
Nodes (10): apply_site_feedback(), OrderItem, _header(), _item(), test_apply_site_feedback_does_not_reset_first_submitted_at(), test_apply_site_feedback_keeps_in_progress_when_requested(), test_apply_site_feedback_rejects_already_voided(), test_apply_site_feedback_rejects_invalid_status() (+2 more)
|
||||
|
||||
### Community 146 - "__init__.py"
|
||||
Cohesion: 0.20
|
||||
Nodes (12): 流程实例 wf_process_instance, WfProcessInstance, _dispatch_local(), Any, 工作流回调服务 —— 流程状态变更时通知业务系统, 注册本地回调函数。当 callback_url 以 path 开头时直接调用 fn。, 尝试本地直调。返回 True 表示已处理,False 表示无匹配的本地回调。, 流程实例状态变更后,向业务系统发送回调通知。 优先级: 1. 如果 callback_url 是内部路径(以 / 开头),优先尝试本地直调 (+4 more)
|
||||
|
||||
### Community 147 - "Community 147"
|
||||
Cohesion: 0.37
|
||||
Nodes (17): check_env(), check_python(), create_venv(), init_env(), install_dependencies(), log_error(), log_info(), log_success() (+9 more)
|
||||
|
||||
### Community 150 - "apply_site_feedback"
|
||||
Cohesion: 0.27
|
||||
Nodes (10): apply_product_snapshot_to_plan(), ProductSnapshot, Session, 地磅模块 — CRM 产品解析与快照写入。, 校验 CRM 产品并返回名称/规格快照。新建单据 product_id 必填。 require_on_shelf=False 用于运输计划联动地磅等历, resolve_crm_product(), _product(), resolve_crm_product:运输联动场景允许已下架产品。 (+2 more)
|
||||
|
||||
### Community 152 - "response.py"
|
||||
Cohesion: 0.23
|
||||
Nodes (10): check_customer_protect_time(), pool_recovery_job(), Session, 每天凌晨1点:检查保护期到期客户,跟进次数不足则退回公海, 每天早上9点:保护期到期提醒(3天内到期且跟进不足), _recycle_to_pool(), remind_customer_recycle(), warn_customer_recycle() (+2 more)
|
||||
|
||||
### Community 154 - "response.py"
|
||||
Cohesion: 0.24
|
||||
Nodes (5): MinIOStorage, Any, 从 MINIO_BASE_URL 解析公网 endpoint;未配置或与内网相同时复用 self.client。, MinIO 预签名下载 URL,携带原始文件名。, MinIO 预签名内联预览 URL(图片新标签页 / PDF iframe 等)。 上传时 put_object 未写 content_typ
|
||||
|
||||
### Community 156 - "utils.py"
|
||||
Cohesion: 0.20
|
||||
Nodes (16): get_pool_record(), list_pool_records(), Any, Session, _row_to_dict(), total_pages(), create_oss(), get_oss_by_id() (+8 more)
|
||||
|
||||
### Community 159 - "customer_update"
|
||||
Cohesion: 0.08
|
||||
Nodes (47): 报销模块认证适配:门户 JWT 用户 → bx 业务上下文。, 菜单 code 权限守卫(V0.3:通过 sys_user_role → sys_role_menu → sys_menu.perms), BxReimburseInvoice, BxWorkflowNode, 报销(bx)模块数据模型 — 对齐 docs/sql/20260606_bx_tables.sql, filter_current_cycle_approvals(), 审批轮次过滤 — 驳回后重新提交时,进度接口仅展示当前轮次。, 审批进度仅展示当前轮次;完整历史由 get_approval_history 返回。 (+39 more)
|
||||
|
||||
### Community 160 - "register_local_callback"
|
||||
Cohesion: 0.06
|
||||
Nodes (49): category_add(), category_delete(), category_tree(), product_add(), product_delete(), product_detail(), product_options(), product_update() (+41 more)
|
||||
|
||||
### Community 161 - "backfill_crm_contract_attachment_urls.py"
|
||||
Cohesion: 0.29
|
||||
Nodes (10): normalize_attachment_csv(), extract_minio_object_key(), join_relative_urls(), MinIO 附件 URL 规范化(数据迁移共用)。 背景 ---- CRM / 地磅 / 报销与 shanxia2.0 共用同一 MinIO(bucket 默, 业务表 OSS ID 串 → 相对 URL 串(用于客户证照、跟进附件等)。, 去掉预签名 query(``?X-Amz-...``)。, 将绝对/相对 URL 规范为 MinIO object key(不含 bucket)。, resolve_oss_id_csv() (+2 more)
|
||||
|
||||
### Community 162 - ".to_tenant_user_dict"
|
||||
Cohesion: 0.42
|
||||
Nodes (7): default_settlement_clause(), test_advance_payment_clause(), test_arrive_payment_clause(), test_empty_falls_back_to_arrive(), test_monthly_settlement_clause_uses_label(), test_unknown_falls_back_to_arrive(), test_volume_quarterly_other_labels()
|
||||
|
||||
### Community 163 - "__init__.py"
|
||||
Cohesion: 0.13
|
||||
Nodes (22): bind_biz(), delete_attachment(), download_file(), list_by_biz(), _parse_id_list(), preview_attachment(), Session, UploadFile (+14 more)
|
||||
|
||||
### Community 164 - "get_client_ip"
|
||||
Cohesion: 0.10
|
||||
Nodes (47): _avatar_url_field(), _build_user_info_payload(), change_current_user_password(), get_current_user_profile(), get_web_auth_menu_codes(), _jwt_user_payload(), logout(), public_quote_share() (+39 more)
|
||||
|
||||
### Community 170 - "test_quote_approver_setting.py"
|
||||
Cohesion: 0.22
|
||||
Nodes (3): BaseStorage, 带下载文件名的访问 URL;默认实现为普通访问地址。, 浏览器内联预览 URL;默认实现为普通访问地址。
|
||||
|
||||
### Community 171 - "minio_url.py"
|
||||
Cohesion: 0.31
|
||||
Nodes (14): create_pool_rule(), delete_pool_rules(), execute_pool_rules(), _execute_single_rule(), get_pool_rule(), list_pool_rules(), Any, Session (+6 more)
|
||||
|
||||
### Community 172 - "apply_pl_scope"
|
||||
Cohesion: 0.12
|
||||
Nodes (15): _db_pool_keepalive_loop(), _error_json(), lifespan(), Exception, Request, 数据库异常:返回 JSON 并保留 CORS 头,避免浏览器只报跨域错误, 未捕获异常:统一 JSON 500,确保跨域场景下仍有 Access-Control-Allow-Origin, 定期 ping MySQL,避免远程库/NAT 回收空闲 TCP 后首请求长时间卡住。 (+7 more)
|
||||
|
||||
### Community 173 - "resolve_create_time_bounds"
|
||||
Cohesion: 0.05
|
||||
Nodes (77): _build_page(), _get_by_token(), _make_handlers(), _models(), Session, 运输计划 · 入库/出库单现场免登录公共 API, SoftDeleteModel, BizAttachment (+69 more)
|
||||
|
||||
### Community 174 - "register_local_callback"
|
||||
Cohesion: 0.06
|
||||
Nodes (60): 返回 (作废, 归档)。现场链接实际走入库/出库单公共页。, _resolve_io_order_link_flags(), _build_public_page(), _build_site_product_read(), _format_file_size(), get_public_site(), _get_site_link_by_token(), _parse_feedback_images() (+52 more)
|
||||
|
||||
### Community 179 - "test_pl_vehicle_personnel_photo_schema.py"
|
||||
Cohesion: 0.27
|
||||
Nodes (16): add_favorite(), count_active(), find_active(), find_soft_deleted(), list_favorites(), _next_order_num(), normalize_menu_path(), Any (+8 more)
|
||||
|
||||
### Community 180 - "OSSStorage"
|
||||
Cohesion: 0.06
|
||||
Nodes (82): archive_plan(), cancel_plan(), cancel_plan_wait_notice(), count_plans_by_contract(), create_from_contract(), create_plan(), create_plan_full(), delete_plan() (+74 more)
|
||||
|
||||
### Community 181 - "allocate_next_biz_no"
|
||||
Cohesion: 0.18
|
||||
Nodes (13): list_customer_options(), 客户下拉选项:租户内只读搜索,不做 owner 数据权限(供地磅/合同等主数据选用)。, list_products(), get_supplier_hazard_flags(), product_allowed(), 产品/客户列表 keyword:名称/编号/拼音首字母 OR 匹配。, test_list_customer_options_keyword_includes_name_and_pinyin(), test_list_customers_keyword_includes_name_and_pinyin() (+5 more)
|
||||
|
||||
### Community 182 - "statistics.py"
|
||||
Cohesion: 0.45
|
||||
Nodes (11): _customer_base_query(), get_follow_statistics(), get_industry_statistics(), get_level_statistics(), get_overview_statistics(), get_owner_statistics(), get_source_statistics(), get_trend_statistics() (+3 more)
|
||||
|
||||
### Community 183 - "resolve_create_time_bounds"
|
||||
Cohesion: 0.67
|
||||
Nodes (3): parse_optional_plan_datetime(), datetime, 运输计划相关日期时间解析:前端 date-picker 仅传 YYYY-MM-DD。
|
||||
|
||||
### Community 184 - "excel_export.py"
|
||||
Cohesion: 0.24
|
||||
Nodes (11): CRM 模块 Excel 导出列定义(字段与字典类型对齐 Java *Vo @ExcelDictFormat)。, _build_workbook(), DictLabelResolver, export_rows_to_xlsx_response(), ExportColumn, Any, Excel 导出工具:集中处理字典 code → label(对齐 Java ExcelDictConvert / ExcelDictPreloadConver, CRM 列表导出统一入口:表头 + 行数据 + 字典 label 转换 + 文件流响应。 (+3 more)
|
||||
|
||||
### Community 188 - "get_client_ip"
|
||||
Cohesion: 0.24
|
||||
Nodes (12): get_last_month_range(), month_range(), date, Session, 地磅计划按月自动生成 — 迁移自 simple-dibang-xiaoshou/auto-create-plan/auto_create_monthly_pla, 按月自动生成采购计划。 - 默认仅在每月 1 日执行(force=True 时跳过该限制,供手动触发验收) - 遍历所有租户,按上月计划模板生, run_auto_generate_monthly_plans(), default_execution_status() (+4 more)
|
||||
|
||||
### Community 193 - "storage.py"
|
||||
Cohesion: 0.04
|
||||
Nodes (83): category_update(), clue_import_template(), contact_by_customer(), customer_create(), customer_import_template(), customer_update(), opportunity_add(), opportunity_assign() (+75 more)
|
||||
|
||||
### Community 194 - "get_current_user_by_token"
|
||||
Cohesion: 0.40
|
||||
Nodes (12): create_follow(), _enrich_follow_dict(), get_follow(), list_follows(), list_follows_by_customer(), Any, Session, _row_to_dict() (+4 more)
|
||||
|
||||
### Community 195 - "PlFreightSettlementTransport"
|
||||
Cohesion: 0.40
|
||||
Nodes (3): Any, 供 get_tenant_no 等门户工具使用。, 与门户 JWT / apply_data_scope 所需的 user_info 字段对齐。
|
||||
|
||||
### Community 196 - "crm_contract_sub_routes.py"
|
||||
Cohesion: 0.20
|
||||
Nodes (4): _guess_preview_content_type(), OSSStorage, 文件存储工具模块 支持多种存储方式: 1. 本地存储 (local) - 默认 2. 七牛云存储 (qiniu) - 推荐生产环境使用 3. 阿里云OSS (, 按对象键后缀推断内联预览 Content-Type(纠正上传时未写入 MIME 的情况)。
|
||||
|
||||
### Community 199 - "price_letter_detail"
|
||||
Cohesion: 0.17
|
||||
Nodes (11): price_letter_add(), price_letter_detail(), price_letter_list(), price_letter_update(), price_letter_void(), PriceLetterCreateBody, PriceLetterDetailBody, PriceLetterListBody (+3 more)
|
||||
|
||||
### Community 200 - "assert_contract_editable_for_update"
|
||||
Cohesion: 0.60
|
||||
Nodes (3): assert_contract_editable_for_update(), test_assert_allows_editable(), test_assert_rejects_non_editable()
|
||||
|
||||
### Community 206 - "crm_quote_pdf.py"
|
||||
Cohesion: 0.36
|
||||
Nodes (10): build_quote_html(), _default_value(), _format_date(), _format_discount(), _format_money(), generate_quote_pdf(), _get_pdf_styles(), Any (+2 more)
|
||||
|
||||
### Community 207 - "shanxia2.0 后端骨架(FastAPI)"
|
||||
Cohesion: 0.18
|
||||
Nodes (10): 1. 创建虚拟环境, 2. 安装依赖, 3. 配置环境变量, 4. 启动服务, API 路由分组, shanxia2.0 后端骨架(FastAPI), 开发规范, 快速开始 (+2 more)
|
||||
|
||||
### Community 220 - "api_crypto_middleware.py"
|
||||
Cohesion: 0.12
|
||||
Nodes (23): ApiCryptoMiddleware, _claims_from_auth(), _coerce_post_method_override_to_get(), _get_redis_client(), _is_crypto_api(), _json_error_response(), _mark_nonce_once(), _nonce_key() (+15 more)
|
||||
|
||||
### Community 237 - "order_status_compat.py"
|
||||
Cohesion: 0.36
|
||||
Nodes (7): derive_legacy_status_from_three(), derive_three_status_from_legacy(), Decimal, 阶段A兼容:从旧 status 推导三字段。 约束:旧 status 本身混合了支付/配送/订单生命周期,因此只能尽力推断; 若业务侧已有更精确, 阶段A兼容:从三字段反推旧 status(用于旧前端/旧筛选参数兼容)。, ThreeStatus, _to_decimal()
|
||||
|
||||
### Community 239 - "shanxia2.0 Backend 生产部署说明"
|
||||
Cohesion: 0.25
|
||||
Nodes (7): 1. 上传到服务器, 2. 服务器首次启动, 3. 日常运维, 4. 配置文件, 5. 与 Nginx 配合, 6. 常见问题, shanxia2.0 Backend 生产部署说明
|
||||
|
||||
### Community 246 - "db_collation.py"
|
||||
Cohesion: 0.38
|
||||
Nodes (6): eq_unicode(), like_unicode(), MySQL 字符串比较/模糊匹配时统一排序规则,避免 Illegal mix of collations。, 跨表字符串等值比较(JOIN / WHERE)时统一为 utf8mb4_unicode_ci。, 将列显式设为 utf8mb4_unicode_ci。, unicode_ci()
|
||||
|
||||
## Knowledge Gaps
|
||||
- **30 isolated node(s):** `Config`, `Config`, `Config`, `Config`, `Config` (+25 more)
|
||||
These have ≤1 connection - possible missing edges or undocumented components.
|
||||
- **18 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
|
||||
|
||||
## Suggested Questions
|
||||
_Questions this graph is uniquely positioned to answer:_
|
||||
|
||||
- **Why does `generate_uuid()` connect `Community 25` to `Community 0`, `plan.py`, `Community 4`, `Community 5`, `Community 6`, `Community 134`, `Community 8`, `statistics.py`, `Community 136`, `Community 11`, `SysEthnic`, `dict_data.py`, `Community 15`, `Community 18`, `Community 16`, `Community 21`, `Community 24`, `response.py`, `utils.py`, `Community 29`, `Community 28`, `customer_update`, `Community 33`, `get_client_ip`, `Community 36`, `Community 39`, `minio_url.py`, `Community 45`, `resolve_create_time_bounds`, `OSSStorage`, `Community 53`, `Community 58`, `get_client_ip`, `Community 61`, `Community 62`, `Community 63`, `Community 64`, `Community 65`, `get_current_user_by_token`, `Community 66`, `Community 68`, `Community 67`, `Community 71`, `Community 72`, `Community 83`, `Community 86`, `Community 87`, `Any`, `pl_ocr_service.py`, `pl_vehicle_recharge_service.py`, `Community 92`, `_invoice_info_by_customer_impl`, `Community 95`, `apply_crm_create_by_scope`, `apply_pl_scope`, `Community 109`, `quote_share_log.py`, `db_models.py`, `site.py`, `Community 116`, `Community 118`, `Community 122`, `customer_pool.py`, `Community 127`?**
|
||||
_High betweenness centrality (0.095) - this node is a cross-community bridge._
|
||||
- **Why does `BaseModel` connect `Community 12` to `Community 0`, `Community 1`, `Community 2`, `datetime_parse.py`, `crm_supplier_hazard.py`, `Community 5`, `LocalStorage`, `Community 7`, `unsettled_plans_q_filter`, `Community 11`, `PlTransport`, `Community 13`, `Community 15`, `Community 16`, `Community 18`, `Community 19`, `Community 24`, `Community 26`, `Community 27`, `Community 28`, `register_local_callback`, `Community 33`, `Community 34`, `__init__.py`, `Community 38`, `Community 43`, `Community 45`, `resolve_create_time_bounds`, `Community 48`, `Community 50`, `Community 51`, `OSSStorage`, `Community 54`, `Community 58`, `Community 60`, `Community 62`, `Community 64`, `storage.py`, `Community 66`, `Community 65`, `price_letter_detail`, `Community 72`, `Community 71`, `Community 74`, `Community 106`, `Community 109`, `Community 111`, `site.py`, `Community 119`?**
|
||||
_High betweenness centrality (0.081) - this node is a cross-community bridge._
|
||||
- **Why does `Settings` connect `Community 46` to `_ensure_subpack_slip_no`, `Community 35`, `Community 67`, `Community 37`, `price_letter_detail`, `Community 73`, `Community 42`, `Community 44`, `Community 52`, `Community 59`, `api_crypto_middleware.py`, `Community 30`?**
|
||||
_High betweenness centrality (0.073) - this node is a cross-community bridge._
|
||||
- **Are the 8 inferred relationships involving `DbUserContext` (e.g. with `InventoryRelatedData` and `AutoGenerateMonthlyResult`) actually correct?**
|
||||
_`DbUserContext` has 8 INFERRED edges - model-reasoned connections that need verification._
|
||||
- **What connects `API 路由包 — 山峡化工生产一体化门户`, `返回运营端角色下拉选项(label=库中角色名,value=Web 角色码)。`, `返回用户下拉选项(label=昵称,value=用户ID,附带 nickName/userNo)。 供申请人/审批人等选择场景使用,返回本租户全部有效` to the rest of the system?**
|
||||
_558 weakly-connected nodes found - possible documentation gaps or missing edges._
|
||||
- **Should `Community 0` be split into smaller, more focused modules?**
|
||||
_Cohesion score 0.10638297872340426 - nodes in this community are weakly interconnected._
|
||||
- **Should `Community 1` be split into smaller, more focused modules?**
|
||||
_Cohesion score 0.07308970099667775 - nodes in this community are weakly interconnected._
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -1,16 +1,16 @@
|
||||
# Graph Report - backend (2026-08-17)
|
||||
# Graph Report - backend (2026-08-18)
|
||||
|
||||
## Corpus Check
|
||||
- 326 files · ~215,608 words
|
||||
- 329 files · ~217,783 words
|
||||
- Verdict: corpus is large enough that graph structure adds value.
|
||||
|
||||
## Summary
|
||||
- 4819 nodes · 16536 edges · 545 communities (527 shown, 18 thin omitted)
|
||||
- Extraction: 96% EXTRACTED · 4% INFERRED · 0% AMBIGUOUS · INFERRED: 730 edges (avg confidence: 0.62)
|
||||
- 4491 nodes · 16684 edges · 173 communities (158 shown, 15 thin omitted)
|
||||
- Extraction: 96% EXTRACTED · 4% INFERRED · 0% AMBIGUOUS · INFERRED: 744 edges (avg confidence: 0.62)
|
||||
- Token cost: 0 input · 0 output
|
||||
|
||||
## Graph Freshness
|
||||
- Built from commit: `9345cf4a`
|
||||
- Built from commit: `c3bd2c00`
|
||||
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
|
||||
- Run `graphify update .` after code changes (no API cost).
|
||||
|
||||
@@ -24,14 +24,18 @@
|
||||
- [[_COMMUNITY_Community 6|Community 6]]
|
||||
- [[_COMMUNITY_Community 7|Community 7]]
|
||||
- [[_COMMUNITY_Community 8|Community 8]]
|
||||
- [[_COMMUNITY_Community 9|Community 9]]
|
||||
- [[_COMMUNITY_Community 10|Community 10]]
|
||||
- [[_COMMUNITY_Community 11|Community 11]]
|
||||
- [[_COMMUNITY_Community 12|Community 12]]
|
||||
- [[_COMMUNITY_Community 13|Community 13]]
|
||||
- [[_COMMUNITY_Community 14|Community 14]]
|
||||
- [[_COMMUNITY_Community 15|Community 15]]
|
||||
- [[_COMMUNITY_Community 16|Community 16]]
|
||||
- [[_COMMUNITY_Community 17|Community 17]]
|
||||
- [[_COMMUNITY_Community 18|Community 18]]
|
||||
- [[_COMMUNITY_Community 19|Community 19]]
|
||||
- [[_COMMUNITY_Community 20|Community 20]]
|
||||
- [[_COMMUNITY_Community 21|Community 21]]
|
||||
- [[_COMMUNITY_Community 22|Community 22]]
|
||||
- [[_COMMUNITY_Community 23|Community 23]]
|
||||
@@ -42,6 +46,8 @@
|
||||
- [[_COMMUNITY_Community 28|Community 28]]
|
||||
- [[_COMMUNITY_Community 29|Community 29]]
|
||||
- [[_COMMUNITY_Community 30|Community 30]]
|
||||
- [[_COMMUNITY_Community 31|Community 31]]
|
||||
- [[_COMMUNITY_Community 32|Community 32]]
|
||||
- [[_COMMUNITY_Community 33|Community 33]]
|
||||
- [[_COMMUNITY_Community 34|Community 34]]
|
||||
- [[_COMMUNITY_Community 35|Community 35]]
|
||||
@@ -49,11 +55,14 @@
|
||||
- [[_COMMUNITY_Community 37|Community 37]]
|
||||
- [[_COMMUNITY_Community 38|Community 38]]
|
||||
- [[_COMMUNITY_Community 39|Community 39]]
|
||||
- [[_COMMUNITY_Community 40|Community 40]]
|
||||
- [[_COMMUNITY_Community 41|Community 41]]
|
||||
- [[_COMMUNITY_Community 42|Community 42]]
|
||||
- [[_COMMUNITY_Community 43|Community 43]]
|
||||
- [[_COMMUNITY_Community 44|Community 44]]
|
||||
- [[_COMMUNITY_Community 45|Community 45]]
|
||||
- [[_COMMUNITY_Community 46|Community 46]]
|
||||
- [[_COMMUNITY_Community 47|Community 47]]
|
||||
- [[_COMMUNITY_Community 48|Community 48]]
|
||||
- [[_COMMUNITY_Community 49|Community 49]]
|
||||
- [[_COMMUNITY_Community 50|Community 50]]
|
||||
@@ -61,6 +70,9 @@
|
||||
- [[_COMMUNITY_Community 52|Community 52]]
|
||||
- [[_COMMUNITY_Community 53|Community 53]]
|
||||
- [[_COMMUNITY_Community 54|Community 54]]
|
||||
- [[_COMMUNITY_Community 55|Community 55]]
|
||||
- [[_COMMUNITY_Community 56|Community 56]]
|
||||
- [[_COMMUNITY_Community 57|Community 57]]
|
||||
- [[_COMMUNITY_Community 58|Community 58]]
|
||||
- [[_COMMUNITY_Community 59|Community 59]]
|
||||
- [[_COMMUNITY_Community 60|Community 60]]
|
||||
@@ -80,12 +92,10 @@
|
||||
- [[_COMMUNITY_Community 74|Community 74]]
|
||||
- [[_COMMUNITY_Community 77|Community 77]]
|
||||
- [[_COMMUNITY_Community 78|Community 78]]
|
||||
- [[_COMMUNITY_Community 81|Community 81]]
|
||||
- [[_COMMUNITY_Community 83|Community 83]]
|
||||
- [[_COMMUNITY_Community 84|Community 84]]
|
||||
- [[_COMMUNITY_Community 86|Community 86]]
|
||||
- [[_COMMUNITY_Community 87|Community 87]]
|
||||
- [[_COMMUNITY_Any|Any]]
|
||||
- [[_COMMUNITY_pl_ocr_service.py|pl_ocr_service.py]]
|
||||
- [[_COMMUNITY_pl_vehicle_recharge_service.py|pl_vehicle_recharge_service.py]]
|
||||
- [[_COMMUNITY_Community 92|Community 92]]
|
||||
@@ -94,11 +104,9 @@
|
||||
- [[_COMMUNITY_Community 95|Community 95]]
|
||||
- [[_COMMUNITY__ensure_subpack_slip_no|_ensure_subpack_slip_no]]
|
||||
- [[_COMMUNITY_price_letter_detail|price_letter_detail]]
|
||||
- [[_COMMUNITY_.to_tenant_user_dict|.to_tenant_user_dict]]
|
||||
- [[_COMMUNITY_dept.py|dept.py]]
|
||||
- [[_COMMUNITY_apply_crm_create_by_scope|apply_crm_create_by_scope]]
|
||||
- [[_COMMUNITY_list_customers|list_customers]]
|
||||
- [[_COMMUNITY_Community 106|Community 106]]
|
||||
- [[_COMMUNITY_apply_pl_scope|apply_pl_scope]]
|
||||
- [[_COMMUNITY_Community 109|Community 109]]
|
||||
- [[_COMMUNITY_Base|Base]]
|
||||
@@ -113,11 +121,9 @@
|
||||
- [[_COMMUNITY_Community 122|Community 122]]
|
||||
- [[_COMMUNITY_Community 125|Community 125]]
|
||||
- [[_COMMUNITY_customer_pool.py|customer_pool.py]]
|
||||
- [[_COMMUNITY_Community 127|Community 127]]
|
||||
- [[_COMMUNITY_plan.py|plan.py]]
|
||||
- [[_COMMUNITY_BaseStorage|BaseStorage]]
|
||||
- [[_COMMUNITY_datetime_parse.py|datetime_parse.py]]
|
||||
- [[_COMMUNITY_LocalStorage|LocalStorage]]
|
||||
- [[_COMMUNITY_crm_supplier_hazard.py|crm_supplier_hazard.py]]
|
||||
- [[_COMMUNITY_Community 134|Community 134]]
|
||||
- [[_COMMUNITY_unsettled_plans_q_filter|unsettled_plans_q_filter]]
|
||||
@@ -126,7 +132,6 @@
|
||||
- [[_COMMUNITY_Community 138|Community 138]]
|
||||
- [[_COMMUNITY_get_current_user_by_token|get_current_user_by_token]]
|
||||
- [[_COMMUNITY_PlTransport|PlTransport]]
|
||||
- [[_COMMUNITY_SysEthnic|SysEthnic]]
|
||||
- [[_COMMUNITY_dict_data.py|dict_data.py]]
|
||||
- [[_COMMUNITY_resolve_crm_product|resolve_crm_product]]
|
||||
- [[_COMMUNITY___init__.py|__init__.py]]
|
||||
@@ -134,13 +139,10 @@
|
||||
- [[_COMMUNITY_apply_site_feedback|apply_site_feedback]]
|
||||
- [[_COMMUNITY_Community 151|Community 151]]
|
||||
- [[_COMMUNITY_response.py|response.py]]
|
||||
- [[_COMMUNITY_response.py|response.py]]
|
||||
- [[_COMMUNITY_utils.py|utils.py]]
|
||||
- [[_COMMUNITY_customer_update|customer_update]]
|
||||
- [[_COMMUNITY_register_local_callback|register_local_callback]]
|
||||
- [[_COMMUNITY_backfill_crm_contract_attachment_urls.py|backfill_crm_contract_attachment_urls.py]]
|
||||
- [[_COMMUNITY_.to_tenant_user_dict|.to_tenant_user_dict]]
|
||||
- [[_COMMUNITY___init__.py|__init__.py]]
|
||||
- [[_COMMUNITY_get_client_ip|get_client_ip]]
|
||||
- [[_COMMUNITY_codeblock1 (iflytekshanxia2.0backend)|code:block1 (/iflytek/shanxia2.0/backend)]]
|
||||
- [[_COMMUNITY_test_quote_approver_setting.py|test_quote_approver_setting.py]]
|
||||
@@ -151,18 +153,12 @@
|
||||
- [[_COMMUNITY_.__init__|.__init__]]
|
||||
- [[_COMMUNITY_test_pl_vehicle_personnel_photo_schema.py|test_pl_vehicle_personnel_photo_schema.py]]
|
||||
- [[_COMMUNITY_OSSStorage|OSSStorage]]
|
||||
- [[_COMMUNITY_allocate_next_biz_no|allocate_next_biz_no]]
|
||||
- [[_COMMUNITY_statistics.py|statistics.py]]
|
||||
- [[_COMMUNITY_resolve_create_time_bounds|resolve_create_time_bounds]]
|
||||
- [[_COMMUNITY_excel_export.py|excel_export.py]]
|
||||
- [[_COMMUNITY_price_letter_detail|price_letter_detail]]
|
||||
- [[_COMMUNITY_get_client_ip|get_client_ip]]
|
||||
- [[_COMMUNITY_context.py|context.py]]
|
||||
- [[_COMMUNITY_storage.py|storage.py]]
|
||||
- [[_COMMUNITY_get_current_user_by_token|get_current_user_by_token]]
|
||||
- [[_COMMUNITY_PlFreightSettlementTransport|PlFreightSettlementTransport]]
|
||||
- [[_COMMUNITY_crm_contract_sub_routes.py|crm_contract_sub_routes.py]]
|
||||
- [[_COMMUNITY_price_letter_detail|price_letter_detail]]
|
||||
- [[_COMMUNITY_assert_contract_editable_for_update|assert_contract_editable_for_update]]
|
||||
- [[_COMMUNITY_crm_quote_pdf.py|crm_quote_pdf.py]]
|
||||
- [[_COMMUNITY_shanxia2.0 后端骨架(FastAPI)|shanxia2.0 后端骨架(FastAPI)]]
|
||||
@@ -170,7 +166,6 @@
|
||||
- [[_COMMUNITY_order_status_compat.py|order_status_compat.py]]
|
||||
- [[_COMMUNITY_shanxia2.0 Backend 生产部署说明|shanxia2.0 Backend 生产部署说明]]
|
||||
- [[_COMMUNITY_db_collation.py|db_collation.py]]
|
||||
- [[_COMMUNITY_一次上传一条发票:多文件挂同一档案,金额为总额,只写一条额度流水。|一次上传一条发票:多文件挂同一档案,金额为总额,只写一条额度流水。]]
|
||||
- [[_COMMUNITY___init__.py|__init__.py]]
|
||||
- [[_COMMUNITY___init__.py|__init__.py]]
|
||||
- [[_COMMUNITY_codebash (cd pathtoshanxia2.0)|code:bash (cd /path/to/shanxia2.0)]]
|
||||
@@ -191,12 +186,14 @@
|
||||
4. `DbUserContext` - 254 edges
|
||||
5. `error_response()` - 247 edges
|
||||
6. `generate_uuid()` - 194 edges
|
||||
7. `BxUserContext` - 183 edges
|
||||
7. `BxUserContext` - 188 edges
|
||||
8. `crm_user_info()` - 137 edges
|
||||
9. `assert_row_deletable_by_creator()` - 83 edges
|
||||
10. `ApiResponse` - 79 edges
|
||||
10. `ApiResponse` - 80 edges
|
||||
|
||||
## Surprising Connections (you probably didn't know these)
|
||||
- `BackfillRule` --uses--> `Settings` [INFERRED]
|
||||
scripts/migrate_crm_sys_oss.py → app/core/config.py
|
||||
- `MigrationStats` --uses--> `Settings` [INFERRED]
|
||||
scripts/migrate_bx_data.py → app/core/config.py
|
||||
- `StepStats` --uses--> `Settings` [INFERRED]
|
||||
@@ -205,61 +202,67 @@
|
||||
scripts/migrate_crm_clue.py → app/core/config.py
|
||||
- `StepStats` --uses--> `Settings` [INFERRED]
|
||||
scripts/migrate_crm_clue.py → app/core/config.py
|
||||
- `MigrationStats` --uses--> `Settings` [INFERRED]
|
||||
scripts/migrate_crm_contract.py → app/core/config.py
|
||||
|
||||
## Import Cycles
|
||||
- None detected.
|
||||
|
||||
## Communities (545 total, 18 thin omitted)
|
||||
## Communities (173 total, 15 thin omitted)
|
||||
|
||||
### Community 0 - "Community 0"
|
||||
Cohesion: 0.11
|
||||
Nodes (44): check_document_expiry(), document_reminder_stats(), get_inventory_ledger(), get_summary(), list_document_reminders(), list_transactions(), list_warnings(), date (+36 more)
|
||||
Cohesion: 0.14
|
||||
Nodes (35): check_document_expiry(), document_reminder_stats(), get_inventory_ledger(), get_summary(), list_document_reminders(), list_transactions(), list_warnings(), date (+27 more)
|
||||
|
||||
### Community 1 - "Community 1"
|
||||
Cohesion: 0.07
|
||||
Nodes (40): CRM 合同 API 路由 — GET/PUT/DELETE/POST 对齐原 Java CRM, 运营端角色守卫:admin / super_admin 可访问。, 接口菜单权限守卫。super_admin 直接放行。, require_admin_or_super_admin(), require_menu_code(), ContractApproverSettingSaveBody, ContractApprovalActionBody, ContractApprovalPassBody (+32 more)
|
||||
Cohesion: 0.06
|
||||
Nodes (64): CRM 合同 API 路由 — GET/PUT/DELETE/POST 对齐原 Java CRM, CRM 合同子模块路由 — 变更/模板/审批独立页, register_contract_sub_routes(), _change_row_to_dict(), create_contract_change(), execute_change(), export_contract_changes(), export_contract_templates() (+56 more)
|
||||
|
||||
### Community 2 - "Community 2"
|
||||
Cohesion: 0.20
|
||||
Nodes (27): _apply_inventory_product_update(), _apply_inventory_update(), _base_query(), create_inventory(), create_link_for_plan(), delete_inventory(), _generate_batch_no(), _get_inventory() (+19 more)
|
||||
Cohesion: 0.18
|
||||
Nodes (30): _apply_inventory_product_update(), _apply_inventory_update(), _base_query(), create_inventory(), create_link_for_plan(), delete_inventory(), _generate_batch_no(), _get_inventory() (+22 more)
|
||||
|
||||
### Community 3 - "Community 3"
|
||||
Cohesion: 0.07
|
||||
Nodes (65): attachment_download_url(), attachment_preview_url(), create_dept(), create_dict_data(), create_dict_type(), create_menu(), create_menu_favorite(), create_post() (+57 more)
|
||||
Cohesion: 0.09
|
||||
Nodes (96): attachment_download_url(), attachment_preview_url(), AttachmentDownloadUrlBody, create_dept(), create_dict_data(), create_dict_type(), create_menu(), create_menu_favorite() (+88 more)
|
||||
|
||||
### Community 4 - "Community 4"
|
||||
Cohesion: 0.17
|
||||
Nodes (43): _apply_list_filters(), assign_opportunities(), batch_update_expect_date(), batch_update_stage(), change_stage(), create_opportunity(), _decimal_value(), _enrich_user_names() (+35 more)
|
||||
Cohesion: 0.09
|
||||
Nodes (76): _apply_list_filters(), assign_opportunities(), batch_update_expect_date(), batch_update_stage(), change_stage(), create_opportunity(), _decimal_value(), _enrich_user_names() (+68 more)
|
||||
|
||||
### Community 5 - "Community 5"
|
||||
Cohesion: 0.10
|
||||
Nodes (56): _generate_notify_message(), PlPlan, PlPlanProduct, PlVehicle, 计划产品明细表 pl_plan_product, PlanArchiveFreightBlock, PlanArchiveProductItem, PlanProductCreate (+48 more)
|
||||
Cohesion: 0.09
|
||||
Nodes (66): update_plan(), PlPlan, PlPlanProduct, PlVehicle, 计划产品明细表 pl_plan_product, PlanArchiveFreightBlock, PlanArchiveProductItem, PlanContractLink (+58 more)
|
||||
|
||||
### Community 6 - "Community 6"
|
||||
Cohesion: 0.20
|
||||
Nodes (30): assign_customer(), batch_claim_customers(), check_duplicate_customer(), claim_customer(), create_customer(), deal_customer(), _enrich_user_names(), _ensure_customer_in_scope() (+22 more)
|
||||
Cohesion: 0.13
|
||||
Nodes (44): assign_customer(), batch_claim_customers(), check_duplicate_customer(), claim_customer(), create_customer(), deal_customer(), _enrich_user_names(), _ensure_customer_in_scope() (+36 more)
|
||||
|
||||
### Community 7 - "Community 7"
|
||||
Cohesion: 0.07
|
||||
Nodes (96): clue_check_convert(), clue_check_duplicate(), clue_detail(), clue_export(), clue_follow_statistics(), clue_import(), clue_list(), clue_mine() (+88 more)
|
||||
Cohesion: 0.04
|
||||
Nodes (209): clue_assign(), clue_check_convert(), clue_check_duplicate(), clue_convert(), clue_create(), clue_delete(), clue_detail(), clue_export() (+201 more)
|
||||
|
||||
### Community 8 - "Community 8"
|
||||
Cohesion: 0.13
|
||||
Nodes (50): register_quote_routes(), accept_quote(), assert_quote_editable_for_update(), calculate_quote_amount(), create_new_version(), create_quote(), _decimal_value(), _enrich_contact_fields() (+42 more)
|
||||
Cohesion: 0.10
|
||||
Nodes (67): register_quote_routes(), accept_quote(), assert_quote_editable_for_update(), _build_quote_filters(), calculate_quote_amount(), create_new_version(), create_quote(), _decimal_value() (+59 more)
|
||||
|
||||
### Community 9 - "Community 9"
|
||||
Cohesion: 0.08
|
||||
Nodes (57): download_reimburse_import_template(), build_applicant_label(), build_expense_path_labels(), build_import_template_bytes(), cell_str(), import_reimburse_workbook(), is_example_row(), iter_data_rows() (+49 more)
|
||||
|
||||
### Community 10 - "Community 10"
|
||||
Cohesion: 0.11
|
||||
Nodes (37): approve_settlement_legacy(), cancel_settlement(), create_settlement(), delete_settlement(), disburse_settlement(), _generate_settlement_no(), get_settlement(), get_settlement_approval_flow() (+29 more)
|
||||
|
||||
### Community 11 - "Community 11"
|
||||
Cohesion: 0.20
|
||||
Nodes (33): PlVehicleUreaApply, PlVehicleUreaDisburse, 车辆尿素申请 pl_vehicle_urea_apply, 车辆尿素发放登记 pl_vehicle_urea_disburse, VehicleUreaApplyRead, VehicleUreaDisburseBoardItem, apply_to_read(), _assert_vehicle_exists() (+25 more)
|
||||
Cohesion: 0.10
|
||||
Nodes (55): cancel_apply(), create_apply(), create_disburse(), delete_applies(), delete_disburses(), get_apply(), get_disburse(), list_applies() (+47 more)
|
||||
|
||||
### Community 12 - "Community 12"
|
||||
Cohesion: 0.05
|
||||
Nodes (68): contact_add(), contact_delete(), contact_update(), customer_delete(), BaseModel, AttachmentUploadResponse, ChangePasswordRequest, Config (+60 more)
|
||||
Nodes (72): customer_create(), customer_update(), customer_name_exists(), BaseModel, AttachmentUploadResponse, ChangePasswordRequest, Config, LoginRequest (+64 more)
|
||||
|
||||
### Community 13 - "Community 13"
|
||||
Cohesion: 0.09
|
||||
Nodes (58): advance_issue_stats(), batch_offset(), export_advance_issues(), issue_advance(), list_advance_issues(), list_advance_offset_records(), list_pending_payments(), list_processed_payments() (+50 more)
|
||||
Cohesion: 0.11
|
||||
Nodes (37): approve_reimburse(), approved_by_me(), _build_details_data(), create_reimburse(), delete_reimburse(), get_approval_history(), get_approvals(), get_generated_title() (+29 more)
|
||||
|
||||
### Community 14 - "Community 14"
|
||||
Cohesion: 0.30
|
||||
@@ -267,75 +270,91 @@ Nodes (12): PlCarrier, assert_carrier_eligible_for_transport(), carrier_agreemen
|
||||
|
||||
### Community 15 - "Community 15"
|
||||
Cohesion: 0.11
|
||||
Nodes (14): BxReimburseApply, BxReimburseApproval, BxReimburseDetail, BxReimburseOffset, 报销单勾选的发票附件(同一发票可勾选多个附件,各占一条)。, ReimburseInvoiceBriefOut, ReimburseInvoiceLinkIn, Any (+6 more)
|
||||
Nodes (16): BxReimburseApply, BxReimburseApproval, BxReimburseDetail, BxReimburseInvoice, BxReimburseOffset, 报销单勾选的发票附件(同一发票可勾选多个附件,各占一条)。, ReimburseInvoiceBriefOut, ReimburseInvoiceLinkIn (+8 more)
|
||||
|
||||
### Community 16 - "Community 16"
|
||||
Cohesion: 0.16
|
||||
Nodes (37): PlVehicleRechargeApply, PlVehicleRechargeRecord, 车辆ETC/加油充值申请 pl_vehicle_recharge_apply, 车辆ETC/加油充值发放登记 pl_vehicle_recharge_record, 发放登记页:申请单 + 关联发放信息(左连接), VehicleRechargeDisburseBoardItem, VehicleRechargeRecordCreate, VehicleRechargeRecordRead (+29 more)
|
||||
Cohesion: 0.14
|
||||
Nodes (40): PlVehicleRechargeApply, PlVehicleRechargeRecord, 车辆ETC/加油充值申请 pl_vehicle_recharge_apply, 车辆ETC/加油充值发放登记 pl_vehicle_recharge_record, Decimal, 发放登记页:申请单 + 关联发放信息(左连接), VehicleRechargeApplyCreate, VehicleRechargeApplyRead (+32 more)
|
||||
|
||||
### Community 17 - "Community 17"
|
||||
Cohesion: 0.14
|
||||
Nodes (16): BxAdvanceApply, BxAdvanceApproval, AdvanceService, Any, Session, 同一节点多次审批时,取 approval_time 最新的一条作为当前节点状态。, 返回全部审批动作,按时间正序排列,用于展示完整审批历史。, apply_bx_scope() (+8 more)
|
||||
|
||||
### Community 18 - "Community 18"
|
||||
Cohesion: 0.11
|
||||
Nodes (39): advance_pending_approval_stats(), advance_status_counts(), approve_advance(), approved_by_me(), create_advance(), delete_advance(), _fill_offset_amounts(), get_advance() (+31 more)
|
||||
Cohesion: 0.15
|
||||
Nodes (30): advance_pending_approval_stats(), advance_status_counts(), approve_advance(), approved_by_me(), create_advance(), delete_advance(), _fill_offset_amounts(), get_advance() (+22 more)
|
||||
|
||||
### Community 19 - "Community 19"
|
||||
Cohesion: 0.34
|
||||
Nodes (30): AttachmentDownloadUrlBody, DeptCreateBody, DeptUpdateBody, DictDataCreateBody, DictDataUpdateBody, DictTypeCreateBody, DictTypeUpdateBody, MenuCreateBody (+22 more)
|
||||
Cohesion: 0.22
|
||||
Nodes (22): advance_issue_stats(), batch_offset(), export_advance_issues(), issue_advance(), list_advance_issues(), list_advance_offset_records(), list_pending_payments(), list_processed_payments() (+14 more)
|
||||
|
||||
### Community 20 - "Community 20"
|
||||
Cohesion: 0.17
|
||||
Nodes (20): day_detail(), delete_assignment(), duty_user_options(), _enrich_assign_names(), month_summary(), Session, 指定用户时补全 personName(用 nick_name)。, 值班可安排人员:仅「值班人员」角色下的启用用户。 (+12 more)
|
||||
|
||||
### Community 21 - "Community 21"
|
||||
Cohesion: 0.09
|
||||
Nodes (58): _alias_row_to_dict(), _assert_brand_deletable(), barcode_exists(), _brand_row_to_dict(), _build_category_tree(), category_code_exists(), _category_row_to_dict(), _collect_category_with_ancestors() (+50 more)
|
||||
Cohesion: 0.06
|
||||
Nodes (76): _alias_row_to_dict(), _assert_brand_deletable(), barcode_exists(), _brand_row_to_dict(), _build_category_tree(), category_code_exists(), _category_row_to_dict(), _collect_category_with_ancestors() (+68 more)
|
||||
|
||||
### Community 22 - "Community 22"
|
||||
Cohesion: 0.08
|
||||
Nodes (28): parse_admin_request_json(), Any, Request, 运营 Web(/web/admin)JSON 请求体解析。 与 ApiCryptoMiddleware 配合:中间件未改写 body 时,在路由内解密 `{, 读取 JSON body;dict 时走 resolve_request_payload(明文 / 密文 / data 包装)。 Returns:, ApiCryptoError, _b64url_decode(), _b64url_encode() (+20 more)
|
||||
|
||||
### Community 23 - "Community 23"
|
||||
Cohesion: 0.15
|
||||
Nodes (6): convert_heic_to_jpeg(), process_image_content(), QiniuStorage, 将 HEIC/HEIF 格式转换为 JPEG 格式, 处理图片内容,如果是 HEIC 格式则转换为 JPEG, BytesIO
|
||||
Cohesion: 0.11
|
||||
Nodes (7): convert_heic_to_jpeg(), OSSStorage, process_image_content(), QiniuStorage, 文件存储工具模块 支持多种存储方式: 1. 本地存储 (local) - 默认 2. 七牛云存储 (qiniu) - 推荐生产环境使用 3. 阿里云OSS (, 将 HEIC/HEIF 格式转换为 JPEG 格式, 处理图片内容,如果是 HEIC 格式则转换为 JPEG
|
||||
|
||||
### Community 24 - "Community 24"
|
||||
Cohesion: 0.13
|
||||
Nodes (44): _apply_plan_product_update(), _apply_plan_update(), AutoGenerateMonthlyResult, _base_query(), create_plan(), delete_plan(), _generate_plan_no(), get_plan() (+36 more)
|
||||
Cohesion: 0.17
|
||||
Nodes (31): _apply_plan_product_update(), _apply_plan_update(), AutoGenerateMonthlyResult, _base_query(), create_plan(), _generate_plan_no(), get_plan(), _get_plan_by_id_only() (+23 more)
|
||||
|
||||
### Community 25 - "Community 25"
|
||||
Cohesion: 0.13
|
||||
Nodes (28): generate_uuid(), PlInventoryTransaction, 库存流水表 pl_inventory_transaction, load_brand_qtys_by_plan_product_ids(), persist_plan_product_brand_qtys(), Any, Decimal, Session (+20 more)
|
||||
|
||||
### Community 26 - "Community 26"
|
||||
Cohesion: 0.08
|
||||
Nodes (70): day_detail(), delete_assignment(), duty_user_options(), _enrich_assign_names(), month_summary(), Session, 指定用户时补全 personName(用 nick_name)。, 值班可安排人员:仅「值班人员」角色下的启用用户。 (+62 more)
|
||||
Cohesion: 0.20
|
||||
Nodes (31): DutyAssignment, DutyDay, DutySlot, 值班排班模型 — docs/superpowers/specs/2026-08-14-duty-schedule-design.md, assert_user_is_duty_staff(), _assignment_dict(), build_public_url(), _count_active_assignments() (+23 more)
|
||||
|
||||
### Community 27 - "Community 27"
|
||||
Cohesion: 0.11
|
||||
Nodes (32): CRM 报价单 API 路由 — 挂接到 crm.router, QuoteApproverSettingSaveBody, QuoteAcceptBody, QuoteApprovalApproveBody, QuoteApprovalListBody, QuoteApprovalRefreshNamesBody, QuoteApprovalWithdrawBody, QuoteCreateBody (+24 more)
|
||||
Cohesion: 0.10
|
||||
Nodes (34): CRM 报价单 API 路由 — 挂接到 crm.router, 接口菜单权限守卫。super_admin 直接放行。, require_menu_code(), QuoteApproverSettingSaveBody, QuoteAcceptBody, QuoteApprovalApproveBody, QuoteApprovalListBody, QuoteApprovalRefreshNamesBody (+26 more)
|
||||
|
||||
### Community 28 - "Community 28"
|
||||
Cohesion: 0.06
|
||||
Nodes (94): approve_settlement_legacy(), cancel_settlement(), create_settlement(), delete_settlement(), disburse_settlement(), _generate_settlement_no(), get_settlement(), get_settlement_approval_flow() (+86 more)
|
||||
Cohesion: 0.10
|
||||
Nodes (59): PlFreightSettlement, PlFreightSettlementApproval, PlRemittanceVoucher, 运费结算表 pl_freight_settlement, 运费结算审批 pl_freight_settlement_approval, 汇款凭证表 pl_remittance_voucher, _advance_after_approval(), _approval_to_dict() (+51 more)
|
||||
|
||||
### Community 29 - "Community 29"
|
||||
Cohesion: 0.08
|
||||
Nodes (96): register_contract_routes(), _apply_contract_entity_fields(), batch_assign_owner(), batch_submit_contract_approval(), _build_contract_filters(), _build_contract_name_from_parties(), _build_customer_address(), _calc_amount_with_tax() (+88 more)
|
||||
Cohesion: 0.07
|
||||
Nodes (95): register_contract_routes(), _apply_contract_entity_fields(), delete_pending_approvals(), Java: withdraw — 软删除 pending 审批, batch_assign_owner(), batch_submit_contract_approval(), _build_contract_filters(), _build_contract_name_from_parties() (+87 more)
|
||||
|
||||
### Community 30 - "Community 30"
|
||||
Cohesion: 0.16
|
||||
Nodes (26): clear_target_categories(), compute_level(), count_target_rows(), derive_source_url(), fetch_portal_admin_id(), fetch_source_rows(), legacy_id_to_str(), load_runtime_settings() (+18 more)
|
||||
|
||||
### Community 33 - "Community 33"
|
||||
### Community 31 - "Community 31"
|
||||
Cohesion: 0.18
|
||||
Nodes (25): CRM 合同子模块路由 — 变更/模板/审批独立页, register_contract_sub_routes(), _change_row_to_dict(), create_contract_change(), execute_change(), export_contract_changes(), _get_change_row(), get_contract_change_detail() (+17 more)
|
||||
Nodes (17): get_oper_log_meta(), OperLogMeta, 请求级操作日志元数据(ContextVar), 供异步落库线程使用,避免 ContextVar 跨线程丢失。, set_oper_log_meta(), snapshot_oper_log_meta(), _format_summary(), oper_log() (+9 more)
|
||||
|
||||
### Community 32 - "Community 32"
|
||||
Cohesion: 0.22
|
||||
Nodes (18): apply_crm_create_by_scope(), _apply_list_filters(), create_supplier_info(), get_supplier_info(), list_all_supplier_info(), list_supplier_info(), Any, Session (+10 more)
|
||||
|
||||
### Community 33 - "Community 33"
|
||||
Cohesion: 0.20
|
||||
Nodes (16): ApiCryptoMiddleware, ApiRequestLogMiddleware, _claims_from_auth(), _format_body_bytes(), _persist_log(), Any, Request, 统一记录 API 请求/响应出入参的中间件 (+8 more)
|
||||
|
||||
### Community 34 - "Community 34"
|
||||
Cohesion: 0.26
|
||||
Nodes (6): start_instance(), StartInstanceBody, WfNode, Any, Session, WorkflowEngine
|
||||
Cohesion: 0.16
|
||||
Nodes (13): create_instance(), 流程连线/转移 wf_transition, 流程实例 wf_process_instance, WfProcessInstance, WfTransition, _dispatch_local(), Any, 尝试本地直调。返回 True 表示已处理,False 表示无匹配的本地回调。 (+5 more)
|
||||
|
||||
### Community 35 - "Community 35"
|
||||
Cohesion: 0.18
|
||||
Nodes (36): clear_portal_workflow(), _dec(), derive_source_url(), fetch_portal_admin_id(), fetch_source_rows(), legacy_id_to_str(), load_bx_user_mapping(), load_runtime_settings() (+28 more)
|
||||
|
||||
### Community 36 - "Community 36"
|
||||
Cohesion: 0.15
|
||||
Nodes (27): 验证密码(哈希非 bcrypt 或损坏时返回 False,避免抛错), verify_password(), change_password(), create_user(), get_user(), get_user_with_relations(), _hash_password(), list_post_ids_for_user() (+19 more)
|
||||
Cohesion: 0.14
|
||||
Nodes (29): 验证密码(哈希非 bcrypt 或损坏时返回 False,避免抛错), verify_password(), change_password(), create_user(), get_user(), get_user_with_relations(), _hash_password(), list_post_ids_for_user() (+21 more)
|
||||
|
||||
### Community 37 - "Community 37"
|
||||
Cohesion: 0.20
|
||||
@@ -343,11 +362,19 @@ Nodes (24): count_source_rows(), derive_source_url(), fetch_portal_admin_id(), f
|
||||
|
||||
### Community 38 - "Community 38"
|
||||
Cohesion: 0.19
|
||||
Nodes (14): create_expense_item(), delete_expense_item(), get_expense_item(), get_expense_item_tree(), Session, update_expense_item(), get_bx_current_user(), BxExpenseItem (+6 more)
|
||||
Nodes (12): create_expense_item(), delete_expense_item(), get_expense_item(), get_expense_item_tree(), Session, update_expense_item(), BxExpenseItem, Any (+4 more)
|
||||
|
||||
### Community 39 - "Community 39"
|
||||
Cohesion: 0.13
|
||||
Nodes (42): clue_follow_detail(), assign_clues(), check_convert(), check_duplicate_clue(), convert_clue(), create_clue(), _enrich_user_names(), _ensure_clue_in_scope() (+34 more)
|
||||
Cohesion: 0.14
|
||||
Nodes (42): assign_clues(), check_convert(), check_duplicate_clue(), convert_clue(), create_clue(), _enrich_user_names(), _ensure_clue_in_scope(), create_clue_follow() (+34 more)
|
||||
|
||||
### Community 40 - "Community 40"
|
||||
Cohesion: 0.19
|
||||
Nodes (16): grab(), list_duty_staff_users(), normalize_person_name(), 按固定 id 查找「值班人员」角色;找不到再按角色名兜底。, 本租户启用且绑定「值班人员」角色的用户(公开页 users / 管理端 options)。, resolve_duty_staff_role_id(), test_grab_rejects_duplicate_same_shift(), test_grab_rejects_invalid_user() (+8 more)
|
||||
|
||||
### Community 41 - "Community 41"
|
||||
Cohesion: 0.12
|
||||
Nodes (15): category_add(), category_delete(), category_tree(), category_update(), product_delete(), 删除产品(支持单条 body.id 和批量 body.ids), CategoryDeleteBody, ProductAliasItem (+7 more)
|
||||
|
||||
### Community 42 - "Community 42"
|
||||
Cohesion: 0.19
|
||||
@@ -355,31 +382,35 @@ Nodes (32): _dec(), derive_source_url(), fetch_portal_admin_id(), fetch_source_r
|
||||
|
||||
### Community 43 - "Community 43"
|
||||
Cohesion: 0.12
|
||||
Nodes (23): bx_health(), 报销(bx)API 路由聚合 — REST 风格,协议对齐 bx 原系统。, P0 健康检查:验证 bx 路由已挂载且响应协议为 code=0。, create_invoice(), delete_invoice(), get_mine_wallet_summary(), list_invoice_ledgers(), list_invoice_wallets() (+15 more)
|
||||
Nodes (25): create_invoice(), delete_invoice(), get_mine_wallet_summary(), list_invoice_ledgers(), list_invoice_wallets(), list_invoices(), Request, Session (+17 more)
|
||||
|
||||
### Community 44 - "Community 44"
|
||||
Cohesion: 0.18
|
||||
Nodes (30): _dec(), derive_source_url(), fetch_portal_admin_id(), fetch_source_rows(), legacy_id_to_str(), legacy_parent_id(), load_oss_id_to_url(), load_runtime_settings() (+22 more)
|
||||
Nodes (31): _dec(), derive_source_url(), fetch_portal_admin_id(), fetch_source_rows(), legacy_id_to_str(), legacy_parent_id(), load_oss_id_to_url(), load_runtime_settings() (+23 more)
|
||||
|
||||
### Community 45 - "Community 45"
|
||||
Cohesion: 0.09
|
||||
Nodes (70): _build_driver_url(), get_plan_arrange_info(), 获取计划已安排运输后的司机链接与地磅出入库关联(供查看弹窗复制简述/二维码)。, _apply_plan_logistics_from_payload(), _apply_transport_arrange_fields(), arrange_transport(), _assert_vehicle_carrier_eligible(), _build_arrange_result() (+62 more)
|
||||
Cohesion: 0.08
|
||||
Nodes (76): _build_driver_url(), get_plan_arrange_info(), 获取计划已安排运输后的司机链接与地磅出入库关联(供查看弹窗复制简述/二维码)。, _apply_plan_logistics_from_payload(), _apply_transport_arrange_fields(), arrange_transport(), _assert_vehicle_carrier_eligible(), _build_arrange_result() (+68 more)
|
||||
|
||||
### Community 46 - "Community 46"
|
||||
Cohesion: 0.16
|
||||
Nodes (32): Settings, BaseSettings, apply_biz_backfill(), BackfillRule, BizRef, build_biz_index(), count_target_rows(), derive_source_url() (+24 more)
|
||||
Cohesion: 0.19
|
||||
Nodes (28): apply_biz_backfill(), BackfillRule, BizRef, build_biz_index(), count_target_rows(), derive_source_url(), fetch_portal_admin_id(), fetch_source_rows() (+20 more)
|
||||
|
||||
### Community 47 - "Community 47"
|
||||
Cohesion: 0.27
|
||||
Nodes (11): customer_dealed(), customer_export(), customer_key(), customer_list(), customer_long_time_no_follow(), customer_mine(), customer_to_follow(), customer_week_new() (+3 more)
|
||||
|
||||
### Community 48 - "Community 48"
|
||||
Cohesion: 0.17
|
||||
Nodes (22): cancel_apply(), create_apply(), create_disburse(), delete_applies(), delete_disburses(), get_apply(), get_disburse(), list_applies() (+14 more)
|
||||
Cohesion: 0.31
|
||||
Nodes (9): change_current_user_password(), get_current_user_profile(), get_web_auth_menu_codes(), public_quote_share(), Request, Session, 获取当前用户信息(与 sys_user 字段对应)。, 获取当前用户菜单权限标识(sys_menu.perms,经角色继承)。 (+1 more)
|
||||
|
||||
### Community 49 - "Community 49"
|
||||
Cohesion: 0.49
|
||||
Nodes (10): clue_statistics_level(), _apply_create_range(), _base_query(), get_clue_level_statistics(), get_clue_overview_statistics(), get_clue_owner_statistics(), get_clue_source_statistics(), get_clue_trend_statistics() (+2 more)
|
||||
|
||||
### Community 50 - "Community 50"
|
||||
Cohesion: 0.08
|
||||
Nodes (25): clue_assign(), clue_convert(), clue_create(), clue_delete(), clue_follow_create(), clue_follow_delete(), clue_follow_list(), clue_follow_update() (+17 more)
|
||||
Cohesion: 0.39
|
||||
Nodes (8): get_redis_client(), get_sync_redis_client(), Redis, 共享 Redis 客户端(同步:会话鉴权;异步:API 加密 nonce)。, 同步 Redis(auth 会话等,在线程池中调用)。, 异步 Redis(API 加密 nonce 等中间件)。, _redis_common_kwargs(), redis_enabled()
|
||||
|
||||
### Community 51 - "Community 51"
|
||||
Cohesion: 0.25
|
||||
@@ -397,6 +428,18 @@ Nodes (24): PlTransportProductBrandQty, PlTransportProductFeedback, 运输单按
|
||||
Cohesion: 0.19
|
||||
Nodes (28): _get_by_plan(), get_inbound(), get_inbound_by_plan(), _get_order(), get_outbound(), get_outbound_by_plan(), inbound_site_url(), list_inbound() (+20 more)
|
||||
|
||||
### Community 55 - "Community 55"
|
||||
Cohesion: 0.50
|
||||
Nodes (4): ApiRequestLogCRUD, Session, SysApiRequestLog, SysApiRequestLogHis
|
||||
|
||||
### Community 56 - "Community 56"
|
||||
Cohesion: 0.38
|
||||
Nodes (4): _coerce_id(), ExpenseItemCreate, ExpenseItemOut, ExpenseItemUpdate
|
||||
|
||||
### Community 57 - "Community 57"
|
||||
Cohesion: 0.50
|
||||
Nodes (4): public_grab(), public_page(), Session, DutyGrabBody
|
||||
|
||||
### Community 58 - "Community 58"
|
||||
Cohesion: 0.28
|
||||
Nodes (17): _address_to_read(), _build_address_query(), create_transport_address(), delete_transport_address(), _get_address(), get_transport_address(), list_all_transport_addresses(), list_transport_addresses() (+9 more)
|
||||
@@ -411,27 +454,27 @@ Nodes (16): 供方全量列表(下拉)— Java: CrmSupplierInfoController.li
|
||||
|
||||
### Community 61 - "Community 61"
|
||||
Cohesion: 0.11
|
||||
Nodes (42): PlTransportDbLink, 运输-地磅出入库关联 pl_transport_db_link, _apply_plan_quantity_delta(), _create_db_inventory(), _create_db_plan(), DibangInventoryLinkResult, find_monthly_db_plan(), _generate_batch_no() (+34 more)
|
||||
Nodes (43): PlTransportDbLink, 运输-地磅出入库关联 pl_transport_db_link, _apply_plan_quantity_delta(), _create_db_inventory(), _create_db_plan(), DibangInventoryLinkResult, find_monthly_db_plan(), _generate_batch_no() (+35 more)
|
||||
|
||||
### Community 62 - "Community 62"
|
||||
Cohesion: 0.16
|
||||
Nodes (27): _apply_weighing_product_update(), _apply_weighing_update(), _base_query(), create_weighing(), delete_weighing(), _get_weighing(), get_weighing_related(), list_weighing() (+19 more)
|
||||
Cohesion: 0.13
|
||||
Nodes (38): _apply_weighing_product_update(), _apply_weighing_update(), _base_query(), create_weighing(), delete_weighing(), _get_weighing(), get_weighing_related(), list_weighing() (+30 more)
|
||||
|
||||
### Community 63 - "Community 63"
|
||||
Cohesion: 0.24
|
||||
Nodes (13): BxInvoiceLedger, BxInvoiceWalletSummary, InvoiceLedgerService, Any, Decimal, Session, should_skip_release(), summarize_entries() (+5 more)
|
||||
Cohesion: 0.33
|
||||
Nodes (8): Any, should_skip_release(), summarize_entries(), 提交占用可超过已上传总额,余额可为负(补票后再回升)。, test_release_idempotent_guard(), test_summarize_allows_negative_balance(), test_summarize_delete_and_release(), test_summarize_upload_and_reimburse()
|
||||
|
||||
### Community 64 - "Community 64"
|
||||
Cohesion: 0.30
|
||||
Nodes (15): paste_recognize_commit(), ConflictAction, PasteCommitBody, PasteCommitResult, PastePersonnelIn, PasteVehicleIn, 运输计划 - 三方粘贴识别提交 Schema, commit_paste_recognize() (+7 more)
|
||||
|
||||
### Community 65 - "Community 65"
|
||||
Cohesion: 0.16
|
||||
Nodes (40): PlVehicleMaintenanceApply, PlVehicleMaintenanceProcess, 车辆维保申请 pl_vehicle_maintenance_apply, 车辆维保处理登记 pl_vehicle_maintenance_process, Decimal, VehicleMaintenanceApplyCreate, VehicleMaintenanceApplyRead, VehicleMaintenanceApplyUpdate (+32 more)
|
||||
Cohesion: 0.10
|
||||
Nodes (58): cancel_apply(), create_apply(), create_process(), delete_applies(), delete_processes(), get_apply(), get_process(), list_applies() (+50 more)
|
||||
|
||||
### Community 66 - "Community 66"
|
||||
Cohesion: 0.14
|
||||
Nodes (21): create_workflow(), delete_workflow(), get_workflow(), get_workflow_by_biz_type(), list_workflows(), Session, save_workflow_nodes(), update_workflow() (+13 more)
|
||||
Cohesion: 0.10
|
||||
Nodes (33): bind_biz(), delete_attachment(), download_file(), list_by_biz(), _parse_id_list(), preview_attachment(), Session, UploadFile (+25 more)
|
||||
|
||||
### Community 67 - "Community 67"
|
||||
Cohesion: 0.12
|
||||
@@ -443,75 +486,67 @@ Nodes (13): _build_tree(), _cascade_soft_delete(), _collect_with_ancestors(), cr
|
||||
|
||||
### Community 69 - "Community 69"
|
||||
Cohesion: 0.15
|
||||
Nodes (25): _append_user_scope_conditions(), apply_data_scope(), assert_row_in_data_scope(), _collect_scope_ids(), _get_child_dept_ids(), get_user_dept_id(), Session, 数据权限过滤(V0.3:基于角色 data_scope 的查询级行级过滤) data_scope 值: 1 = 全部数据 2 = 本部门数据 3 (+17 more)
|
||||
Nodes (23): _append_user_scope_conditions(), apply_data_scope(), _collect_scope_ids(), _get_child_dept_ids(), get_user_dept_id(), Session, 合并多角色数据范围为 dept_ids / user_ids 并集。, 根据用户角色的数据权限过滤查询。 适用于业务单据列表/详情(报销单、客户、运单等)。 以下场景 SHALL NOT 调用本函数(仅租户隔离即 (+15 more)
|
||||
|
||||
### Community 70 - "Community 70"
|
||||
Cohesion: 0.19
|
||||
Nodes (16): align_contract_no_to_sign_date(), date, 标准编号替换日期段,保留序号;非标准返回 None。, 将标准合同编号日期段对齐签订日;撞号则序号顺延(含软删查重)。 非标准编号原样返回。无签订日则原样返回。, replace_contract_no_date(), _mock_last_contract(), 软删占用的编号仍占唯一索引,生成时须跳过(对齐 uk_contract_tenant_no)。, 取最大流水号须含软删,避免复用已软删的最大编号。 (+8 more)
|
||||
Cohesion: 0.18
|
||||
Nodes (18): align_contract_no_to_sign_date(), generate_contract_no(), date, Java: generateContractNo — {供方编码}/X-JT{yyyyMMdd}{3位序号} 流水与撞号检测须含软删:uk_contr, 标准编号替换日期段,保留序号;非标准返回 None。, 将标准合同编号日期段对齐签订日;撞号则序号顺延(含软删查重)。 非标准编号原样返回。无签订日则原样返回。, replace_contract_no_date(), _mock_last_contract() (+10 more)
|
||||
|
||||
### Community 71 - "Community 71"
|
||||
Cohesion: 0.13
|
||||
Nodes (26): PlCert, CertRead, parse_resource_snapshot(), Any, 运输计划 - 安排运输时车辆/人员/证件资源快照 Schema, 从 ORM 文本列或 dict 解析快照。, SnapshotCertRead, SnapshotPersonnelRow (+18 more)
|
||||
Cohesion: 0.12
|
||||
Nodes (28): list_vehicles(), _vehicle_to_read(), PlCert, CertRead, parse_resource_snapshot(), Any, 运输计划 - 安排运输时车辆/人员/证件资源快照 Schema, 从 ORM 文本列或 dict 解析快照。 (+20 more)
|
||||
|
||||
### Community 72 - "Community 72"
|
||||
Cohesion: 0.20
|
||||
Nodes (23): bind_vehicle_driver(), create_driver(), create_personnel(), delete_carrier(), delete_driver(), delete_personnel(), delete_vehicle(), get_driver() (+15 more)
|
||||
Cohesion: 0.67
|
||||
Nodes (3): Web 管理端约定角色(由 sys_role.role_name 映射而来,非数据库枚举), UserRole, str
|
||||
|
||||
### Community 73 - "Community 73"
|
||||
Cohesion: 0.20
|
||||
Nodes (25): derive_source_url(), fetch_portal_admin_id(), fetch_source_rows(), legacy_id_to_str(), load_oss_id_to_url(), load_runtime_settings(), load_settings_env(), load_user_mapping() (+17 more)
|
||||
|
||||
### Community 74 - "Community 74"
|
||||
Cohesion: 0.12
|
||||
Nodes (21): list_public_vehicle_plates(), Session, 地磅公共只读:免登车牌 options(现场填写页), 免登:按出入库/计划解析 tenant_no,返回 pl_vehicle 车牌(self + carrier)。, _resolve_tenant_no(), menu_badge_stats(), Session, Session (+13 more)
|
||||
Cohesion: 0.28
|
||||
Nodes (14): menu_badge_stats(), Session, apply_pl_scope(), Session, PL 模块数据权限辅助(create_by / create_dept_id,无 owner_field)。, build_menu_badge_counts(), count_document_reminder_expired_pending(), count_freight_approval_pending() (+6 more)
|
||||
|
||||
### Community 77 - "Community 77"
|
||||
Cohesion: 0.11
|
||||
Nodes (12): get_wechat_pay_client(), get_wechat_pay_v2(), Any, 微信支付工具模块 支持微信支付 V3 API 文档参考: https://pay.weixin.qq.com/docs/v3/apis/index.html, 统一下单接口 (V3) Args: out_trade_no: 商户订单号 total_amo, 申请退款 Args: out_trade_no: 商户订单号 total_amount: 订单, 小程序调起支付的二次签名参数(与公众号 JSAPI 规则一致), 统一下单 (V2 API) Args: out_trade_no: 商户订单号 total_f (+4 more)
|
||||
|
||||
### Community 78 - "Community 78"
|
||||
Cohesion: 0.24
|
||||
Nodes (18): _actor_ids(), _actor_names(), assert_row_deletable_by_creator(), assert_rows_deletable_by_creator(), can_delete_by_creator(), is_super_admin(), normalize_user_info(), Any (+10 more)
|
||||
|
||||
### Community 81 - "Community 81"
|
||||
Cohesion: 0.29
|
||||
Nodes (12): build_name_pinyin_abbr(), normalize_name_pinyin_abbr(), 名称拼音首字母缩写:库内存小写,检索时大小写不敏感。, 保存时解析拼音字段。 - submitted_provided=True:用提交值(空则按 name 生成) - 否则:已有非空 existi, resolve_name_pinyin_abbr(), test_chinese_and_alnum(), test_empty_and_whitespace(), test_resolve_not_submitted_empty_existing_generates() (+4 more)
|
||||
Cohesion: 0.17
|
||||
Nodes (26): delete_plan(), delete_carrier(), delete_driver(), delete_personnel(), delete_vehicle(), unbind_vehicle_driver(), delete_plan(), _actor_ids() (+18 more)
|
||||
|
||||
### Community 83 - "Community 83"
|
||||
Cohesion: 0.14
|
||||
Nodes (32): _advance_contract_after_approval(), approve_contract(), approve_contract_by_contract_id(), approve_pass(), approve_reject(), approve_transfer(), _build_approval_list_filters(), can_user_approve() (+24 more)
|
||||
Cohesion: 0.15
|
||||
Nodes (30): _advance_contract_after_approval(), approve_contract(), approve_contract_by_contract_id(), approve_pass(), approve_reject(), approve_transfer(), _build_approval_list_filters(), can_user_approve() (+22 more)
|
||||
|
||||
### Community 84 - "Community 84"
|
||||
Cohesion: 0.23
|
||||
Nodes (23): build_id_maps(), clip_text(), count_rows(), export_mapping(), fetch_rows(), legacy_id_to_str(), main(), map_id() (+15 more)
|
||||
|
||||
### Community 86 - "Community 86"
|
||||
Cohesion: 0.21
|
||||
Nodes (15): create_leave(), get_leave(), list_leaves(), Any, Session, _row_to_dict(), soft_delete_leave(), update_leave() (+7 more)
|
||||
Cohesion: 0.40
|
||||
Nodes (9): create_leave(), get_leave(), list_leaves(), Any, Session, _row_to_dict(), soft_delete_leave(), update_leave() (+1 more)
|
||||
|
||||
### Community 87 - "Community 87"
|
||||
Cohesion: 0.11
|
||||
Nodes (32): approve_quote(), can_user_approve(), create_approval_records(), delete_pending_approvals(), get_quote_approval(), invalidate_all_approvals(), list_quote_approvals(), Any (+24 more)
|
||||
|
||||
### Community 88 - "Any"
|
||||
Cohesion: 0.34
|
||||
Nodes (18): batch_add_opportunity_products(), _calc_subtotal(), create_opportunity_product(), _decimal_value(), _fill_product_redundant(), _get_opportunity_for_product(), get_opportunity_product(), list_opportunity_products() (+10 more)
|
||||
|
||||
### Community 89 - "pl_ocr_service.py"
|
||||
Cohesion: 0.19
|
||||
Nodes (22): PlOcrUsageMonth, 阿里云 OCR 月度调用计数 pl_ocr_usage_month(按自然月全局统计), assert_quota_available(), _build_ocr_client(), compress_image_bytes(), current_usage_month(), _first_attachment_url(), get_or_create_usage_row() (+14 more)
|
||||
|
||||
### Community 91 - "pl_vehicle_recharge_service.py"
|
||||
Cohesion: 0.13
|
||||
Nodes (34): apply_crm_create_by_scope(), ensure_crm_create_by_row_in_scope(), ensure_crm_customer_linked_row_in_scope(), ensure_crm_owner_row_in_scope(), Session, CRM 数据权限辅助(负责人 owner_user_id + 标准 create_by/create_dept_id)。, 客户子表(开票信息等):先校验关联客户 owner scope,否则校验 create_by。, _clear_default_for_customer() (+26 more)
|
||||
Cohesion: 0.14
|
||||
Nodes (34): create_contact(), get_contact(), list_contacts_by_customer(), _parse_optional_date(), Any, date, Session, _row_to_dict() (+26 more)
|
||||
|
||||
### Community 92 - "Community 92"
|
||||
Cohesion: 0.20
|
||||
Nodes (21): _assert_users_exist(), get_setting(), Any, Session, _row_to_dict(), save_setting(), _assert_users_exist(), get_setting() (+13 more)
|
||||
Cohesion: 0.18
|
||||
Nodes (22): _assert_users_exist(), get_setting(), Any, Session, _row_to_dict(), save_setting(), _assert_users_exist(), get_setting() (+14 more)
|
||||
|
||||
### Community 93 - "_invoice_info_by_customer_impl"
|
||||
Cohesion: 0.19
|
||||
Nodes (11): get_current_time(), parse_datetime(), datetime, allocate_next_biz_no(), _format_biz_no(), _prefix_for(), datetime, Session (+3 more)
|
||||
Cohesion: 0.33
|
||||
Nodes (8): allocate_next_biz_no(), _format_biz_no(), _prefix_for(), datetime, Session, 业务协议号自动生成:XBCC / JZCC + YYYYMMDD + 6 位序号, 原子分配下一个业务协议号。 须在 create 事务内调用;使用 MySQL LAST_INSERT_ID 保证并发安全。, BizNoKind
|
||||
|
||||
### Community 94 - "response.py"
|
||||
Cohesion: 0.18
|
||||
@@ -526,12 +561,8 @@ Cohesion: 0.21
|
||||
Nodes (22): clear_target_products(), count_target_rows(), _dec(), derive_source_url(), fetch_portal_admin_id(), fetch_source_rows(), legacy_id_to_str(), load_runtime_settings() (+14 more)
|
||||
|
||||
### Community 100 - "price_letter_detail"
|
||||
Cohesion: 0.31
|
||||
Nodes (13): apply_renumber(), build_mapping(), fetch_products(), format_new_product_no(), load_settings_env(), main(), product_id_sort_key(), Engine (+5 more)
|
||||
|
||||
### Community 101 - ".to_tenant_user_dict"
|
||||
Cohesion: 0.16
|
||||
Nodes (18): _build_quote_filters(), _fill_contract_linked_count(), list_quotes(), _parse_optional_datetime(), _period_key(), Any, datetime, Query (+10 more)
|
||||
Cohesion: 0.27
|
||||
Nodes (15): Settings, BaseSettings, apply_renumber(), build_mapping(), fetch_products(), format_new_product_no(), load_settings_env(), main() (+7 more)
|
||||
|
||||
### Community 102 - "dept.py"
|
||||
Cohesion: 0.25
|
||||
@@ -545,17 +576,13 @@ Nodes (15): create_price_letter(), get_price_letter(), list_price_letters(), _pa
|
||||
Cohesion: 0.15
|
||||
Nodes (9): 微信消息通知服务 支持微信小程序模板消息推送 使用方式: from app.core.wechat_message import WechatMes, 发送订单状态通知 Args: openid: 用户openid order_n, 发送支付成功通知 Args: openid: 用户openid order_n, 发送配送通知 Args: openid: 配送员openid task_no:, 发送配送时效预警 Args: openid: 配送员openid task_n, 发送退款结果通知 Args: openid: 用户openid order_n, 发送催单通知给配送员 Args: deliver_openid: 配送员openid, 发送模板消息 Args: openid: 用户openid template_ (+1 more)
|
||||
|
||||
### Community 106 - "Community 106"
|
||||
Cohesion: 0.12
|
||||
Nodes (15): pool_rule_add(), pool_rule_delete(), pool_rule_detail(), pool_rule_execute(), pool_rule_list(), pool_rule_update(), pool_rule_update_status(), PoolRuleAddBody (+7 more)
|
||||
|
||||
### Community 107 - "apply_pl_scope"
|
||||
Cohesion: 0.15
|
||||
Nodes (32): create_instance(), create_process_def(), create_task(), get_all_tasks_for_business(), get_instance(), get_latest_version(), get_nodes_by_version(), get_pending_tasks_by_node() (+24 more)
|
||||
Cohesion: 0.14
|
||||
Nodes (31): create_process_def(), create_task(), get_all_tasks_for_business(), get_instance(), get_latest_version(), get_nodes_by_version(), get_pending_tasks_by_node(), get_process_def() (+23 more)
|
||||
|
||||
### Community 109 - "Community 109"
|
||||
Cohesion: 0.46
|
||||
Nodes (7): VehiclePersonnelRow, load_vehicle_personnel_rows(), Session, 运输计划 - 人员 upsert 与车辆-人员关联保存, 按 personnelId → phone → name 弱匹配 upsert,返回 personnel_id。, save_vehicle_personnel(), upsert_personnel()
|
||||
Cohesion: 0.27
|
||||
Nodes (14): create_vehicle(), get_vehicle(), update_vehicle(), _vehicle_to_detail(), VehicleDetailRead, VehiclePersonnelRow, load_vehicle_personnel_rows(), load_vehicle_personnel_summaries() (+6 more)
|
||||
|
||||
### Community 110 - "Base"
|
||||
Cohesion: 0.31
|
||||
@@ -578,16 +605,16 @@ Cohesion: 0.24
|
||||
Nodes (12): DbSequence, 地磅(db)模块数据模型 — 对齐 docs/sql/db_tables.sql, _allocate_inbound_outbound_id(), _ensure_sequence_row(), max_numeric_inbound_outbound_id(), next_inbound_outbound_id(), Session, 出入库单 id 生成 — 租户内纯数字自增,并发安全。 (+4 more)
|
||||
|
||||
### Community 115 - "site.py"
|
||||
Cohesion: 0.10
|
||||
Nodes (45): approve_carrier(), create_carrier(), create_vehicle(), get_carrier(), get_vehicle(), list_carriers(), list_drivers(), list_vehicles() (+37 more)
|
||||
Cohesion: 0.09
|
||||
Nodes (52): approve_carrier(), bind_vehicle_driver(), create_carrier(), create_driver(), create_personnel(), get_carrier(), get_driver(), get_personnel() (+44 more)
|
||||
|
||||
### Community 116 - "Community 116"
|
||||
Cohesion: 0.17
|
||||
Nodes (28): batch_insert_quote_products(), calculate_subtotal(), _decimal_value(), delete_products_by_quote_id(), _ensure_quote_parent_scope(), _get_field(), get_quote_product(), insert_quote_product() (+20 more)
|
||||
|
||||
### Community 118 - "Community 118"
|
||||
Cohesion: 0.15
|
||||
Nodes (29): batch_insert_contract_products(), calculate_subtotal(), _decimal_value(), delete_price_tiers_by_contract_id(), delete_products_by_contract_id(), _get_field(), insert_contract_product(), _letter_oss_ids_to_json() (+21 more)
|
||||
Cohesion: 0.12
|
||||
Nodes (37): batch_insert_contract_products(), calculate_subtotal(), _decimal_value(), delete_price_tiers_by_contract_id(), delete_products_by_contract_id(), _get_field(), insert_contract_product(), _letter_oss_ids_to_json() (+29 more)
|
||||
|
||||
### Community 119 - "Community 119"
|
||||
Cohesion: 0.16
|
||||
@@ -598,17 +625,13 @@ Cohesion: 0.39
|
||||
Nodes (10): create_post(), get_post(), list_posts(), post_code_exists(), Any, Session, _row_to_dict(), soft_delete_post() (+2 more)
|
||||
|
||||
### Community 125 - "Community 125"
|
||||
Cohesion: 0.24
|
||||
Nodes (7): camel_to_snake(), CRUDBase, get_model_attr(), HistoryCRUD, Any, Session, ModelType
|
||||
Cohesion: 0.23
|
||||
Nodes (8): generate_id(), camel_to_snake(), CRUDBase, get_model_attr(), HistoryCRUD, Any, Session, ModelType
|
||||
|
||||
### Community 126 - "customer_pool.py"
|
||||
Cohesion: 0.36
|
||||
Nodes (13): _build_tree(), _cascade_soft_delete(), create_menu(), get_menu(), list_menus(), list_menus_tree(), menu_name_exists(), Any (+5 more)
|
||||
|
||||
### Community 127 - "Community 127"
|
||||
Cohesion: 0.37
|
||||
Nodes (14): create_opportunity_follow(), _ensure_parent_opportunity(), list_follows_by_opportunity(), list_opportunity_follows(), _parse_optional_datetime(), Any, datetime, Session (+6 more)
|
||||
|
||||
### Community 128 - "plan.py"
|
||||
Cohesion: 0.39
|
||||
Nodes (10): create_site(), get_site(), list_sites(), Any, Session, _row_to_dict(), site_code_exists(), soft_delete_site() (+2 more)
|
||||
@@ -618,12 +641,8 @@ Cohesion: 0.12
|
||||
Nodes (24): _auto_contract_name_body(), generate_contract_name(), 按面板单位全称精确匹配交易主体/客户主数据,取 shortName;对不上则用全称。, 前端预览误用单位全称时,名称片段会等于面板/客户/开票全称。, 按面板供方(partyA)-需方(partyB)生成:{供方简称}-{需方简称}({合同编号})。 - 名称为空:按主数据简称自动生成 - 名, resolve_party_short_name(), _strip_contract_name_no_suffix(), _user_contract_name_uses_party_full_name() (+16 more)
|
||||
|
||||
### Community 130 - "datetime_parse.py"
|
||||
Cohesion: 0.06
|
||||
Nodes (56): operlog_detail(), operlog_list(), Session, 操作日志分页列表(基于 API 请求日志适配 RuoYi OperLog 结构), _user_info(), _claims_from_auth(), _format_body_bytes(), _persist_log() (+48 more)
|
||||
|
||||
### Community 131 - "LocalStorage"
|
||||
Cohesion: 0.18
|
||||
Nodes (22): cancel_apply(), create_apply(), create_process(), delete_applies(), delete_processes(), get_apply(), get_process(), list_applies() (+14 more)
|
||||
Cohesion: 0.16
|
||||
Nodes (22): operlog_detail(), operlog_list(), Session, 操作日志分页列表(基于 API 请求日志适配 RuoYi OperLog 结构), _user_info(), 获取用户所有角色的数据权限配置。 返回: [{"data_scope": str, "details": [SysRoleDataScopeDetai, _resolve_user_role_scopes(), business_type_from_path() (+14 more)
|
||||
|
||||
### Community 132 - "crm_supplier_hazard.py"
|
||||
Cohesion: 0.15
|
||||
@@ -634,12 +653,12 @@ Cohesion: 0.25
|
||||
Nodes (18): batch_generate_sample_codes(), batch_issue_sample_codes(), batch_revoke_sample_codes(), create_sample_code(), get_sample_code(), list_sample_codes(), _parse_issued_time(), Any (+10 more)
|
||||
|
||||
### Community 135 - "unsettled_plans_q_filter"
|
||||
Cohesion: 0.17
|
||||
Cohesion: 0.18
|
||||
Nodes (23): cancel_apply(), create_apply(), create_record(), delete_applies(), delete_records(), get_apply(), get_record(), list_applies() (+15 more)
|
||||
|
||||
### Community 136 - "Community 136"
|
||||
Cohesion: 0.19
|
||||
Nodes (18): create_role(), get_role(), get_role_data_scope(), get_role_menu_ids(), list_roles(), Any, Session, 角色 CRUD(sys_role / sys_role_menu / sys_role_data_scope_detail) (+10 more)
|
||||
Cohesion: 0.22
|
||||
Nodes (16): create_role(), get_role(), get_role_data_scope(), get_role_menu_ids(), list_roles(), Any, Session, 角色 CRUD(sys_role / sys_role_menu / sys_role_data_scope_detail) (+8 more)
|
||||
|
||||
### Community 137 - "statistics.py"
|
||||
Cohesion: 0.35
|
||||
@@ -654,12 +673,8 @@ Cohesion: 0.22
|
||||
Nodes (15): get_storage(), download_file_bytes(), file_stream(), get_access_url(), get_download_url(), get_preview_url(), normalize_storage_path(), 报销附件存储适配 — 复用门户 app.core.storage,对外保持 bx 协议。 (+7 more)
|
||||
|
||||
### Community 141 - "PlTransport"
|
||||
Cohesion: 0.17
|
||||
Nodes (27): approve_task(), _assert_wf_instance_in_scope(), _business_type_label(), create_process_def(), _enrich_task_list(), get_business_all_tasks(), _get_business_form_fields(), get_designer_data() (+19 more)
|
||||
|
||||
### Community 143 - "SysEthnic"
|
||||
Cohesion: 0.34
|
||||
Nodes (13): create_contact(), get_contact(), _parse_optional_date(), date, Session, set_primary_contact(), soft_delete_contact(), total_pages() (+5 more)
|
||||
Cohesion: 0.15
|
||||
Nodes (31): approve_task(), _assert_wf_instance_in_scope(), _business_type_label(), create_process_def(), _enrich_task_list(), get_business_all_tasks(), _get_business_form_fields(), get_designer_data() (+23 more)
|
||||
|
||||
### Community 144 - "dict_data.py"
|
||||
Cohesion: 0.27
|
||||
@@ -670,72 +685,60 @@ Cohesion: 0.51
|
||||
Nodes (10): apply_site_feedback(), OrderItem, _header(), _item(), test_apply_site_feedback_does_not_reset_first_submitted_at(), test_apply_site_feedback_keeps_in_progress_when_requested(), test_apply_site_feedback_rejects_already_voided(), test_apply_site_feedback_rejects_invalid_status() (+2 more)
|
||||
|
||||
### Community 146 - "__init__.py"
|
||||
Cohesion: 0.20
|
||||
Nodes (12): 流程实例 wf_process_instance, WfProcessInstance, _dispatch_local(), Any, 工作流回调服务 —— 流程状态变更时通知业务系统, 注册本地回调函数。当 callback_url 以 path 开头时直接调用 fn。, 尝试本地直调。返回 True 表示已处理,False 表示无匹配的本地回调。, 流程实例状态变更后,向业务系统发送回调通知。 优先级: 1. 如果 callback_url 是内部路径(以 / 开头),优先尝试本地直调 (+4 more)
|
||||
Cohesion: 0.67
|
||||
Nodes (3): 注册本地回调函数。当 callback_url 以 path 开头时直接调用 fn。, register_local_callback(), _LocalCallbackFn
|
||||
|
||||
### Community 147 - "Community 147"
|
||||
Cohesion: 0.37
|
||||
Nodes (17): check_env(), check_python(), create_venv(), init_env(), install_dependencies(), log_error(), log_info(), log_success() (+9 more)
|
||||
|
||||
### Community 150 - "apply_site_feedback"
|
||||
Cohesion: 0.27
|
||||
Nodes (10): apply_product_snapshot_to_plan(), ProductSnapshot, Session, 地磅模块 — CRM 产品解析与快照写入。, 校验 CRM 产品并返回名称/规格快照。新建单据 product_id 必填。 require_on_shelf=False 用于运输计划联动地磅等历, resolve_crm_product(), _product(), resolve_crm_product:运输联动场景允许已下架产品。 (+2 more)
|
||||
|
||||
### Community 152 - "response.py"
|
||||
Cohesion: 0.23
|
||||
Nodes (10): check_customer_protect_time(), pool_recovery_job(), Session, 每天凌晨1点:检查保护期到期客户,跟进次数不足则退回公海, 每天早上9点:保护期到期提醒(3天内到期且跟进不足), _recycle_to_pool(), remind_customer_recycle(), warn_customer_recycle() (+2 more)
|
||||
Cohesion: 0.29
|
||||
Nodes (9): ProductSnapshot, Session, 地磅模块 — CRM 产品解析与快照写入。, 校验 CRM 产品并返回名称/规格快照。新建单据 product_id 必填。 require_on_shelf=False 用于运输计划联动地磅等历, resolve_crm_product(), _product(), resolve_crm_product:运输联动场景允许已下架产品。, test_resolve_allows_off_shelf_when_not_required() (+1 more)
|
||||
|
||||
### Community 154 - "response.py"
|
||||
Cohesion: 0.24
|
||||
Nodes (5): MinIOStorage, Any, 从 MINIO_BASE_URL 解析公网 endpoint;未配置或与内网相同时复用 self.client。, MinIO 预签名下载 URL,携带原始文件名。, MinIO 预签名内联预览 URL(图片新标签页 / PDF iframe 等)。 上传时 put_object 未写 content_typ
|
||||
|
||||
### Community 156 - "utils.py"
|
||||
Cohesion: 0.20
|
||||
Nodes (16): get_pool_record(), list_pool_records(), Any, Session, _row_to_dict(), total_pages(), create_oss(), get_oss_by_id() (+8 more)
|
||||
Cohesion: 0.19
|
||||
Nodes (7): _guess_preview_content_type(), MinIOStorage, Any, 从 MINIO_BASE_URL 解析公网 endpoint;未配置或与内网相同时复用 self.client。, 按对象键后缀推断内联预览 Content-Type(纠正上传时未写入 MIME 的情况)。, MinIO 预签名下载 URL,携带原始文件名。, MinIO 预签名内联预览 URL(图片新标签页 / PDF iframe 等)。 上传时 put_object 未写 content_typ
|
||||
|
||||
### Community 159 - "customer_update"
|
||||
Cohesion: 0.08
|
||||
Nodes (47): 报销模块认证适配:门户 JWT 用户 → bx 业务上下文。, 菜单 code 权限守卫(V0.3:通过 sys_user_role → sys_role_menu → sys_menu.perms), BxReimburseInvoice, BxWorkflowNode, 报销(bx)模块数据模型 — 对齐 docs/sql/20260606_bx_tables.sql, filter_current_cycle_approvals(), 审批轮次过滤 — 驳回后重新提交时,进度接口仅展示当前轮次。, 审批进度仅展示当前轮次;完整历史由 get_approval_history 返回。 (+39 more)
|
||||
Cohesion: 0.06
|
||||
Nodes (50): bx_health(), 报销(bx)API 路由聚合 — REST 风格,协议对齐 bx 原系统。, P0 健康检查:验证 bx 路由已挂载且响应协议为 code=0。, get_menu_list(), 运营端菜单接口(与 web-admin 前端静态路由 sqxp.ts 对齐,14 个一级菜单), Session, recognize_cert(), RecognizeCertBody (+42 more)
|
||||
|
||||
### Community 160 - "register_local_callback"
|
||||
Cohesion: 0.06
|
||||
Nodes (49): category_add(), category_delete(), category_tree(), product_add(), product_delete(), product_detail(), product_options(), product_update() (+41 more)
|
||||
Cohesion: 0.16
|
||||
Nodes (17): API 路由包 — 山峡化工生产一体化门户, create_leave(), delete_leave(), get_leave(), _leave_local_callback(), leave_workflow_callback(), list_leaves(), Session (+9 more)
|
||||
|
||||
### Community 161 - "backfill_crm_contract_attachment_urls.py"
|
||||
Cohesion: 0.29
|
||||
Nodes (10): normalize_attachment_csv(), extract_minio_object_key(), join_relative_urls(), MinIO 附件 URL 规范化(数据迁移共用)。 背景 ---- CRM / 地磅 / 报销与 shanxia2.0 共用同一 MinIO(bucket 默, 业务表 OSS ID 串 → 相对 URL 串(用于客户证照、跟进附件等)。, 去掉预签名 query(``?X-Amz-...``)。, 将绝对/相对 URL 规范为 MinIO object key(不含 bucket)。, resolve_oss_id_csv() (+2 more)
|
||||
Cohesion: 0.24
|
||||
Nodes (11): extract_minio_object_key(), join_relative_urls(), normalize_oss_storage_url(), MinIO 附件 URL 规范化(数据迁移共用)。 背景 ---- CRM / 地磅 / 报销与 shanxia2.0 共用同一 MinIO(bucket 默, 业务表 OSS ID 串 → 相对 URL 串(用于客户证照、跟进附件等)。, 去掉预签名 query(``?X-Amz-...``)。, 将绝对/相对 URL 规范为 MinIO object key(不含 bucket)。, 迁移 CRM ``sys_oss`` 行:``url`` + ``file_name`` → 门户相对路径。 (+3 more)
|
||||
|
||||
### Community 162 - ".to_tenant_user_dict"
|
||||
Cohesion: 0.42
|
||||
Nodes (7): default_settlement_clause(), test_advance_payment_clause(), test_arrive_payment_clause(), test_empty_falls_back_to_arrive(), test_monthly_settlement_clause_uses_label(), test_unknown_falls_back_to_arrive(), test_volume_quarterly_other_labels()
|
||||
|
||||
### Community 163 - "__init__.py"
|
||||
Cohesion: 0.13
|
||||
Nodes (22): bind_biz(), delete_attachment(), download_file(), list_by_biz(), _parse_id_list(), preview_attachment(), Session, UploadFile (+14 more)
|
||||
|
||||
### Community 164 - "get_client_ip"
|
||||
Cohesion: 0.10
|
||||
Nodes (47): _avatar_url_field(), _build_user_info_payload(), change_current_user_password(), get_current_user_profile(), get_web_auth_menu_codes(), _jwt_user_payload(), logout(), public_quote_share() (+39 more)
|
||||
Cohesion: 0.16
|
||||
Nodes (20): logout(), 登出:吊销 Redis 会话(若已配置 REDIS_HOST)。无 Token 或已失效时仍返回成功(幂等)。, decode_token(), extend_session(), get_client_ip(), get_current_user(), get_current_user_by_token(), get_current_user_simple() (+12 more)
|
||||
|
||||
### Community 170 - "test_quote_approver_setting.py"
|
||||
Cohesion: 0.22
|
||||
Nodes (3): BaseStorage, 带下载文件名的访问 URL;默认实现为普通访问地址。, 浏览器内联预览 URL;默认实现为普通访问地址。
|
||||
|
||||
### Community 171 - "minio_url.py"
|
||||
Cohesion: 0.31
|
||||
Nodes (14): create_pool_rule(), delete_pool_rules(), execute_pool_rules(), _execute_single_rule(), get_pool_rule(), list_pool_rules(), Any, Session (+6 more)
|
||||
Cohesion: 0.11
|
||||
Nodes (31): get_pool_record(), list_pool_records(), Any, Session, _row_to_dict(), total_pages(), create_pool_rule(), delete_pool_rules() (+23 more)
|
||||
|
||||
### Community 172 - "apply_pl_scope"
|
||||
Cohesion: 0.12
|
||||
Nodes (15): _db_pool_keepalive_loop(), _error_json(), lifespan(), Exception, Request, 数据库异常:返回 JSON 并保留 CORS 头,避免浏览器只报跨域错误, 未捕获异常:统一 JSON 500,确保跨域场景下仍有 Access-Control-Allow-Origin, 定期 ping MySQL,避免远程库/NAT 回收空闲 TCP 后首请求长时间卡住。 (+7 more)
|
||||
Cohesion: 0.10
|
||||
Nodes (20): list_public_vehicle_plates(), Session, 地磅公共只读:免登车牌 options(现场填写页), 免登:按出入库/计划解析 tenant_no,返回 pl_vehicle 车牌(self + carrier)。, _resolve_tenant_no(), _db_pool_keepalive_loop(), _error_json(), lifespan() (+12 more)
|
||||
|
||||
### Community 173 - "resolve_create_time_bounds"
|
||||
Cohesion: 0.05
|
||||
Nodes (77): _build_page(), _get_by_token(), _make_handlers(), _models(), Session, 运输计划 · 入库/出库单现场免登录公共 API, SoftDeleteModel, BizAttachment (+69 more)
|
||||
Cohesion: 0.04
|
||||
Nodes (83): _build_page(), _get_by_token(), _make_handlers(), _models(), Session, 运输计划 · 入库/出库单现场免登录公共 API, SoftDeleteModel, CrmProduct (+75 more)
|
||||
|
||||
### Community 174 - "register_local_callback"
|
||||
Cohesion: 0.06
|
||||
Nodes (60): 返回 (作废, 归档)。现场链接实际走入库/出库单公共页。, _resolve_io_order_link_flags(), _build_public_page(), _build_site_product_read(), _format_file_size(), get_public_site(), _get_site_link_by_token(), _parse_feedback_images() (+52 more)
|
||||
Cohesion: 0.08
|
||||
Nodes (49): 返回 (作废, 归档)。现场链接实际走入库/出库单公共页。, _resolve_io_order_link_flags(), _build_public_page(), _build_site_product_read(), _format_file_size(), get_public_site(), _get_site_link_by_token(), _parse_feedback_images() (+41 more)
|
||||
|
||||
### Community 179 - "test_pl_vehicle_personnel_photo_schema.py"
|
||||
Cohesion: 0.27
|
||||
@@ -743,11 +746,7 @@ Nodes (16): add_favorite(), count_active(), find_active(), find_soft_deleted(),
|
||||
|
||||
### Community 180 - "OSSStorage"
|
||||
Cohesion: 0.06
|
||||
Nodes (82): archive_plan(), cancel_plan(), cancel_plan_wait_notice(), count_plans_by_contract(), create_from_contract(), create_plan(), create_plan_full(), delete_plan() (+74 more)
|
||||
|
||||
### Community 181 - "allocate_next_biz_no"
|
||||
Cohesion: 0.18
|
||||
Nodes (13): list_customer_options(), 客户下拉选项:租户内只读搜索,不做 owner 数据权限(供地磅/合同等主数据选用)。, list_products(), get_supplier_hazard_flags(), product_allowed(), 产品/客户列表 keyword:名称/编号/拼音首字母 OR 匹配。, test_list_customer_options_keyword_includes_name_and_pinyin(), test_list_customers_keyword_includes_name_and_pinyin() (+5 more)
|
||||
Nodes (70): archive_plan(), cancel_plan(), cancel_plan_wait_notice(), count_plans_by_contract(), create_from_contract(), create_plan(), create_plan_full(), _determine_plan_type() (+62 more)
|
||||
|
||||
### Community 182 - "statistics.py"
|
||||
Cohesion: 0.45
|
||||
@@ -758,32 +757,20 @@ Cohesion: 0.67
|
||||
Nodes (3): parse_optional_plan_datetime(), datetime, 运输计划相关日期时间解析:前端 date-picker 仅传 YYYY-MM-DD。
|
||||
|
||||
### Community 184 - "excel_export.py"
|
||||
Cohesion: 0.24
|
||||
Nodes (11): CRM 模块 Excel 导出列定义(字段与字典类型对齐 Java *Vo @ExcelDictFormat)。, _build_workbook(), DictLabelResolver, export_rows_to_xlsx_response(), ExportColumn, Any, Excel 导出工具:集中处理字典 code → label(对齐 Java ExcelDictConvert / ExcelDictPreloadConver, CRM 列表导出统一入口:表头 + 行数据 + 字典 label 转换 + 文件流响应。 (+3 more)
|
||||
Cohesion: 0.19
|
||||
Nodes (15): CRM 模块 Excel 导出列定义(字段与字典类型对齐 Java *Vo @ExcelDictFormat)。, _build_workbook(), DictLabelResolver, export_rows_to_xlsx_response(), ExportColumn, load_dict_label_maps(), Any, Session (+7 more)
|
||||
|
||||
### Community 187 - "price_letter_detail"
|
||||
Cohesion: 0.18
|
||||
Nodes (19): _avatar_url_field(), _build_user_info_payload(), _jwt_user_payload(), 公共接口(Web 登录、刷新 Token、登出;用户模型对齐 docs/sql/shanxia2.0.sql 基线), 与 web-admin 静态路由 meta.roles(admin/super_admin)对齐的单元素数组。, 与 web-admin `userInfo` 字段约定一致。, Web 端登录用 RSA 公钥(PKCS#1 v1.5,与 JSEncrypt 一致), Web 端登录:账号为 sys_user.user_no,密码为 sys_user.password(bcrypt)。 (+11 more)
|
||||
|
||||
### Community 188 - "get_client_ip"
|
||||
Cohesion: 0.24
|
||||
Nodes (12): get_last_month_range(), month_range(), date, Session, 地磅计划按月自动生成 — 迁移自 simple-dibang-xiaoshou/auto-create-plan/auto_create_monthly_pla, 按月自动生成采购计划。 - 默认仅在每月 1 日执行(force=True 时跳过该限制,供手动触发验收) - 遍历所有租户,按上月计划模板生, run_auto_generate_monthly_plans(), default_execution_status() (+4 more)
|
||||
Cohesion: 0.15
|
||||
Nodes (16): format_datetime(), get_current_time(), parse_datetime(), datetime, get_last_month_range(), month_range(), date, Session (+8 more)
|
||||
|
||||
### Community 193 - "storage.py"
|
||||
Cohesion: 0.04
|
||||
Nodes (83): category_update(), clue_import_template(), contact_by_customer(), customer_create(), customer_import_template(), customer_update(), opportunity_add(), opportunity_assign() (+75 more)
|
||||
|
||||
### Community 194 - "get_current_user_by_token"
|
||||
Cohesion: 0.40
|
||||
Nodes (12): create_follow(), _enrich_follow_dict(), get_follow(), list_follows(), list_follows_by_customer(), Any, Session, _row_to_dict() (+4 more)
|
||||
|
||||
### Community 195 - "PlFreightSettlementTransport"
|
||||
Cohesion: 0.40
|
||||
Nodes (3): Any, 供 get_tenant_no 等门户工具使用。, 与门户 JWT / apply_data_scope 所需的 user_info 字段对齐。
|
||||
|
||||
### Community 196 - "crm_contract_sub_routes.py"
|
||||
Cohesion: 0.20
|
||||
Nodes (4): _guess_preview_content_type(), OSSStorage, 文件存储工具模块 支持多种存储方式: 1. 本地存储 (local) - 默认 2. 七牛云存储 (qiniu) - 推荐生产环境使用 3. 阿里云OSS (, 按对象键后缀推断内联预览 Content-Type(纠正上传时未写入 MIME 的情况)。
|
||||
|
||||
### Community 199 - "price_letter_detail"
|
||||
Cohesion: 0.17
|
||||
Nodes (11): price_letter_add(), price_letter_detail(), price_letter_list(), price_letter_update(), price_letter_void(), PriceLetterCreateBody, PriceLetterDetailBody, PriceLetterListBody (+3 more)
|
||||
Nodes (11): pool_customer_assign(), pool_customer_claim(), pool_customer_list(), pool_record_detail(), pool_record_list(), PoolAssignBody, PoolClaimBody, PoolCustomerListBody (+3 more)
|
||||
|
||||
### Community 200 - "assert_contract_editable_for_update"
|
||||
Cohesion: 0.60
|
||||
@@ -798,8 +785,8 @@ Cohesion: 0.18
|
||||
Nodes (10): 1. 创建虚拟环境, 2. 安装依赖, 3. 配置环境变量, 4. 启动服务, API 路由分组, shanxia2.0 后端骨架(FastAPI), 开发规范, 快速开始 (+2 more)
|
||||
|
||||
### Community 220 - "api_crypto_middleware.py"
|
||||
Cohesion: 0.12
|
||||
Nodes (23): ApiCryptoMiddleware, _claims_from_auth(), _coerce_post_method_override_to_get(), _get_redis_client(), _is_crypto_api(), _json_error_response(), _mark_nonce_once(), _nonce_key() (+15 more)
|
||||
Cohesion: 0.24
|
||||
Nodes (13): _claims_from_auth(), _coerce_post_method_override_to_get(), _get_redis_client(), _is_crypto_api(), _json_error_response(), _mark_nonce_once(), _nonce_key(), Redis (+5 more)
|
||||
|
||||
### Community 237 - "order_status_compat.py"
|
||||
Cohesion: 0.36
|
||||
@@ -816,22 +803,22 @@ Nodes (6): eq_unicode(), like_unicode(), MySQL 字符串比较/模糊匹配时
|
||||
## Knowledge Gaps
|
||||
- **30 isolated node(s):** `Config`, `Config`, `Config`, `Config`, `Config` (+25 more)
|
||||
These have ≤1 connection - possible missing edges or undocumented components.
|
||||
- **18 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
|
||||
- **15 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
|
||||
|
||||
## Suggested Questions
|
||||
_Questions this graph is uniquely positioned to answer:_
|
||||
|
||||
- **Why does `generate_uuid()` connect `Community 25` to `Community 0`, `plan.py`, `Community 4`, `Community 5`, `Community 6`, `Community 134`, `Community 8`, `statistics.py`, `Community 136`, `Community 11`, `SysEthnic`, `dict_data.py`, `Community 15`, `Community 18`, `Community 16`, `Community 21`, `Community 24`, `response.py`, `utils.py`, `Community 29`, `Community 28`, `customer_update`, `Community 33`, `get_client_ip`, `Community 36`, `Community 39`, `minio_url.py`, `Community 45`, `resolve_create_time_bounds`, `OSSStorage`, `Community 53`, `Community 58`, `get_client_ip`, `Community 61`, `Community 62`, `Community 63`, `Community 64`, `Community 65`, `get_current_user_by_token`, `Community 66`, `Community 68`, `Community 67`, `Community 71`, `Community 72`, `Community 83`, `Community 86`, `Community 87`, `Any`, `pl_ocr_service.py`, `pl_vehicle_recharge_service.py`, `Community 92`, `_invoice_info_by_customer_impl`, `Community 95`, `apply_crm_create_by_scope`, `apply_pl_scope`, `Community 109`, `quote_share_log.py`, `db_models.py`, `site.py`, `Community 116`, `Community 118`, `Community 122`, `customer_pool.py`, `Community 127`?**
|
||||
_High betweenness centrality (0.095) - this node is a cross-community bridge._
|
||||
- **Why does `BaseModel` connect `Community 12` to `Community 0`, `Community 1`, `Community 2`, `datetime_parse.py`, `crm_supplier_hazard.py`, `Community 5`, `LocalStorage`, `Community 7`, `unsettled_plans_q_filter`, `Community 11`, `PlTransport`, `Community 13`, `Community 15`, `Community 16`, `Community 18`, `Community 19`, `Community 24`, `Community 26`, `Community 27`, `Community 28`, `register_local_callback`, `Community 33`, `Community 34`, `__init__.py`, `Community 38`, `Community 43`, `Community 45`, `resolve_create_time_bounds`, `Community 48`, `Community 50`, `Community 51`, `OSSStorage`, `Community 54`, `Community 58`, `Community 60`, `Community 62`, `Community 64`, `storage.py`, `Community 66`, `Community 65`, `price_letter_detail`, `Community 72`, `Community 71`, `Community 74`, `Community 106`, `Community 109`, `Community 111`, `site.py`, `Community 119`?**
|
||||
_High betweenness centrality (0.081) - this node is a cross-community bridge._
|
||||
- **Why does `Settings` connect `Community 46` to `_ensure_subpack_slip_no`, `Community 35`, `Community 67`, `Community 37`, `price_letter_detail`, `Community 73`, `Community 42`, `Community 44`, `Community 52`, `Community 59`, `api_crypto_middleware.py`, `Community 30`?**
|
||||
_High betweenness centrality (0.073) - this node is a cross-community bridge._
|
||||
- **Why does `generate_uuid()` connect `Community 25` to `Community 0`, `Community 1`, `plan.py`, `Community 4`, `Community 5`, `Community 6`, `Community 134`, `Community 8`, `statistics.py`, `Community 136`, `Community 11`, `Community 15`, `dict_data.py`, `Community 17`, `Community 16`, `Community 21`, `Community 24`, `Community 28`, `Community 29`, `customer_update`, `Community 32`, `Community 34`, `Community 36`, `Community 38`, `Community 39`, `minio_url.py`, `Community 43`, `Community 45`, `resolve_create_time_bounds`, `OSSStorage`, `Community 53`, `Community 58`, `price_letter_detail`, `get_client_ip`, `Community 61`, `Community 62`, `Community 64`, `Community 65`, `Community 66`, `Community 67`, `Community 68`, `Community 71`, `Community 83`, `Community 86`, `Community 87`, `pl_ocr_service.py`, `pl_vehicle_recharge_service.py`, `Community 92`, `_invoice_info_by_customer_impl`, `Community 95`, `apply_crm_create_by_scope`, `apply_pl_scope`, `Community 109`, `quote_share_log.py`, `db_models.py`, `site.py`, `Community 116`, `Community 118`, `Community 122`, `customer_pool.py`?**
|
||||
_High betweenness centrality (0.134) - this node is a cross-community bridge._
|
||||
- **Why does `get_tenant_no()` connect `Community 7` to `Community 0`, `Community 1`, `Community 2`, `crm_supplier_hazard.py`, `Community 5`, `unsettled_plans_q_filter`, `Community 8`, `Community 10`, `Community 11`, `Community 12`, `Community 13`, `Community 15`, `Community 17`, `Community 18`, `Community 19`, `Community 24`, `Community 27`, `Community 29`, `customer_update`, `Community 38`, `Community 41`, `Community 43`, `Community 45`, `Community 47`, `Community 49`, `Community 51`, `OSSStorage`, `Community 54`, `Community 58`, `Community 60`, `Community 62`, `Community 64`, `storage.py`, `Community 66`, `Community 65`, `Community 71`, `Community 74`, `Community 78`, `Community 109`, `site.py`?**
|
||||
_High betweenness centrality (0.097) - this node is a cross-community bridge._
|
||||
- **Why does `BaseModel` connect `Community 12` to `Community 0`, `Community 1`, `Community 2`, `Community 3`, `crm_supplier_hazard.py`, `datetime_parse.py`, `Community 5`, `Community 7`, `unsettled_plans_q_filter`, `Community 10`, `Community 11`, `PlTransport`, `Community 13`, `Community 15`, `Community 16`, `Community 18`, `Community 19`, `Community 20`, `Community 24`, `Community 27`, `customer_update`, `register_local_callback`, `Community 41`, `Community 43`, `Community 45`, `resolve_create_time_bounds`, `Community 47`, `Community 51`, `OSSStorage`, `Community 54`, `Community 56`, `Community 57`, `Community 58`, `Community 60`, `Community 62`, `Community 64`, `storage.py`, `Community 65`, `Community 71`, `Community 109`, `Community 111`, `site.py`, `Community 119`?**
|
||||
_High betweenness centrality (0.089) - this node is a cross-community bridge._
|
||||
- **Are the 8 inferred relationships involving `DbUserContext` (e.g. with `InventoryRelatedData` and `AutoGenerateMonthlyResult`) actually correct?**
|
||||
_`DbUserContext` has 8 INFERRED edges - model-reasoned connections that need verification._
|
||||
- **What connects `API 路由包 — 山峡化工生产一体化门户`, `返回运营端角色下拉选项(label=库中角色名,value=Web 角色码)。`, `返回用户下拉选项(label=昵称,value=用户ID,附带 nickName/userNo)。 供申请人/审批人等选择场景使用,返回本租户全部有效` to the rest of the system?**
|
||||
_558 weakly-connected nodes found - possible documentation gaps or missing edges._
|
||||
_560 weakly-connected nodes found - possible documentation gaps or missing edges._
|
||||
- **Should `Community 0` be split into smaller, more focused modules?**
|
||||
_Cohesion score 0.10638297872340426 - nodes in this community are weakly interconnected._
|
||||
_Cohesion score 0.14082503556187767 - nodes in this community are weakly interconnected._
|
||||
- **Should `Community 1` be split into smaller, more focused modules?**
|
||||
_Cohesion score 0.07308970099667775 - nodes in this community are weakly interconnected._
|
||||
_Cohesion score 0.0636523266022827 - nodes in this community are weakly interconnected._
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_bx_reimburse_py", "label": "reimburse.py", "file_type": "code", "source_file": "app/api/bx/reimburse.py", "source_location": "L1"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_bx_reimburse_tenant", "label": "_tenant()", "file_type": "code", "source_file": "app/api/bx/reimburse.py", "source_location": "L41"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/api/bx/reimburse.py"}, {"id": "bxusercontext", "label": "BxUserContext", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/api/bx/reimburse.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_bx_reimburse_build_details_data", "label": "_build_details_data()", "file_type": "code", "source_file": "app/api/bx/reimburse.py", "source_location": "L45"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_bx_reimburse_get_generated_title", "label": "get_generated_title()", "file_type": "code", "source_file": "app/api/bx/reimburse.py", "source_location": "L82"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_bx_reimburse_download_reimburse_import_template", "label": "download_reimburse_import_template()", "file_type": "code", "source_file": "app/api/bx/reimburse.py", "source_location": "L99"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_bx_reimburse_import_reimburses", "label": "import_reimburses()", "file_type": "code", "source_file": "app/api/bx/reimburse.py", "source_location": "L131"}, {"id": "uploadfile", "label": "UploadFile", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/api/bx/reimburse.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_bx_reimburse_list_reimburses", "label": "list_reimburses()", "file_type": "code", "source_file": "app/api/bx/reimburse.py", "source_location": "L147"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_bx_reimburse_my_reimburses", "label": "my_reimburses()", "file_type": "code", "source_file": "app/api/bx/reimburse.py", "source_location": "L193"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_bx_reimburse_my_reimburse_stats", "label": "my_reimburse_stats()", "file_type": "code", "source_file": "app/api/bx/reimburse.py", "source_location": "L238"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_bx_reimburse_reimburse_status_counts", "label": "reimburse_status_counts()", "file_type": "code", "source_file": "app/api/bx/reimburse.py", "source_location": "L248"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_bx_reimburse_pending_approval", "label": "pending_approval()", "file_type": "code", "source_file": "app/api/bx/reimburse.py", "source_location": "L257"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_bx_reimburse_pending_approval_stats", "label": "pending_approval_stats()", "file_type": "code", "source_file": "app/api/bx/reimburse.py", "source_location": "L289"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_bx_reimburse_approved_by_me", "label": "approved_by_me()", "file_type": "code", "source_file": "app/api/bx/reimburse.py", "source_location": "L299"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_bx_reimburse_get_reimburse", "label": "get_reimburse()", "file_type": "code", "source_file": "app/api/bx/reimburse.py", "source_location": "L331"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_bx_reimburse_get_approvals", "label": "get_approvals()", "file_type": "code", "source_file": "app/api/bx/reimburse.py", "source_location": "L345"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_bx_reimburse_get_approval_history", "label": "get_approval_history()", "file_type": "code", "source_file": "app/api/bx/reimburse.py", "source_location": "L358"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_bx_reimburse_create_reimburse", "label": "create_reimburse()", "file_type": "code", "source_file": "app/api/bx/reimburse.py", "source_location": "L371"}, {"id": "reimburseapplycreate", "label": "ReimburseApplyCreate", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/api/bx/reimburse.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_bx_reimburse_update_reimburse", "label": "update_reimburse()", "file_type": "code", "source_file": "app/api/bx/reimburse.py", "source_location": "L425"}, {"id": "reimburseapplyupdate", "label": "ReimburseApplyUpdate", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/api/bx/reimburse.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_appLine truncated
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_bx_reimburse_import_py", "label": "test_bx_reimburse_import.py", "file_type": "code", "source_file": "tests/test_bx_reimburse_import.py", "source_location": "L1"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_bx_reimburse_import_test_build_expense_path_labels_unique_and_enabled_only", "label": "test_build_expense_path_labels_unique_and_enabled_only()", "file_type": "code", "source_file": "tests/test_bx_reimburse_import.py", "source_location": "L20"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_bx_reimburse_import_test_build_expense_path_labels_disambiguate_collision", "label": "test_build_expense_path_labels_disambiguate_collision()", "file_type": "code", "source_file": "tests/test_bx_reimburse_import.py", "source_location": "L34"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_bx_reimburse_import_test_build_applicant_label", "label": "test_build_applicant_label()", "file_type": "code", "source_file": "tests/test_bx_reimburse_import.py", "source_location": "L44"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_bx_reimburse_import_test_is_example_row", "label": "test_is_example_row()", "file_type": "code", "source_file": "tests/test_bx_reimburse_import.py", "source_location": "L49"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_bx_reimburse_import_test_partition_empty_group_is_solo", "label": "test_partition_empty_group_is_solo()", "file_type": "code", "source_file": "tests/test_bx_reimburse_import.py", "source_location": "L54"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_bx_reimburse_import_test_validate_group_headers_mismatch", "label": "test_validate_group_headers_mismatch()", "file_type": "code", "source_file": "tests/test_bx_reimburse_import.py", "source_location": "L68"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_bx_reimburse_import_test_validate_group_headers_title_partial_empty", "label": "test_validate_group_headers_title_partial_empty()", "file_type": "code", "source_file": "tests/test_bx_reimburse_import.py", "source_location": "L77"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_bx_reimburse_import_test_write_option_sheet_and_validation", "label": "test_write_option_sheet_and_validation()", "file_type": "code", "source_file": "tests/test_bx_reimburse_import.py", "source_location": "L86"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_bx_reimburse_import_test_build_import_template_has_guide_examples_and_dv", "label": "test_build_import_template_has_guide_examples_and_dv()", "file_type": "code", "source_file": "tests/test_bx_reimburse_import.py", "source_location": "L104"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_bx_reimburse_import_test_iter_data_rows_skips_examples", "label": "test_iter_data_rows_skips_examples()", "file_type": "code", "source_file": "tests/test_bx_reimburse_import.py", "source_location": "L124"}], "edges": [{"source": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_bx_reimburse_import_py", "target": "datetime", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "tests/test_bx_reimburse_import.py", "source_location": "L2", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_bx_reimburse_import_py", "target": "io", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "tests/test_bx_reimburse_import.py", "source_location": "L3", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_bx_reimburse_import_py", "target": "openpyxl", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "tests/test_bx_reimburse_import.py", "source_location": "L5", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_bx_reimburse_import_py", "target": "app_services_bx_reimburse_import", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "tests/test_bx_reimburse_import.py", "source_location": "L7", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_bx_reimburse_import_py", "target": "app_utils_excel_list_validation", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "tests/test_bx_reimburse_import.py", "source_location": "L17", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_bx_reimburse_import_py", "target": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_bx_reimburse_import_test_build_expense_path_labels_unique_and_enabled_only", "relation": "contains", "confidence": "EXTRACTED", "source_file": "tests/test_bx_reimburse_import.py", "source_location": "L20", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_bx_reimburLine truncated
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_bx_reimburse_import_py", "label": "reimburse_import.py", "file_type": "code", "source_file": "app/services/bx/reimburse_import.py", "source_location": "L1"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_bx_reimburse_import_build_applicant_label", "label": "build_applicant_label()", "file_type": "code", "source_file": "app/services/bx/reimburse_import.py", "source_location": "L57"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_bx_reimburse_import_build_expense_path_labels", "label": "build_expense_path_labels()", "file_type": "code", "source_file": "app/services/bx/reimburse_import.py", "source_location": "L63"}, {"id": "any", "label": "Any", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/bx/reimburse_import.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_bx_reimburse_import_is_example_row", "label": "is_example_row()", "file_type": "code", "source_file": "app/services/bx/reimburse_import.py", "source_location": "L96"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_bx_reimburse_import_partition_import_groups", "label": "partition_import_groups()", "file_type": "code", "source_file": "app/services/bx/reimburse_import.py", "source_location": "L101"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_bx_reimburse_import_validate_group_headers", "label": "validate_group_headers()", "file_type": "code", "source_file": "app/services/bx/reimburse_import.py", "source_location": "L117"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_bx_reimburse_import_cell_str", "label": "cell_str()", "file_type": "code", "source_file": "app/services/bx/reimburse_import.py", "source_location": "L135"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_bx_reimburse_import_parse_amount", "label": "parse_amount()", "file_type": "code", "source_file": "app/services/bx/reimburse_import.py", "source_location": "L145"}, {"id": "decimal", "label": "Decimal", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/bx/reimburse_import.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_bx_reimburse_import_load_expense_nodes_flat", "label": "load_expense_nodes_flat()", "file_type": "code", "source_file": "app/services/bx/reimburse_import.py", "source_location": "L157"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/bx/reimburse_import.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_bx_reimburse_import_load_applicant_options", "label": "load_applicant_options()", "file_type": "code", "source_file": "app/services/bx/reimburse_import.py", "source_location": "L176"}, {"id": "bxusercontext", "label": "BxUserContext", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/bx/reimburse_import.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_bx_reimburse_import_build_import_template_bytes", "label": "build_import_template_bytes()", "file_type": "code", "source_file": "app/services/bx/reimburse_import.py", "source_location": "L193"}, {"id": "date", "label": "date", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/bx/reimburse_import.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_bx_reimburse_import_iter_data_rows", "label": "iter_data_rows()", "file_type": "code", "source_file": "app/services/bx/reimburse_import.py", "source_location": "L270"}, {"id": "worksheet", "label": "Worksheet", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/bx/reimburse_import.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_bx_reimburse_import_import_reimburse_workbook", "label": "import_reimburse_workbook()", "file_type": "code", "source_file": "app/services/bx/reimburse_import.py", "source_location": "L300"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_bx_reimburse_import_rationale_64", "label": "\u8fd4\u56de {id: \u5c55\u793a\u6587\u6848}\uff0c\u4ec5 status==1\uff1b\u51b2\u7a81\u65f6\u8ffd\u52a0 \uff08\u7f16\u7801:xxx\uff09\u3002", "file_type": "rationale", "source_file": "app/services/bx/reimburse_import.py", "source_location": "L64"}], "edges": [{"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_bx_reimburse_import_py", "target": "datetime", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": Line truncated
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_duty_models_py", "label": "duty_models.py", "file_type": "code", "source_file": "app/models/duty_models.py", "source_location": "L1"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_duty_models_dutyday", "label": "DutyDay", "file_type": "code", "source_file": "app/models/duty_models.py", "source_location": "L25"}, {"id": "base", "label": "Base", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_duty_models_dutyslot", "label": "DutySlot", "file_type": "code", "source_file": "app/models/duty_models.py", "source_location": "L45"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_duty_models_dutyassignment", "label": "DutyAssignment", "file_type": "code", "source_file": "app/models/duty_models.py", "source_location": "L65"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_duty_models_rationale_1", "label": "\u503c\u73ed\u6392\u73ed\u6a21\u578b \u2014 docs/superpowers/specs/2026-08-14-duty-schedule-design.md", "file_type": "rationale", "source_file": "app/models/duty_models.py", "source_location": "L1"}], "edges": [{"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_duty_models_py", "target": "datetime", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/models/duty_models.py", "source_location": "L2", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_duty_models_py", "target": "sqlalchemy", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/models/duty_models.py", "source_location": "L4", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_duty_models_py", "target": "app_core_database", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/models/duty_models.py", "source_location": "L6", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_duty_models_py", "target": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_duty_models_dutyday", "relation": "contains", "confidence": "EXTRACTED", "source_file": "app/models/duty_models.py", "source_location": "L25", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_duty_models_dutyday", "target": "base", "relation": "inherits", "confidence": "EXTRACTED", "source_file": "app/models/duty_models.py", "source_location": "L25", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_duty_models_py", "target": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_duty_models_dutyslot", "relation": "contains", "confidence": "EXTRACTED", "source_file": "app/models/duty_models.py", "source_location": "L45", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_duty_models_dutyslot", "target": "base", "relation": "inherits", "confidence": "EXTRACTED", "source_file": "app/models/duty_models.py", "source_location": "L45", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_duty_models_py", "target": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_duty_models_dutyassignment", "relation": "contains", "confidence": "EXTRACTED", "source_file": "app/models/duty_models.py", "source_location": "L65", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_duty_models_dutyassignment", "target": "base", "relation": "inherits", "confidence": "EXTRACTED", "source_file": "app/models/duty_models.py", "source_location": "L65", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_duty_models_rationale_1", "target": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_duty_models_py", "relation": "rationale_for", "confidence": "EXTRACTED", "source_file": "app/models/duty_models.py", "source_location": "L1", "weight": 1.0}], "raw_calls": [], "callable_nids": ["users_wuzilong_work_00code_shanxia2_0_backend_app_models_duty_models_dutyassignment", "users_wuzilong_work_00code_shanxia2_0_backend_app_models_duty_models_dutyday", "users_wuzilong_work_00code_shanxia2_0_backend_app_models_duty_models_dutyslot"]}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_utils_excel_list_validation_py", "label": "excel_list_validation.py", "file_type": "code", "source_file": "app/utils/excel_list_validation.py", "source_location": "L1"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_utils_excel_list_validation_write_option_sheet", "label": "write_option_sheet()", "file_type": "code", "source_file": "app/utils/excel_list_validation.py", "source_location": "L11"}, {"id": "workbook", "label": "Workbook", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/utils/excel_list_validation.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_utils_excel_list_validation_add_list_validation", "label": "add_list_validation()", "file_type": "code", "source_file": "app/utils/excel_list_validation.py", "source_location": "L35"}, {"id": "worksheet", "label": "Worksheet", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/utils/excel_list_validation.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_utils_excel_list_validation_rationale_19", "label": "\u5199\u5165\u9009\u9879\u8868\uff1aA=label\uff0c\u53ef\u9009 B=id\u3002\u8fd4\u56de sheet_title\u3002", "file_type": "rationale", "source_file": "app/utils/excel_list_validation.py", "source_location": "L19"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_utils_excel_list_validation_rationale_45", "label": "option_rows \u4e3a\u9009\u9879\u6570\u636e\u884c\u6570\uff08\u4e0d\u542b\u8868\u5934\uff09\u3002option \u5728 A2:A{1+option_rows}\u3002", "file_type": "rationale", "source_file": "app/utils/excel_list_validation.py", "source_location": "L45"}], "edges": [{"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_utils_excel_list_validation_py", "target": "typing", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/utils/excel_list_validation.py", "source_location": "L3", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_utils_excel_list_validation_py", "target": "openpyxl_utils", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/utils/excel_list_validation.py", "source_location": "L5", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_utils_excel_list_validation_py", "target": "openpyxl_workbook_workbook", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/utils/excel_list_validation.py", "source_location": "L6", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_utils_excel_list_validation_py", "target": "openpyxl_worksheet_datavalidation", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/utils/excel_list_validation.py", "source_location": "L7", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_utils_excel_list_validation_py", "target": "openpyxl_worksheet_worksheet", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/utils/excel_list_validation.py", "source_location": "L8", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_utils_excel_list_validation_py", "target": "users_wuzilong_work_00code_shanxia2_0_backend_app_utils_excel_list_validation_write_option_sheet", "relation": "contains", "confidence": "EXTRACTED", "source_file": "app/utils/excel_list_validation.py", "source_location": "L11", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_utils_excel_list_validation_write_option_sheet", "target": "workbook", "relation": "references", "context": "parameter_type", "confidence": "EXTRACTED", "source_file": "app/utils/excel_list_validation.py", "source_location": "L11", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_utils_excel_list_validation_py", "target": "users_wuzilong_work_00code_shanxia2_0_backend_app_utils_excel_list_validation_add_list_validation", "relation": "contains", "confidence": "EXTRACTED", "source_file": "app/utils/excel_list_validation.py", "source_location": "L35", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_utils_excel_list_validation_add_list_validation", "target": "worksheet", "relation": "references", "context": "parameter_type", "confidence": "EXTRACTED", "source_file": "app/utils/excel_list_validation.py", "source_location": "L35", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_utils_excel_list_validation_rationale_19", "target": "users_wuzilong_work_00code_shanxia2_0_backend_app_utils_excel_list_validation_write_option_sheet", "relation": "rationale_for", "confidence": "EXTRACTED", "source_file": "app/utils/excel_list_validation.py", "source_location": "L19", "Line truncated
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"/Users/zlwu/work/00code/shanxia2.0/backend/start-prod.sh":{"size":7357,"mtime_ns":1782639355476060453,"hash":"ca89869446be74a2bf2367b3b28f448580220859a39a84ad9613ac5fe35a8433"},"/Users/zlwu/work/00code/shanxia2.0/backend/start.sh":{"size":9548,"mtime_ns":1780114661446983244,"hash":"4d632863e8a0cd81194b491d83a0218a4157435ae812dec76a6f6197b4d48e57"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/__init__.py":{"size":21,"mtime_ns":1780113982281973984,"hash":"60a04ff08fd97084e83ea809fd13feda6d2848be20ebf309e31fc1dad7523230"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/main.py":{"size":6280,"mtime_ns":1782790852770448643,"hash":"8f9da799977bee7134d87a26ff8434ac5ef8eabb0b7368dd537eee49ad7fd00b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/__init__.py":{"size":2207,"mtime_ns":1783208067881286091,"hash":"313e6da0e91d743ce891dd283458c79b50b24c4d909dc96ca7b3fd9d2bfa5013"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/auth.py":{"size":7457,"mtime_ns":1782790841500516545,"hash":"34bac56e1c2cb6215f4b5fa2dc32d77d4b4abca51428819eebf4afd269cc51cf"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_crypto.py":{"size":1870,"mtime_ns":1780108930724444498,"hash":"c04b748aea86890a5971f749523c7b17dbad1ddd6f9f7f07a5679d159c984e33"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/login_crypto.py":{"size":3858,"mtime_ns":1780108930725155710,"hash":"b82d51afabf2293a9cb1fb2e5f994c383948e7f47ea95fe674686b4f3f3342b1"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/config.py":{"size":6958,"mtime_ns":1784032424448146538,"hash":"c2fb44aedb880733de28afa7f92d2c1c681eb600f821bbeba2bcda3933e5bc37"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/wechat_pay.py":{"size":23585,"mtime_ns":1780108930725768130,"hash":"3d52265ce65d21e7103f94afa1e8547aee0823b5ea6d65c50317436ac81fb4fc"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_request_log_middleware.py":{"size":11971,"mtime_ns":1782701669198208175,"hash":"0b84c32828939e6102bb3647691fa8f04b1f68e0e0e9f28127374cc44e19a11c"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/database.py":{"size":1510,"mtime_ns":1782790847265601586,"hash":"8bc9c8ae62e2be2c6920036582ba448dfcd1711c41cebb62274ff6bc6ee14b0b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/security.py":{"size":728,"mtime_ns":1780108930725470004,"hash":"df0f01115064f7b8de1c81dbe9a6db39360c236610b68c33b3234a51d7f0025b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/request_payload.py":{"size":2842,"mtime_ns":1780108930725350836,"hash":"e9937151530ab1417ef297aef59111977725cf90be2a8fa76c00a0618789aba0"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/__init__.py":{"size":61,"mtime_ns":1780108930724312414,"hash":"3dd1842ee59b0c804176b03b4b8a323bd4a252b7feb1bbc32db64f3a8928570e"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/response.py":{"size":1824,"mtime_ns":1780108930725412462,"hash":"f1739352d65ea8287d76e282a59553aa8d63c97e35764c64ac378aa5bd9c347e"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/menu_guard.py":{"size":1611,"mtime_ns":1780108930725223002,"hash":"3e26077288c3620a4780038b891a326206b45bffe00aca545bdae30172487235"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/db_collation.py":{"size":548,"mtime_ns":1780108930725025043,"hash":"0c680e25a18244ca839a7de6ed1f4313b33bc8da118d89c463aee1b6af00c6f9"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/context.py":{"size":421,"mtime_ns":1780108930724824458,"hash":"83a10aee67162374993c10a30bf8bb8bd9c4b35cf48f42c832415ccebd91660f"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/utils.py":{"size":1498,"mtime_ns":1780108930725607296,"hash":"09323693661c1d4560458b86a954128ccff82bbaf6522da0b44f24da99ddf9ea"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/storage.py":{"size":14141,"mtime_ns":1782855136810943216,"hash":"3ae2c413c10898a8adb3d6773870bafbfbc18c739c10e0e8b4f765b4b5ca6c08"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/order_status_compat.py":{"size":2828,"mtime_ns":1780108930725284669,"hash":"5349e5a0745ee6734c4a995337779307adefc46f453906171988a77de2081540"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/exceptions.py":{"size":4946,"mtime_ns":1780108930725084335,"hash":"79c997d964bca1a4f12f2b92fb1f181fdb9e2b3cf72eae39f4cc164d23794fd7"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/data_scope.py":{"size":8043,"mtime_ns":1782648925694296304,"hash":"626dff86937e5cacec634ecbc61cd53d3763d760223a1d4ec7cd11f64e59a220"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/admin_request_json.py":{"size":2669,"mtime_ns":1780108930724378914,"hash":"4f68b3cf84291e7bddad6ecf4bb4d29ca5e1ec246a9f7f9c4cf5909d0ec1174a"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_crypto_middleware.py":{"size":10876,"mtime_ns":1782790001926627098,"hash":"b39f128b6612589ea6ac5babd30752e9585b3af07b82bb1ad53c9e3c0d90816f"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/wechat_message.py":{"size":11194,"mtime_ns":1780108930725684255,"hash":"5086070deb14d99d1a69e419b2299ec4Line truncated
|
||||
{"/Users/zlwu/work/00code/shanxia2.0/backend/start-prod.sh":{"size":7357,"mtime_ns":1782639355476060453,"hash":"ca89869446be74a2bf2367b3b28f448580220859a39a84ad9613ac5fe35a8433"},"/Users/zlwu/work/00code/shanxia2.0/backend/start.sh":{"size":9548,"mtime_ns":1780114661446983244,"hash":"4d632863e8a0cd81194b491d83a0218a4157435ae812dec76a6f6197b4d48e57"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/__init__.py":{"size":21,"mtime_ns":1780113982281973984,"hash":"60a04ff08fd97084e83ea809fd13feda6d2848be20ebf309e31fc1dad7523230"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/main.py":{"size":6280,"mtime_ns":1782790852770448643,"hash":"8f9da799977bee7134d87a26ff8434ac5ef8eabb0b7368dd537eee49ad7fd00b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/__init__.py":{"size":2207,"mtime_ns":1783208067881286091,"hash":"313e6da0e91d743ce891dd283458c79b50b24c4d909dc96ca7b3fd9d2bfa5013"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/auth.py":{"size":7457,"mtime_ns":1782790841500516545,"hash":"34bac56e1c2cb6215f4b5fa2dc32d77d4b4abca51428819eebf4afd269cc51cf"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_crypto.py":{"size":1870,"mtime_ns":1780108930724444498,"hash":"c04b748aea86890a5971f749523c7b17dbad1ddd6f9f7f07a5679d159c984e33"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/login_crypto.py":{"size":3858,"mtime_ns":1780108930725155710,"hash":"b82d51afabf2293a9cb1fb2e5f994c383948e7f47ea95fe674686b4f3f3342b1"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/config.py":{"size":6958,"mtime_ns":1784032424448146538,"hash":"c2fb44aedb880733de28afa7f92d2c1c681eb600f821bbeba2bcda3933e5bc37"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/wechat_pay.py":{"size":23585,"mtime_ns":1780108930725768130,"hash":"3d52265ce65d21e7103f94afa1e8547aee0823b5ea6d65c50317436ac81fb4fc"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_request_log_middleware.py":{"size":11971,"mtime_ns":1782701669198208175,"hash":"0b84c32828939e6102bb3647691fa8f04b1f68e0e0e9f28127374cc44e19a11c"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/database.py":{"size":1510,"mtime_ns":1782790847265601586,"hash":"8bc9c8ae62e2be2c6920036582ba448dfcd1711c41cebb62274ff6bc6ee14b0b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/security.py":{"size":728,"mtime_ns":1780108930725470004,"hash":"df0f01115064f7b8de1c81dbe9a6db39360c236610b68c33b3234a51d7f0025b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/request_payload.py":{"size":2842,"mtime_ns":1780108930725350836,"hash":"e9937151530ab1417ef297aef59111977725cf90be2a8fa76c00a0618789aba0"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/__init__.py":{"size":61,"mtime_ns":1780108930724312414,"hash":"3dd1842ee59b0c804176b03b4b8a323bd4a252b7feb1bbc32db64f3a8928570e"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/response.py":{"size":1824,"mtime_ns":1780108930725412462,"hash":"f1739352d65ea8287d76e282a59553aa8d63c97e35764c64ac378aa5bd9c347e"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/menu_guard.py":{"size":1611,"mtime_ns":1780108930725223002,"hash":"3e26077288c3620a4780038b891a326206b45bffe00aca545bdae30172487235"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/db_collation.py":{"size":548,"mtime_ns":1780108930725025043,"hash":"0c680e25a18244ca839a7de6ed1f4313b33bc8da118d89c463aee1b6af00c6f9"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/context.py":{"size":421,"mtime_ns":1780108930724824458,"hash":"83a10aee67162374993c10a30bf8bb8bd9c4b35cf48f42c832415ccebd91660f"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/utils.py":{"size":1498,"mtime_ns":1780108930725607296,"hash":"09323693661c1d4560458b86a954128ccff82bbaf6522da0b44f24da99ddf9ea"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/storage.py":{"size":14141,"mtime_ns":1782855136810943216,"hash":"3ae2c413c10898a8adb3d6773870bafbfbc18c739c10e0e8b4f765b4b5ca6c08"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/order_status_compat.py":{"size":2828,"mtime_ns":1780108930725284669,"hash":"5349e5a0745ee6734c4a995337779307adefc46f453906171988a77de2081540"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/exceptions.py":{"size":4946,"mtime_ns":1780108930725084335,"hash":"79c997d964bca1a4f12f2b92fb1f181fdb9e2b3cf72eae39f4cc164d23794fd7"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/data_scope.py":{"size":8043,"mtime_ns":1782648925694296304,"hash":"626dff86937e5cacec634ecbc61cd53d3763d760223a1d4ec7cd11f64e59a220"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/admin_request_json.py":{"size":2669,"mtime_ns":1780108930724378914,"hash":"4f68b3cf84291e7bddad6ecf4bb4d29ca5e1ec246a9f7f9c4cf5909d0ec1174a"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_crypto_middleware.py":{"size":10876,"mtime_ns":1782790001926627098,"hash":"b39f128b6612589ea6ac5babd30752e9585b3af07b82bb1ad53c9e3c0d90816f"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/wechat_message.py":{"size":11194,"mtime_ns":1780108930725684255,"hash":"5086070deb14d99d1a69e419b2299ec4Line truncated
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>graphify - graphify-out/graph.html</title>
|
||||
<title>graphify - backend/graphify-out/graph.html</title>
|
||||
<script src="https://unpkg.com/vis-network@9.1.6/standalone/umd/vis-network.min.js"
|
||||
integrity="sha384-Ux6phic9PEHJ38YtrijhkzyJ8yQlH8i/+buBR8s3mAZOJrP1gwyvAcIYl3GWtpX1"
|
||||
crossorigin="anonymous"></script>
|
||||
@@ -63,12 +63,12 @@
|
||||
</div>
|
||||
<div id="legend"></div>
|
||||
</div>
|
||||
<div id="stats">4819 nodes · 16536 edges · 545 communities</div>
|
||||
<div id="stats">4491 nodes · 16684 edges · 173 communities</div>
|
||||
</div>
|
||||
<script>
|
||||
const RAW_NODES = [{"id": "app_init", "label": "__init__.py", "color": {"background": "#59A14F", "border": "#59A14F", "highlight": {"background": "#ffffff", "border": "#59A14F"}}, "size": 10.0, "font": {"size": 0, "color": "#ffffff"}, "title": "__init__.py", "community": 304, "community_name": "__init__.py", "source_file": "app/__init__.py", "file_type": "code", "degree": 0}, {"id": "app_api_init", "label": "__init__.py", "color": {"background": "#4E79A7", "border": "#4E79A7", "highlight": {"background": "#ffffff", "border": "#4E79A7"}}, "size": 10.6, "font": {"size": 0, "color": "#ffffff"}, "title": "__init__.py", "community": 160, "community_name": "register_local_callback", "source_file": "app/api/__init__.py", "file_type": "code", "degree": 10}, {"id": "app_api_init_rationale_1", "label": "API \u8def\u7531\u5305 \u2014 \u5c71\u5ce1\u5316\u5de5\u751f\u4ea7\u4e00\u4f53\u5316\u95e8\u6237", "color": {"background": "#4E79A7", "border": "#4E79A7", "highlight": {"background": "#ffffff", "border": "#4E79A7"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "API \u8def\u7531\u5305 \u2014 \u5c71\u5ce1\u5316\u5de5\u751f\u4ea7\u4e00\u4f53\u5316\u95e8\u6237", "community": 160, "community_name": "register_local_callback", "source_file": "app/api/__init__.py", "file_type": "rationale", "degree": 1}, {"id": "app_api_admin", "label": "admin.py", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 17.7, "font": {"size": 12, "color": "#ffffff"}, "title": "admin.py", "community": 3, "community_name": "Community 3", "source_file": "app/api/admin.py", "file_type": "code", "degree": 119}, {"id": "app_api_admin_list_roles_for_select", "label": "list_roles_for_select()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "list_roles_for_select()", "community": 3, "community_name": "Community 3", "source_file": "app/api/admin.py", "file_type": "code", "degree": 5}, {"id": "app_api_admin_py_session", "label": "Session", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 13.7, "font": {"size": 0, "color": "#ffffff"}, "title": "Session", "community": 3, "community_name": "Community 3", "source_file": "", "file_type": "code", "degree": 57}, {"id": "app_api_admin_list_users_for_select", "label": "list_users_for_select()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "list_users_for_select()", "community": 3, "community_name": "Community 3", "source_file": "app/api/admin.py", "file_type": "code", "degree": 5}, {"id": "app_api_admin_list_roles_for_designer", "label": "list_roles_for_designer()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "list_roles_for_designer()", "community": 3, "community_name": "Community 3", "source_file": "app/api/admin.py", "file_type": "code", "degree": 5}, {"id": "app_api_admin_tenantcreatebody", "label": "TenantCreateBody", "color": {"background": "#BAB0AC", "border": "#BAB0AC", "highlight": {"background": "#ffffff", "border": "#BAB0AC"}}, "size": 10.6, "font": {"size": 0, "color": "#ffffff"}, "title": "TenantCreateBody", "community": 19, "community_name": "Community 19", "source_file": "app/api/admin.py", "file_type": "code", "degree": 10}, {"id": "app_api_admin_tenantupdatebody", "label": "TenantUpdateBody", "color": {"background": "#BAB0AC", "border": "#BAB0AC", "highlight": {"background": "#ffffff", "border": "#BAB0AC"}}, "size": 10.6, "font": {"size": 0, "color": "#ffffff"}, "title": "TenantUpdateBody", "community": 19, "community_name": "Community 19", "source_file": "app/api/admin.py", "file_type": "code", "degree": 10}, {"id": "app_api_admin_list_tenants", "label": "list_tenants()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "list_tenants()", "community": 3, "community_name": "Community 3", "source_file": "app/api/admin.py", "file_type": "code", "degree": 3}, {"id": "app_api_admin_create_tenant", "label": "create_tenant()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "create_tenant()", "community": 3, "community_name": "Community 3", "source_file": "app/api/admin.py", "file_type": "code", "degree": 5}, {"id": "app_api_admin_update_tenant", "label": "update_tenant()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "bordLine truncated
|
||||
const RAW_EDGES = [{"from": "app_api_init", "to": "app_api_admin", "label": "re_exports", "title": "re_exports [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_init", "to": "app_api_bx_init", "label": "re_exports", "title": "re_exports [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_init", "to": "app_api_crm", "label": "re_exports", "title": "re_exports [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_init", "to": "app_api_db_init", "label": "re_exports", "title": "re_exports [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_init", "to": "app_api_leave", "label": "re_exports", "title": "re_exports [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_init", "to": "app_api_menu", "label": "re_exports", "title": "re_exports [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_init", "to": "app_api_monitor", "label": "re_exports", "title": "re_exports [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_init", "to": "app_api_public", "label": "re_exports", "title": "re_exports [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_init", "to": "app_api_workflow", "label": "re_exports", "title": "re_exports [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_init_rationale_1", "to": "app_api_init", "label": "rationale_for", "title": "rationale_for [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_attachment_download_url", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_attachment_preview_url", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_attachmentdownloadurlbody", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_create_dept", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_create_dict_data", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_create_dict_type", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_create_menu", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_create_menu_favorite", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_create_post", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_create_role_management", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_create_site", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_create_tenant", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_create_user", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_delete_dept", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_delete_dict_data", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"Line truncated
|
||||
const LEGEND = [{"cid": 0, "color": "#4E79A7", "label": "Community 0", "count": 48}, {"cid": 1, "color": "#F28E2B", "label": "Community 1", "count": 43}, {"cid": 2, "color": "#E15759", "label": "Community 2", "count": 29}, {"cid": 3, "color": "#76B7B2", "label": "Community 3", "count": 66}, {"cid": 4, "color": "#59A14F", "label": "Community 4", "count": 44}, {"cid": 5, "color": "#EDC948", "label": "Community 5", "count": 59}, {"cid": 6, "color": "#B07AA1", "label": "Community 6", "count": 31}, {"cid": 7, "color": "#FF9DA7", "label": "Community 7", "count": 97}, {"cid": 8, "color": "#9C755F", "label": "Community 8", "count": 51}, {"cid": 9, "color": "#BAB0AC", "label": "Community 9", "count": 1}, {"cid": 10, "color": "#4E79A7", "label": "Community 10", "count": 1}, {"cid": 11, "color": "#F28E2B", "label": "Community 11", "count": 34}, {"cid": 12, "color": "#E15759", "label": "Community 12", "count": 80}, {"cid": 13, "color": "#76B7B2", "label": "Community 13", "count": 72}, {"cid": 14, "color": "#59A14F", "label": "Community 14", "count": 14}, {"cid": 15, "color": "#EDC948", "label": "Community 15", "count": 52}, {"cid": 16, "color": "#B07AA1", "label": "Community 16", "count": 39}, {"cid": 17, "color": "#FF9DA7", "label": "Community 17", "count": 1}, {"cid": 18, "color": "#9C755F", "label": "Community 18", "count": 66}, {"cid": 19, "color": "#BAB0AC", "label": "Community 19", "count": 31}, {"cid": 20, "color": "#4E79A7", "label": "Community 20", "count": 1}, {"cid": 21, "color": "#F28E2B", "label": "Community 21", "count": 60}, {"cid": 22, "color": "#E15759", "label": "Community 22", "count": 43}, {"cid": 23, "color": "#76B7B2", "label": "Community 23", "count": 17}, {"cid": 24, "color": "#59A14F", "label": "Community 24", "count": 47}, {"cid": 25, "color": "#EDC948", "label": "Community 25", "count": 31}, {"cid": 26, "color": "#B07AA1", "label": "Community 26", "count": 76}, {"cid": 27, "color": "#FF9DA7", "label": "Community 27", "count": 35}, {"cid": 28, "color": "#9C755F", "label": "Community 28", "count": 99}, {"cid": 29, "color": "#BAB0AC", "label": "Community 29", "count": 99}, {"cid": 30, "color": "#4E79A7", "label": "Community 30", "count": 27}, {"cid": 31, "color": "#F28E2B", "label": "Community 31", "count": 1}, {"cid": 32, "color": "#E15759", "label": "Community 32", "count": 1}, {"cid": 33, "color": "#76B7B2", "label": "Community 33", "count": 26}, {"cid": 34, "color": "#59A14F", "label": "Community 34", "count": 18}, {"cid": 35, "color": "#EDC948", "label": "Community 35", "count": 38}, {"cid": 36, "color": "#B07AA1", "label": "Community 36", "count": 31}, {"cid": 37, "color": "#FF9DA7", "label": "Community 37", "count": 26}, {"cid": 38, "color": "#9C755F", "label": "Community 38", "count": 25}, {"cid": 39, "color": "#BAB0AC", "label": "Community 39", "count": 44}, {"cid": 40, "color": "#4E79A7", "label": "Community 40", "count": 1}, {"cid": 41, "color": "#F28E2B", "label": "Community 41", "count": 1}, {"cid": 42, "color": "#E15759", "label": "Community 42", "count": 34}, {"cid": 43, "color": "#76B7B2", "label": "Community 43", "count": 42}, {"cid": 44, "color": "#59A14F", "label": "Community 44", "count": 32}, {"cid": 45, "color": "#EDC948", "label": "Community 45", "count": 75}, {"cid": 46, "color": "#B07AA1", "label": "Community 46", "count": 33}, {"cid": 47, "color": "#FF9DA7", "label": "Community 47", "count": 1}, {"cid": 48, "color": "#9C755F", "label": "Community 48", "count": 27}, {"cid": 49, "color": "#BAB0AC", "label": "Community 49", "count": 11}, {"cid": 50, "color": "#4E79A7", "label": "Community 50", "count": 26}, {"cid": 51, "color": "#F28E2B", "label": "Community 51", "count": 21}, {"cid": 52, "color": "#E15759", "label": "Community 52", "count": 31}, {"cid": 53, "color": "#76B7B2", "label": "Community 53", "count": 26}, {"cid": 54, "color": "#59A14F", "label": "Community 54", "count": 30}, {"cid": 55, "color": "#EDC948", "label": "Community 55", "count": 1}, {"cid": 56, "color": "#B07AA1", "label": "Community 56", "count": 1}, {"cid": 57, "color": "#FF9DA7", "label": "Community 57", "count": 1}, {"cid": 58, "color": "#9C755F", "label": "Community 58", "count": 20}, {"cid": 59, "color": "#BAB0AC", "label": "Community 59", "count": 30}, {"cid": 60, "color": "#4E79A7", "label": "Community 60", "count": 17}, {"cid": 61, "color": "#F28E2B", "label": "Community 61", "count": 45}, {"cid": 62, "color": "#E15759", "label": "Community 62", "count": 31}, {"cid": 63, "color": "#76B7B2", "label": "Community 63", "count": 23}, {"cid": 64, "color": "#59A14F", "label": "Community 64", "count": 17}, {"cid": 65, "color": "#EDC948", "label": "Community 65", "count": 43}, {"cid": 66, "color": "#B07AA1", "label": "Community 66", "count": 38}, {"cid": 67, "color": "#FF9DA7", "label": "Community 67", "count": 38}, {"cid": 68, "color": "#9C755F", "label": "Community 68", "count": 14}, {"cid": 69, "color": "#BAB0AC", "label": "Community 69", "count": 27}, {"cid": 70, "color": "#4E79A7", "label": "ComLine truncated
|
||||
const RAW_NODES = [{"id": "app_init", "label": "__init__.py", "color": {"background": "#59A14F", "border": "#59A14F", "highlight": {"background": "#ffffff", "border": "#59A14F"}}, "size": 10.0, "font": {"size": 0, "color": "#ffffff"}, "title": "__init__.py", "community": 304, "community_name": "__init__.py", "source_file": "app/__init__.py", "file_type": "code", "degree": 0}, {"id": "app_api_init", "label": "__init__.py", "color": {"background": "#4E79A7", "border": "#4E79A7", "highlight": {"background": "#ffffff", "border": "#4E79A7"}}, "size": 10.6, "font": {"size": 0, "color": "#ffffff"}, "title": "__init__.py", "community": 160, "community_name": "register_local_callback", "source_file": "app/api/__init__.py", "file_type": "code", "degree": 10}, {"id": "app_api_init_rationale_1", "label": "API \u8def\u7531\u5305 \u2014 \u5c71\u5ce1\u5316\u5de5\u751f\u4ea7\u4e00\u4f53\u5316\u95e8\u6237", "color": {"background": "#4E79A7", "border": "#4E79A7", "highlight": {"background": "#ffffff", "border": "#4E79A7"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "API \u8def\u7531\u5305 \u2014 \u5c71\u5ce1\u5316\u5de5\u751f\u4ea7\u4e00\u4f53\u5316\u95e8\u6237", "community": 160, "community_name": "register_local_callback", "source_file": "app/api/__init__.py", "file_type": "rationale", "degree": 1}, {"id": "app_api_admin", "label": "admin.py", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 17.7, "font": {"size": 12, "color": "#ffffff"}, "title": "admin.py", "community": 3, "community_name": "Community 3", "source_file": "app/api/admin.py", "file_type": "code", "degree": 119}, {"id": "app_api_admin_list_roles_for_select", "label": "list_roles_for_select()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "list_roles_for_select()", "community": 3, "community_name": "Community 3", "source_file": "app/api/admin.py", "file_type": "code", "degree": 5}, {"id": "app_api_admin_py_session", "label": "Session", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 13.7, "font": {"size": 0, "color": "#ffffff"}, "title": "Session", "community": 3, "community_name": "Community 3", "source_file": "", "file_type": "code", "degree": 57}, {"id": "app_api_admin_list_users_for_select", "label": "list_users_for_select()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "list_users_for_select()", "community": 3, "community_name": "Community 3", "source_file": "app/api/admin.py", "file_type": "code", "degree": 5}, {"id": "app_api_admin_list_roles_for_designer", "label": "list_roles_for_designer()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "list_roles_for_designer()", "community": 3, "community_name": "Community 3", "source_file": "app/api/admin.py", "file_type": "code", "degree": 5}, {"id": "app_api_admin_tenantcreatebody", "label": "TenantCreateBody", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.6, "font": {"size": 0, "color": "#ffffff"}, "title": "TenantCreateBody", "community": 3, "community_name": "Community 3", "source_file": "app/api/admin.py", "file_type": "code", "degree": 10}, {"id": "app_api_admin_tenantupdatebody", "label": "TenantUpdateBody", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.6, "font": {"size": 0, "color": "#ffffff"}, "title": "TenantUpdateBody", "community": 3, "community_name": "Community 3", "source_file": "app/api/admin.py", "file_type": "code", "degree": 10}, {"id": "app_api_admin_list_tenants", "label": "list_tenants()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "list_tenants()", "community": 3, "community_name": "Community 3", "source_file": "app/api/admin.py", "file_type": "code", "degree": 3}, {"id": "app_api_admin_create_tenant", "label": "create_tenant()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "create_tenant()", "community": 3, "community_name": "Community 3", "source_file": "app/api/admin.py", "file_type": "code", "degree": 5}, {"id": "app_api_admin_update_tenant", "label": "update_tenant()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border":Line truncated
|
||||
const RAW_EDGES = [{"from": "app_api_init", "to": "app_api_admin", "label": "re_exports", "title": "re_exports [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_init", "to": "app_api_bx_init", "label": "re_exports", "title": "re_exports [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_init", "to": "app_api_crm", "label": "re_exports", "title": "re_exports [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_init", "to": "app_api_db_init", "label": "re_exports", "title": "re_exports [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_init", "to": "app_api_leave", "label": "re_exports", "title": "re_exports [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_init", "to": "app_api_menu", "label": "re_exports", "title": "re_exports [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_init", "to": "app_api_monitor", "label": "re_exports", "title": "re_exports [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_init", "to": "app_api_public", "label": "re_exports", "title": "re_exports [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_init", "to": "app_api_workflow", "label": "re_exports", "title": "re_exports [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_init_rationale_1", "to": "app_api_init", "label": "rationale_for", "title": "rationale_for [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_attachment_download_url", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_attachment_preview_url", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_attachmentdownloadurlbody", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_create_dept", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_create_dict_data", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_create_dict_type", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_create_menu", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_create_menu_favorite", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_create_post", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_create_role_management", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_create_site", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_create_tenant", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_create_user", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_delete_dept", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_api_admin", "to": "app_api_admin_delete_dict_data", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"Line truncated
|
||||
const LEGEND = [{"cid": 0, "color": "#4E79A7", "label": "Community 0", "count": 38}, {"cid": 1, "color": "#F28E2B", "label": "Community 1", "count": 68}, {"cid": 2, "color": "#E15759", "label": "Community 2", "count": 32}, {"cid": 3, "color": "#76B7B2", "label": "Community 3", "count": 98}, {"cid": 4, "color": "#59A14F", "label": "Community 4", "count": 79}, {"cid": 5, "color": "#EDC948", "label": "Community 5", "count": 69}, {"cid": 6, "color": "#B07AA1", "label": "Community 6", "count": 46}, {"cid": 7, "color": "#FF9DA7", "label": "Community 7", "count": 215}, {"cid": 8, "color": "#9C755F", "label": "Community 8", "count": 68}, {"cid": 9, "color": "#BAB0AC", "label": "Community 9", "count": 62}, {"cid": 10, "color": "#4E79A7", "label": "Community 10", "count": 40}, {"cid": 11, "color": "#F28E2B", "label": "Community 11", "count": 61}, {"cid": 12, "color": "#E15759", "label": "Community 12", "count": 85}, {"cid": 13, "color": "#76B7B2", "label": "Community 13", "count": 50}, {"cid": 14, "color": "#59A14F", "label": "Community 14", "count": 14}, {"cid": 15, "color": "#EDC948", "label": "Community 15", "count": 53}, {"cid": 16, "color": "#B07AA1", "label": "Community 16", "count": 45}, {"cid": 17, "color": "#FF9DA7", "label": "Community 17", "count": 40}, {"cid": 18, "color": "#9C755F", "label": "Community 18", "count": 34}, {"cid": 19, "color": "#BAB0AC", "label": "Community 19", "count": 24}, {"cid": 20, "color": "#4E79A7", "label": "Community 20", "count": 22}, {"cid": 21, "color": "#F28E2B", "label": "Community 21", "count": 82}, {"cid": 22, "color": "#E15759", "label": "Community 22", "count": 43}, {"cid": 23, "color": "#76B7B2", "label": "Community 23", "count": 24}, {"cid": 24, "color": "#59A14F", "label": "Community 24", "count": 33}, {"cid": 25, "color": "#EDC948", "label": "Community 25", "count": 31}, {"cid": 26, "color": "#B07AA1", "label": "Community 26", "count": 33}, {"cid": 27, "color": "#FF9DA7", "label": "Community 27", "count": 37}, {"cid": 28, "color": "#9C755F", "label": "Community 28", "count": 61}, {"cid": 29, "color": "#BAB0AC", "label": "Community 29", "count": 96}, {"cid": 30, "color": "#4E79A7", "label": "Community 30", "count": 27}, {"cid": 31, "color": "#F28E2B", "label": "Community 31", "count": 19}, {"cid": 32, "color": "#E15759", "label": "Community 32", "count": 19}, {"cid": 33, "color": "#76B7B2", "label": "Community 33", "count": 18}, {"cid": 34, "color": "#59A14F", "label": "Community 34", "count": 26}, {"cid": 35, "color": "#EDC948", "label": "Community 35", "count": 38}, {"cid": 36, "color": "#B07AA1", "label": "Community 36", "count": 33}, {"cid": 37, "color": "#FF9DA7", "label": "Community 37", "count": 26}, {"cid": 38, "color": "#9C755F", "label": "Community 38", "count": 24}, {"cid": 39, "color": "#BAB0AC", "label": "Community 39", "count": 44}, {"cid": 40, "color": "#4E79A7", "label": "Community 40", "count": 18}, {"cid": 41, "color": "#F28E2B", "label": "Community 41", "count": 16}, {"cid": 42, "color": "#E15759", "label": "Community 42", "count": 34}, {"cid": 43, "color": "#76B7B2", "label": "Community 43", "count": 51}, {"cid": 44, "color": "#59A14F", "label": "Community 44", "count": 33}, {"cid": 45, "color": "#EDC948", "label": "Community 45", "count": 82}, {"cid": 46, "color": "#B07AA1", "label": "Community 46", "count": 29}, {"cid": 47, "color": "#FF9DA7", "label": "Community 47", "count": 12}, {"cid": 48, "color": "#9C755F", "label": "Community 48", "count": 9}, {"cid": 49, "color": "#BAB0AC", "label": "Community 49", "count": 11}, {"cid": 50, "color": "#4E79A7", "label": "Community 50", "count": 9}, {"cid": 51, "color": "#F28E2B", "label": "Community 51", "count": 21}, {"cid": 52, "color": "#E15759", "label": "Community 52", "count": 31}, {"cid": 53, "color": "#76B7B2", "label": "Community 53", "count": 26}, {"cid": 54, "color": "#59A14F", "label": "Community 54", "count": 30}, {"cid": 55, "color": "#EDC948", "label": "Community 55", "count": 8}, {"cid": 56, "color": "#B07AA1", "label": "Community 56", "count": 7}, {"cid": 57, "color": "#FF9DA7", "label": "Community 57", "count": 4}, {"cid": 58, "color": "#9C755F", "label": "Community 58", "count": 20}, {"cid": 59, "color": "#BAB0AC", "label": "Community 59", "count": 30}, {"cid": 60, "color": "#4E79A7", "label": "Community 60", "count": 17}, {"cid": 61, "color": "#F28E2B", "label": "Community 61", "count": 46}, {"cid": 62, "color": "#E15759", "label": "Community 62", "count": 43}, {"cid": 63, "color": "#76B7B2", "label": "Community 63", "count": 9}, {"cid": 64, "color": "#59A14F", "label": "Community 64", "count": 17}, {"cid": 65, "color": "#EDC948", "label": "Community 65", "count": 62}, {"cid": 66, "color": "#B07AA1", "label": "Community 66", "count": 51}, {"cid": 67, "color": "#FF9DA7", "label": "Community 67", "count": 38}, {"cid": 68, "color": "#9C755F", "label": "Community 68", "count": 14}, {"cid": 69, "color": "#BAB0AC", "label": "Community 69", "count": 24}, {"cid": 70, "color": "#4E79A7", "labelLine truncated
|
||||
|
||||
// HTML-escape helper — prevents XSS when injecting graph data into innerHTML
|
||||
function esc(s) {
|
||||
|
||||
+3574
-4887
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,178 @@
|
||||
# backend/tests/test_bx_reimburse_import.py
|
||||
from datetime import date
|
||||
from io import BytesIO
|
||||
|
||||
from openpyxl import Workbook, load_workbook
|
||||
|
||||
from app.services.bx.reimburse_import import (
|
||||
HEADERS,
|
||||
build_applicant_label,
|
||||
build_expense_path_labels,
|
||||
build_import_template_bytes,
|
||||
is_example_row,
|
||||
iter_data_rows,
|
||||
partition_import_groups,
|
||||
validate_group_headers,
|
||||
)
|
||||
from app.utils.excel_list_validation import add_list_validation, write_option_sheet
|
||||
|
||||
|
||||
def test_build_expense_path_labels_unique_and_enabled_only():
|
||||
nodes = [
|
||||
{"id": "1", "parent_id": "0", "item_name": "差旅费", "status": 1, "item_code": "CL"},
|
||||
{"id": "2", "parent_id": "1", "item_name": "市内交通", "status": 1, "item_code": "JT"},
|
||||
{"id": "3", "parent_id": "1", "item_name": "住宿费", "status": 0, "item_code": "ZS"},
|
||||
{"id": "4", "parent_id": "0", "item_name": "办公费", "status": 1, "item_code": "BG"},
|
||||
]
|
||||
labels = build_expense_path_labels(nodes)
|
||||
assert labels["2"] == "差旅费 / 市内交通"
|
||||
assert labels["1"] == "差旅费"
|
||||
assert labels["4"] == "办公费"
|
||||
assert "3" not in labels
|
||||
|
||||
|
||||
def test_build_expense_path_labels_exclude_orphan_when_parent_missing():
|
||||
"""父级未传入(等同已删除)时,子节点不应进入下拉——与 Web 树一致。"""
|
||||
nodes = [
|
||||
{"id": "29", "parent_id": "28", "item_name": "办公用品", "status": 1, "item_code": "BGYP"},
|
||||
{"id": "30", "parent_id": "0", "item_name": "物流运输费用", "status": 1, "item_code": "YSF"},
|
||||
]
|
||||
labels = build_expense_path_labels(nodes)
|
||||
assert "29" not in labels
|
||||
assert labels["30"] == "物流运输费用"
|
||||
|
||||
|
||||
def test_build_expense_path_labels_disambiguate_collision():
|
||||
nodes = [
|
||||
{"id": "a", "parent_id": "0", "item_name": "同名", "status": 1, "item_code": "A1"},
|
||||
{"id": "b", "parent_id": "0", "item_name": "同名", "status": 1, "item_code": "B1"},
|
||||
]
|
||||
labels = build_expense_path_labels(nodes)
|
||||
assert labels["a"] != labels["b"]
|
||||
assert "编码" in labels["a"] or "编码" in labels["b"] or labels["a"].endswith(")")
|
||||
|
||||
|
||||
def test_cell_str_normalizes_excel_float_ids():
|
||||
from app.services.bx.reimburse_import import cell_str
|
||||
|
||||
assert cell_str(29.0) == "29"
|
||||
assert cell_str(29) == "29"
|
||||
|
||||
|
||||
def test_build_applicant_label():
|
||||
assert build_applicant_label("张三", "z001", "uid-abcdef") == "张三(z001)"
|
||||
assert build_applicant_label("李四", "", "abcdefghij") == "李四(efghij)"
|
||||
|
||||
|
||||
def test_is_example_row():
|
||||
assert is_example_row({"报销事由": "【示例】地铁"}) is True
|
||||
assert is_example_row({"报销事由": "正常事由"}) is False
|
||||
|
||||
|
||||
def test_partition_empty_group_is_solo():
|
||||
rows = [
|
||||
{"_row": 2, "分组号": "G1", "申请人": "A", "报销日期": "2026-08-18", "报销标题": "", "费用项目": "x", "金额(元)": 1, "报销事由": "r1", "发生时间": ""},
|
||||
{"_row": 3, "分组号": "G1", "申请人": "A", "报销日期": "2026-08-18", "报销标题": "", "费用项目": "y", "金额(元)": 2, "报销事由": "r2", "发生时间": ""},
|
||||
{"_row": 4, "分组号": "", "申请人": "B", "报销日期": "2026-08-18", "报销标题": "", "费用项目": "z", "金额(元)": 3, "报销事由": "r3", "发生时间": ""},
|
||||
]
|
||||
groups = partition_import_groups(rows)
|
||||
assert len(groups) == 2
|
||||
g1 = next(g for g in groups if g["group_no"] == "G1")
|
||||
assert len(g1["rows"]) == 2
|
||||
solo = next(g for g in groups if g["group_no"] == "")
|
||||
assert len(solo["rows"]) == 1
|
||||
|
||||
|
||||
def test_validate_group_headers_mismatch():
|
||||
rows = [
|
||||
{"_row": 2, "申请人": "A", "报销日期": "2026-08-18", "报销标题": ""},
|
||||
{"_row": 3, "申请人": "B", "报销日期": "2026-08-18", "报销标题": ""},
|
||||
]
|
||||
err = validate_group_headers(rows)
|
||||
assert err and "申请人" in err
|
||||
|
||||
|
||||
def test_validate_group_headers_title_partial_empty():
|
||||
rows = [
|
||||
{"_row": 2, "申请人": "A", "报销日期": "2026-08-18", "报销标题": "有标题"},
|
||||
{"_row": 3, "申请人": "A", "报销日期": "2026-08-18", "报销标题": ""},
|
||||
]
|
||||
err = validate_group_headers(rows)
|
||||
assert err and "标题" in err
|
||||
|
||||
|
||||
def test_write_option_sheet_and_validation():
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
ws.title = "报销导入"
|
||||
ws.append(["申请人"])
|
||||
write_option_sheet(wb, "_选项_申请人", ["张三(z001)"], ["u1"], hidden=True)
|
||||
add_list_validation(
|
||||
ws,
|
||||
option_sheet="_选项_申请人",
|
||||
option_rows=1,
|
||||
target_col=1,
|
||||
start_row=2,
|
||||
end_row=100,
|
||||
)
|
||||
assert wb["_选项_申请人"].sheet_state == "hidden"
|
||||
assert len(ws.data_validations.dataValidation) == 1
|
||||
|
||||
|
||||
def test_build_import_template_has_guide_examples_and_dv():
|
||||
raw = build_import_template_bytes(
|
||||
expense_nodes=[
|
||||
{"id": "1", "parent_id": "0", "item_name": "差旅费", "status": 1, "item_code": "CL"},
|
||||
{"id": "2", "parent_id": "1", "item_name": "市内交通", "status": 1, "item_code": "JT"},
|
||||
],
|
||||
applicants=[{"id": "u1", "nick_name": "张三", "user_no": "z001"}],
|
||||
current_applicant_label="张三(z001)",
|
||||
today=date(2026, 8, 18),
|
||||
)
|
||||
wb = load_workbook(BytesIO(raw))
|
||||
assert "填写说明" in wb.sheetnames
|
||||
assert "报销导入" in wb.sheetnames
|
||||
assert wb["_选项_费用项目"].sheet_state == "hidden"
|
||||
ws = wb["报销导入"]
|
||||
assert [c.value for c in ws[1]][:8] == list(HEADERS)
|
||||
assert str(ws["G2"].value).startswith("【示例】")
|
||||
assert len(ws.data_validations.dataValidation) >= 1
|
||||
|
||||
|
||||
def test_iter_data_rows_skips_examples():
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
ws.title = "报销导入"
|
||||
ws.append(list(HEADERS))
|
||||
ws.append(
|
||||
["G1", "张三(z001)", "2026-08-18", "", "差旅费", 50, "【示例】跳过", ""]
|
||||
)
|
||||
ws.append(
|
||||
["G1", "张三(z001)", "2026-08-18", "", "差旅费", 80, "正式事由", ""]
|
||||
)
|
||||
rows = iter_data_rows(ws)
|
||||
assert len(rows) == 1
|
||||
assert rows[0]["报销事由"] == "正式事由"
|
||||
assert rows[0]["_row"] == 3
|
||||
|
||||
|
||||
def test_build_annotated_result_bytes_appends_status_columns():
|
||||
from app.services.bx.reimburse_import import build_annotated_result_bytes
|
||||
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
ws.title = "报销导入"
|
||||
ws.append(list(HEADERS))
|
||||
ws.append(["G1", "张三(z001)", "2026-08-18", "", "差旅费", 80, "正式事由", ""])
|
||||
buf = BytesIO()
|
||||
wb.save(buf)
|
||||
raw = build_annotated_result_bytes(
|
||||
buf.getvalue(),
|
||||
{2: {"status": "成功", "message": "成功,单号 BX001"}},
|
||||
)
|
||||
out = load_workbook(BytesIO(raw))["报销导入"]
|
||||
headers = [c.value for c in out[1]]
|
||||
assert "导入结果" in headers
|
||||
assert "失败原因" in headers
|
||||
status_i = headers.index("导入结果") + 1
|
||||
assert out.cell(row=2, column=status_i).value == "成功"
|
||||
@@ -0,0 +1,947 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>列头过滤 Demo · 文本 / 数字 / 时间</title>
|
||||
<style>
|
||||
:root {
|
||||
--primary: #10b981;
|
||||
--primary-dark: #059669;
|
||||
--primary-bg: #ecfdf5;
|
||||
--text: #1e293b;
|
||||
--text-muted: #64748b;
|
||||
--border: #e2e8f0;
|
||||
--table-head: #f8fafc;
|
||||
--danger: #dc2626;
|
||||
--warning: #d97706;
|
||||
--radius: 8px;
|
||||
--shadow: 0 8px 24px rgba(15, 23, 42, 0.12);
|
||||
font-family: Inter, system-ui, -apple-system, "Segoe UI", Roboto, "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 24px;
|
||||
color: var(--text);
|
||||
background: #f1f5f9;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.page {
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.desc {
|
||||
margin: 0 0 16px;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.legend {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border);
|
||||
background: #fff;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.chip strong { color: var(--text); font-weight: 600; }
|
||||
.dot {
|
||||
width: 8px; height: 8px; border-radius: 50%;
|
||||
background: var(--primary);
|
||||
}
|
||||
.dot.num { background: #3b82f6; }
|
||||
.dot.date { background: var(--warning); }
|
||||
|
||||
.panel {
|
||||
background: #fff;
|
||||
border: 1px solid #d9e8e2;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 16px;
|
||||
background: var(--table-head);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.toolbar .hint { color: var(--text-muted); font-size: 12px; flex: 1; min-width: 200px; }
|
||||
|
||||
button {
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid #d9e8e2;
|
||||
background: #fff;
|
||||
color: var(--text);
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
button:hover { border-color: #a7f3d0; background: var(--primary-bg); }
|
||||
button.primary {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
button.primary:hover { background: var(--primary-dark); border-color: var(--primary-dark); }
|
||||
button.ghost-danger { color: var(--danger); border-color: #fecaca; }
|
||||
button.ghost-danger:hover { background: #fef2f2; }
|
||||
button:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
|
||||
.table-wrap { overflow: auto; }
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
min-width: 860px;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
background: var(--table-head);
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
th .th-inner {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
th.align-right, td.align-right { text-align: right; }
|
||||
th.align-center, td.align-center { text-align: center; }
|
||||
|
||||
tbody tr:nth-child(even) { background: #fafbfc; }
|
||||
tbody tr:hover { background: #f0fdf4; }
|
||||
tbody tr.is-hidden { display: none; }
|
||||
|
||||
.filter-btn {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: #94a3b8;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.filter-btn:hover { background: #e2e8f0; color: var(--text); }
|
||||
.filter-btn.active {
|
||||
color: #fff;
|
||||
background: var(--primary);
|
||||
}
|
||||
.filter-btn svg { width: 14px; height: 14px; }
|
||||
|
||||
.tag {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
.tag.draft { background: #64748b; }
|
||||
.tag.pending { background: var(--warning); }
|
||||
.tag.approved { background: var(--primary-dark); }
|
||||
.tag.rejected { background: var(--danger); }
|
||||
|
||||
/* Popover panel */
|
||||
.popover {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
width: 280px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
box-shadow: var(--shadow);
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
max-height: min(420px, calc(100vh - 24px));
|
||||
}
|
||||
.popover.open { display: flex; }
|
||||
|
||||
.popover-hd {
|
||||
padding: 10px 12px 8px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.popover-hd .title {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.popover-hd .sub {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.popover-bd {
|
||||
padding: 10px 12px;
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.popover-ft {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
border-top: 1px solid var(--border);
|
||||
background: var(--table-head);
|
||||
}
|
||||
|
||||
.field { margin-bottom: 10px; }
|
||||
.field:last-child { margin-bottom: 0; }
|
||||
.field label {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="number"],
|
||||
input[type="date"],
|
||||
input[type="datetime-local"],
|
||||
select {
|
||||
width: 100%;
|
||||
height: 34px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
font: inherit;
|
||||
color: var(--text);
|
||||
background: #fff;
|
||||
}
|
||||
input:focus, select:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.15);
|
||||
}
|
||||
|
||||
.range-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.range-row .sep { color: var(--text-muted); font-size: 12px; text-align: center; }
|
||||
|
||||
.ops {
|
||||
display: grid;
|
||||
grid-template-columns: 110px 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.value-list {
|
||||
max-height: 180px;
|
||||
overflow: auto;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 4px;
|
||||
}
|
||||
.value-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 8px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
}
|
||||
.value-item:hover { background: var(--primary-bg); }
|
||||
.value-item input { width: auto; margin: 0; }
|
||||
.value-item .count {
|
||||
margin-left: auto;
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.select-all-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.select-all-row button {
|
||||
height: 26px;
|
||||
padding: 0 8px;
|
||||
font-size: 12px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
.select-all-row button:hover { background: var(--primary-bg); }
|
||||
|
||||
.empty-tip {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.footer-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 16px;
|
||||
border-top: 1px solid var(--border);
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.notes {
|
||||
margin-top: 20px;
|
||||
padding: 14px 16px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.notes h2 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 14px;
|
||||
color: var(--text);
|
||||
}
|
||||
.notes ul { margin: 0; padding-left: 18px; }
|
||||
.notes li { margin-bottom: 4px; }
|
||||
.notes code {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: 12px;
|
||||
background: #f1f5f9;
|
||||
padding: 1px 5px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<h1>Excel 风格列头过滤 Demo</h1>
|
||||
<p class="desc">
|
||||
模拟报销申请列表:点击表头漏斗图标过滤<strong>当前页数据</strong>。
|
||||
已覆盖 <strong>文本</strong>、<strong>数字</strong>、<strong>日期/时间</strong>三类控件。
|
||||
</p>
|
||||
|
||||
<div class="legend">
|
||||
<span class="chip"><span class="dot"></span><strong>文本</strong>:搜索 + 勾选本页去重值</span>
|
||||
<span class="chip"><span class="dot num"></span><strong>数字</strong>:运算符 + 单值 / 区间</span>
|
||||
<span class="chip"><span class="dot date"></span><strong>时间</strong>:日期区间(可扩到时分)</span>
|
||||
</div>
|
||||
|
||||
<section class="panel">
|
||||
<div class="toolbar">
|
||||
<span class="hint" id="activeFilterHint">当前无列过滤</span>
|
||||
<button type="button" id="btnClearAll" class="ghost-danger" disabled>清空全部过滤</button>
|
||||
<button type="button" id="btnResetData">恢复演示数据</button>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap">
|
||||
<table id="demoTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="align-center" style="width:56px">序号</th>
|
||||
<th data-col="applyCode" data-type="text">
|
||||
<span class="th-inner">报销单号<button type="button" class="filter-btn" data-col="applyCode" aria-label="过滤报销单号"></button></span>
|
||||
</th>
|
||||
<th data-col="title" data-type="text">
|
||||
<span class="th-inner">报销标题<button type="button" class="filter-btn" data-col="title" aria-label="过滤报销标题"></button></span>
|
||||
</th>
|
||||
<th data-col="expenseName" data-type="text">
|
||||
<span class="th-inner">费用项目<button type="button" class="filter-btn" data-col="expenseName" aria-label="过滤费用项目"></button></span>
|
||||
</th>
|
||||
<th class="align-right" data-col="amount" data-type="number">
|
||||
<span class="th-inner">报销金额(元)<button type="button" class="filter-btn" data-col="amount" aria-label="过滤金额"></button></span>
|
||||
</th>
|
||||
<th class="align-center" data-col="status" data-type="enum">
|
||||
<span class="th-inner">状态<button type="button" class="filter-btn" data-col="status" aria-label="过滤状态"></button></span>
|
||||
</th>
|
||||
<th class="align-center" data-col="applyTime" data-type="datetime">
|
||||
<span class="th-inner">申请时间<button type="button" class="filter-btn" data-col="applyTime" aria-label="过滤申请时间"></button></span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="footer-bar">
|
||||
<span id="pageStat">显示 0 / 0 条(当前页)</span>
|
||||
<span>说明:本 Demo 仅过滤当前表格内数据,不请求后端</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="notes">
|
||||
<h2>各数据类型过滤建议</h2>
|
||||
<ul>
|
||||
<li><strong>文本 / 枚举</strong>:漏斗内「关键词搜索」缩小可选值,再多选勾选;枚举列(状态)可固定选项,不必依赖本页去重。</li>
|
||||
<li><strong>数字</strong>:提供运算符(等于 / 大于 / 小于 / 介于);「介于」用最小值~最大值;注意金额展示与比较用原始数值。</li>
|
||||
<li><strong>日期 / 时间</strong>:默认「开始日期~结束日期」闭区间;若业务要精确到时分,用 <code>datetime-local</code> 或拆成日期+时间两段。</li>
|
||||
<li><strong>组合</strong>:多列过滤取交集(AND);任一列清空后只影响该列。</li>
|
||||
<li><strong>与顶部筛选并存时</strong>:顶部筛选 = 查库;列头过滤 = 当前页二次过滤,文案上要区分,避免用户误以为筛了全库。</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Shared popover -->
|
||||
<div class="popover" id="popover" role="dialog" aria-modal="false">
|
||||
<div class="popover-hd">
|
||||
<div class="title" id="popTitle">过滤</div>
|
||||
<div class="sub" id="popSub">按本列筛选当前页</div>
|
||||
</div>
|
||||
<div class="popover-bd" id="popBody"></div>
|
||||
<div class="popover-ft">
|
||||
<button type="button" id="popClear">清空本列</button>
|
||||
<div style="display:flex;gap:8px">
|
||||
<button type="button" id="popCancel">取消</button>
|
||||
<button type="button" class="primary" id="popOk">确定</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const STATUS_MAP = {
|
||||
draft: { label: '待提交', cls: 'draft' },
|
||||
pending: { label: '审批中', cls: 'pending' },
|
||||
approved: { label: '审批通过', cls: 'approved' },
|
||||
rejected: { label: '审批驳回', cls: 'rejected' }
|
||||
}
|
||||
|
||||
const COLUMNS = {
|
||||
applyCode: { label: '报销单号', type: 'text' },
|
||||
title: { label: '报销标题', type: 'text' },
|
||||
expenseName: { label: '费用项目', type: 'text' },
|
||||
amount: { label: '报销金额(元)', type: 'number' },
|
||||
status: { label: '状态', type: 'enum', enumOptions: Object.keys(STATUS_MAP) },
|
||||
applyTime: { label: '申请时间', type: 'datetime' }
|
||||
}
|
||||
|
||||
/** Demo rows — mixed types */
|
||||
const DEMO_ROWS = [
|
||||
{ applyCode: 'BX20260818001', title: '差旅报销-上海客户拜访', expenseName: '差旅费 / 交通费', amount: 1280.5, status: 'pending', applyTime: '2026-08-10 09:20:00' },
|
||||
{ applyCode: 'BX20260818002', title: '办公用品采购', expenseName: '办公费 / 耗材', amount: 356, status: 'draft', applyTime: '2026-08-11 14:05:00' },
|
||||
{ applyCode: 'BX20260818003', title: '团队团建餐费', expenseName: '业务招待费', amount: 980, status: 'approved', applyTime: '2026-08-12 18:40:00' },
|
||||
{ applyCode: 'BX20260818004', title: '快递运费报销', expenseName: '物流费', amount: 86.2, status: 'rejected', applyTime: '2026-08-13 10:12:00' },
|
||||
{ applyCode: 'BX20260818005', title: '培训报名费', expenseName: '培训费', amount: 2500, status: 'pending', applyTime: '2026-08-14 11:30:00' },
|
||||
{ applyCode: 'BX20260818006', title: '差旅报销-广州展会', expenseName: '差旅费 / 住宿费', amount: 1860, status: 'approved', applyTime: '2026-08-15 08:55:00' },
|
||||
{ applyCode: 'BX20260818007', title: '加油站加油费', expenseName: '车辆使用费', amount: 420.8, status: 'draft', applyTime: '2026-08-16 16:22:00' },
|
||||
{ applyCode: 'BX20260818008', title: '软件订阅年费', expenseName: '信息化费用', amount: 3999, status: 'pending', applyTime: '2026-08-17 09:08:00' },
|
||||
{ applyCode: 'BX20260818009', title: '客户招待午餐', expenseName: '业务招待费', amount: 680, status: 'approved', applyTime: '2026-08-17 13:45:00' },
|
||||
{ applyCode: 'BX20260818010', title: '打印机维修', expenseName: '办公费 / 维修', amount: 230, status: 'draft', applyTime: '2026-08-18 10:01:00' }
|
||||
]
|
||||
|
||||
/** Active filters: { colKey: filterState } */
|
||||
const filters = {}
|
||||
|
||||
let draft = null // { col, type, ... } while popover open
|
||||
let openCol = null
|
||||
|
||||
const funnelSvg = `
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M3 5a1 1 0 0 1 1-1h16a1 1 0 0 1 .8 1.6L15 13v5.2a1 1 0 0 1-1.45.9l-3-1.5A1 1 0 0 1 10 16.7V13L3.2 5.6A1 1 0 0 1 3 5z"/>
|
||||
</svg>`
|
||||
|
||||
document.querySelectorAll('.filter-btn').forEach((btn) => {
|
||||
btn.innerHTML = funnelSvg
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation()
|
||||
openFilter(btn.dataset.col, btn)
|
||||
})
|
||||
})
|
||||
|
||||
function formatMoney(n) {
|
||||
return Number(n).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
}
|
||||
|
||||
function formatDateOnly(s) {
|
||||
return String(s || '').slice(0, 10)
|
||||
}
|
||||
|
||||
function parseDate(s) {
|
||||
// Treat as local date for range compare (date part or full datetime)
|
||||
const t = Date.parse(String(s).replace(/-/g, '/'))
|
||||
return Number.isNaN(t) ? null : t
|
||||
}
|
||||
|
||||
function startOfDay(yyyyMmDd) {
|
||||
return parseDate(yyyyMmDd + ' 00:00:00')
|
||||
}
|
||||
|
||||
function endOfDay(yyyyMmDd) {
|
||||
return parseDate(yyyyMmDd + ' 23:59:59')
|
||||
}
|
||||
|
||||
function displayValue(col, row) {
|
||||
if (col === 'status') return STATUS_MAP[row.status]?.label || row.status
|
||||
if (col === 'amount') return formatMoney(row.amount)
|
||||
if (col === 'applyTime') return formatDateOnly(row.applyTime)
|
||||
return String(row[col] ?? '')
|
||||
}
|
||||
|
||||
function renderTable() {
|
||||
const tbody = document.getElementById('tbody')
|
||||
tbody.innerHTML = ''
|
||||
let visible = 0
|
||||
|
||||
DEMO_ROWS.forEach((row, idx) => {
|
||||
const pass = matchAllFilters(row)
|
||||
if (pass) visible += 1
|
||||
|
||||
const tr = document.createElement('tr')
|
||||
if (!pass) tr.classList.add('is-hidden')
|
||||
|
||||
const st = STATUS_MAP[row.status]
|
||||
tr.innerHTML = `
|
||||
<td class="align-center">${idx + 1}</td>
|
||||
<td>${escapeHtml(row.applyCode)}</td>
|
||||
<td>${escapeHtml(row.title)}</td>
|
||||
<td>${escapeHtml(row.expenseName)}</td>
|
||||
<td class="align-right">${formatMoney(row.amount)}</td>
|
||||
<td class="align-center"><span class="tag ${st.cls}">${st.label}</span></td>
|
||||
<td class="align-center">${formatDateOnly(row.applyTime)}</td>
|
||||
`
|
||||
tbody.appendChild(tr)
|
||||
})
|
||||
|
||||
document.getElementById('pageStat').textContent =
|
||||
`显示 ${visible} / ${DEMO_ROWS.length} 条(当前页)`
|
||||
updateFilterUi()
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
}
|
||||
|
||||
function matchAllFilters(row) {
|
||||
return Object.keys(filters).every((col) => matchOne(col, filters[col], row))
|
||||
}
|
||||
|
||||
function matchOne(col, f, row) {
|
||||
if (!f) return true
|
||||
const meta = COLUMNS[col]
|
||||
const type = meta.type
|
||||
|
||||
if (type === 'text' || type === 'enum') {
|
||||
const selected = f.selected
|
||||
if (!selected || selected.length === 0) return true
|
||||
const raw = type === 'enum' ? row[col] : String(row[col] ?? '')
|
||||
return selected.includes(raw)
|
||||
}
|
||||
|
||||
if (type === 'number') {
|
||||
const val = Number(row[col])
|
||||
const op = f.op || 'between'
|
||||
if (op === 'eq') return f.value !== '' && val === Number(f.value)
|
||||
if (op === 'gt') return f.value !== '' && val > Number(f.value)
|
||||
if (op === 'lt') return f.value !== '' && val < Number(f.value)
|
||||
if (op === 'gte') return f.value !== '' && val >= Number(f.value)
|
||||
if (op === 'lte') return f.value !== '' && val <= Number(f.value)
|
||||
// between
|
||||
const minOk = f.min === '' || f.min == null || val >= Number(f.min)
|
||||
const maxOk = f.max === '' || f.max == null || val <= Number(f.max)
|
||||
if ((f.min === '' || f.min == null) && (f.max === '' || f.max == null)) return true
|
||||
return minOk && maxOk
|
||||
}
|
||||
|
||||
if (type === 'datetime') {
|
||||
const t = parseDate(row[col])
|
||||
if (t == null) return false
|
||||
const fromOk = !f.from || t >= startOfDay(f.from)
|
||||
const toOk = !f.to || t <= endOfDay(f.to)
|
||||
if (!f.from && !f.to) return true
|
||||
return fromOk && toOk
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function updateFilterUi() {
|
||||
const activeCols = Object.keys(filters).filter((k) => isFilterActive(k, filters[k]))
|
||||
document.querySelectorAll('.filter-btn').forEach((btn) => {
|
||||
btn.classList.toggle('active', activeCols.includes(btn.dataset.col))
|
||||
})
|
||||
const hint = document.getElementById('activeFilterHint')
|
||||
const clearBtn = document.getElementById('btnClearAll')
|
||||
if (activeCols.length === 0) {
|
||||
hint.textContent = '当前无列过滤'
|
||||
clearBtn.disabled = true
|
||||
} else {
|
||||
hint.textContent = '已过滤列:' + activeCols.map((c) => COLUMNS[c].label).join('、')
|
||||
clearBtn.disabled = false
|
||||
}
|
||||
}
|
||||
|
||||
function isFilterActive(col, f) {
|
||||
if (!f) return false
|
||||
const type = COLUMNS[col].type
|
||||
if (type === 'text' || type === 'enum') return Array.isArray(f.selected) && f.selected.length > 0
|
||||
if (type === 'number') {
|
||||
if (f.op === 'between') return (f.min !== '' && f.min != null) || (f.max !== '' && f.max != null)
|
||||
return f.value !== '' && f.value != null
|
||||
}
|
||||
if (type === 'datetime') return !!(f.from || f.to)
|
||||
return false
|
||||
}
|
||||
|
||||
function uniqueValues(col) {
|
||||
const map = new Map()
|
||||
DEMO_ROWS.forEach((row) => {
|
||||
const key = COLUMNS[col].type === 'enum' ? row[col] : String(row[col] ?? '')
|
||||
map.set(key, (map.get(key) || 0) + 1)
|
||||
})
|
||||
return [...map.entries()].sort((a, b) => String(a[0]).localeCompare(String(b[0]), 'zh-CN'))
|
||||
}
|
||||
|
||||
function openFilter(col, anchorBtn) {
|
||||
const meta = COLUMNS[col]
|
||||
openCol = col
|
||||
draft = cloneFilter(col, filters[col])
|
||||
|
||||
const pop = document.getElementById('popover')
|
||||
document.getElementById('popTitle').textContent = '过滤 · ' + meta.label
|
||||
document.getElementById('popSub').textContent =
|
||||
meta.type === 'text' ? '文本:搜索并勾选本页出现的值'
|
||||
: meta.type === 'enum' ? '枚举:勾选状态(固定选项)'
|
||||
: meta.type === 'number' ? '数字:运算符 + 单值或区间'
|
||||
: '时间:按日期闭区间过滤(含起止当天)'
|
||||
|
||||
document.getElementById('popBody').innerHTML = buildBodyHtml(col, draft)
|
||||
wireBodyEvents(col)
|
||||
|
||||
positionPopover(pop, anchorBtn)
|
||||
pop.classList.add('open')
|
||||
}
|
||||
|
||||
function cloneFilter(col, f) {
|
||||
const type = COLUMNS[col].type
|
||||
if (!f) {
|
||||
if (type === 'text' || type === 'enum') return { selected: [], keyword: '' }
|
||||
if (type === 'number') return { op: 'between', value: '', min: '', max: '' }
|
||||
if (type === 'datetime') return { from: '', to: '' }
|
||||
}
|
||||
return JSON.parse(JSON.stringify(f))
|
||||
}
|
||||
|
||||
function buildBodyHtml(col, f) {
|
||||
const type = COLUMNS[col].type
|
||||
|
||||
if (type === 'text' || type === 'enum') {
|
||||
const options = type === 'enum'
|
||||
? COLUMNS[col].enumOptions.map((k) => [k, DEMO_ROWS.filter((r) => r.status === k).length])
|
||||
: uniqueValues(col)
|
||||
|
||||
const items = options.map(([key, count]) => {
|
||||
const checked = f.selected.includes(key) ? 'checked' : ''
|
||||
const label = type === 'enum' ? STATUS_MAP[key].label : key
|
||||
return `
|
||||
<label class="value-item" data-key="${escapeHtml(key)}">
|
||||
<input type="checkbox" value="${escapeHtml(key)}" ${checked} />
|
||||
<span>${escapeHtml(label)}</span>
|
||||
<span class="count">${count}</span>
|
||||
</label>`
|
||||
}).join('')
|
||||
|
||||
return `
|
||||
<div class="field">
|
||||
<label>搜索可选值</label>
|
||||
<input type="text" id="kwInput" placeholder="输入关键字过滤下方列表" value="${escapeHtml(f.keyword || '')}" />
|
||||
</div>
|
||||
<div class="select-all-row">
|
||||
<span>本页可选值</span>
|
||||
<span>
|
||||
<button type="button" id="btnSelectAll">全选</button>
|
||||
<button type="button" id="btnSelectNone">全不选</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="value-list" id="valueList">${items || '<div class="empty-tip">无数据</div>'}</div>
|
||||
`
|
||||
}
|
||||
|
||||
if (type === 'number') {
|
||||
const showSingle = f.op !== 'between'
|
||||
return `
|
||||
<div class="field">
|
||||
<label>条件</label>
|
||||
<div class="ops">
|
||||
<select id="numOp">
|
||||
<option value="between" ${f.op === 'between' ? 'selected' : ''}>介于</option>
|
||||
<option value="eq" ${f.op === 'eq' ? 'selected' : ''}>等于</option>
|
||||
<option value="gt" ${f.op === 'gt' ? 'selected' : ''}>大于</option>
|
||||
<option value="gte" ${f.op === 'gte' ? 'selected' : ''}>大于等于</option>
|
||||
<option value="lt" ${f.op === 'lt' ? 'selected' : ''}>小于</option>
|
||||
<option value="lte" ${f.op === 'lte' ? 'selected' : ''}>小于等于</option>
|
||||
</select>
|
||||
<div id="numSingleWrap" style="${showSingle ? '' : 'display:none'}">
|
||||
<input type="number" id="numValue" step="0.01" placeholder="输入金额" value="${escapeHtml(f.value ?? '')}" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field" id="numRangeWrap" style="${showSingle ? 'display:none' : ''}">
|
||||
<label>金额区间(元)</label>
|
||||
<div class="range-row">
|
||||
<input type="number" id="numMin" step="0.01" placeholder="最小值" value="${escapeHtml(f.min ?? '')}" />
|
||||
<span class="sep">至</span>
|
||||
<input type="number" id="numMax" step="0.01" placeholder="最大值" value="${escapeHtml(f.max ?? '')}" />
|
||||
</div>
|
||||
</div>
|
||||
<p style="margin:8px 0 0;font-size:11px;color:var(--text-muted)">
|
||||
提示:比较使用原始数值;本页金额范围约 ${formatMoney(Math.min(...DEMO_ROWS.map(r => r.amount)))} ~ ${formatMoney(Math.max(...DEMO_ROWS.map(r => r.amount)))}
|
||||
</p>
|
||||
`
|
||||
}
|
||||
|
||||
if (type === 'datetime') {
|
||||
return `
|
||||
<div class="field">
|
||||
<label>开始日期</label>
|
||||
<input type="date" id="dateFrom" value="${escapeHtml(f.from || '')}" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>结束日期</label>
|
||||
<input type="date" id="dateTo" value="${escapeHtml(f.to || '')}" />
|
||||
</div>
|
||||
<p style="margin:8px 0 0;font-size:11px;color:var(--text-muted)">
|
||||
闭区间:含起止当天 00:00:00~23:59:59。若需时分秒,可把控件换成 <code>datetime-local</code>。
|
||||
</p>
|
||||
`
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
function wireBodyEvents(col) {
|
||||
const type = COLUMNS[col].type
|
||||
|
||||
if (type === 'text' || type === 'enum') {
|
||||
const kw = document.getElementById('kwInput')
|
||||
const list = document.getElementById('valueList')
|
||||
|
||||
kw.addEventListener('input', () => {
|
||||
draft.keyword = kw.value.trim()
|
||||
const q = draft.keyword.toLowerCase()
|
||||
list.querySelectorAll('.value-item').forEach((el) => {
|
||||
const key = el.dataset.key
|
||||
const label = COLUMNS[col].type === 'enum' ? STATUS_MAP[key].label : key
|
||||
const hit = !q || label.toLowerCase().includes(q) || String(key).toLowerCase().includes(q)
|
||||
el.style.display = hit ? '' : 'none'
|
||||
})
|
||||
})
|
||||
|
||||
list.addEventListener('change', (e) => {
|
||||
if (e.target.type !== 'checkbox') return
|
||||
const val = e.target.value
|
||||
if (e.target.checked) {
|
||||
if (!draft.selected.includes(val)) draft.selected.push(val)
|
||||
} else {
|
||||
draft.selected = draft.selected.filter((x) => x !== val)
|
||||
}
|
||||
})
|
||||
|
||||
document.getElementById('btnSelectAll').addEventListener('click', () => {
|
||||
list.querySelectorAll('.value-item').forEach((el) => {
|
||||
if (el.style.display === 'none') return
|
||||
const cb = el.querySelector('input')
|
||||
cb.checked = true
|
||||
if (!draft.selected.includes(cb.value)) draft.selected.push(cb.value)
|
||||
})
|
||||
})
|
||||
|
||||
document.getElementById('btnSelectNone').addEventListener('click', () => {
|
||||
list.querySelectorAll('.value-item').forEach((el) => {
|
||||
if (el.style.display === 'none') return
|
||||
const cb = el.querySelector('input')
|
||||
cb.checked = false
|
||||
draft.selected = draft.selected.filter((x) => x !== cb.value)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if (type === 'number') {
|
||||
const op = document.getElementById('numOp')
|
||||
const sync = () => {
|
||||
draft.op = op.value
|
||||
const single = draft.op !== 'between'
|
||||
document.getElementById('numSingleWrap').style.display = single ? '' : 'none'
|
||||
document.getElementById('numRangeWrap').style.display = single ? 'none' : ''
|
||||
}
|
||||
op.addEventListener('change', sync)
|
||||
;['numValue', 'numMin', 'numMax'].forEach((id) => {
|
||||
const el = document.getElementById(id)
|
||||
if (!el) return
|
||||
el.addEventListener('input', () => {
|
||||
if (id === 'numValue') draft.value = el.value
|
||||
if (id === 'numMin') draft.min = el.value
|
||||
if (id === 'numMax') draft.max = el.value
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if (type === 'datetime') {
|
||||
document.getElementById('dateFrom').addEventListener('change', (e) => { draft.from = e.target.value })
|
||||
document.getElementById('dateTo').addEventListener('change', (e) => { draft.to = e.target.value })
|
||||
}
|
||||
}
|
||||
|
||||
function readDraftFromDom(col) {
|
||||
const type = COLUMNS[col].type
|
||||
if (type === 'number') {
|
||||
draft.op = document.getElementById('numOp').value
|
||||
draft.value = document.getElementById('numValue')?.value ?? ''
|
||||
draft.min = document.getElementById('numMin')?.value ?? ''
|
||||
draft.max = document.getElementById('numMax')?.value ?? ''
|
||||
}
|
||||
if (type === 'datetime') {
|
||||
draft.from = document.getElementById('dateFrom').value
|
||||
draft.to = document.getElementById('dateTo').value
|
||||
}
|
||||
}
|
||||
|
||||
function applyDraft() {
|
||||
if (!openCol) return
|
||||
readDraftFromDom(openCol)
|
||||
|
||||
// Validate number range
|
||||
if (COLUMNS[openCol].type === 'number' && draft.op === 'between') {
|
||||
if (draft.min !== '' && draft.max !== '' && Number(draft.min) > Number(draft.max)) {
|
||||
alert('最小值不能大于最大值')
|
||||
return
|
||||
}
|
||||
}
|
||||
if (COLUMNS[openCol].type === 'datetime') {
|
||||
if (draft.from && draft.to && draft.from > draft.to) {
|
||||
alert('开始日期不能晚于结束日期')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (isFilterActive(openCol, draft)) {
|
||||
filters[openCol] = JSON.parse(JSON.stringify(draft))
|
||||
} else {
|
||||
delete filters[openCol]
|
||||
}
|
||||
closePopover()
|
||||
renderTable()
|
||||
}
|
||||
|
||||
function clearCurrentCol() {
|
||||
if (!openCol) return
|
||||
delete filters[openCol]
|
||||
closePopover()
|
||||
renderTable()
|
||||
}
|
||||
|
||||
function clearAllFilters() {
|
||||
Object.keys(filters).forEach((k) => delete filters[k])
|
||||
closePopover()
|
||||
renderTable()
|
||||
}
|
||||
|
||||
function closePopover() {
|
||||
document.getElementById('popover').classList.remove('open')
|
||||
openCol = null
|
||||
draft = null
|
||||
}
|
||||
|
||||
function positionPopover(pop, anchor) {
|
||||
const rect = anchor.getBoundingClientRect()
|
||||
const pad = 8
|
||||
let left = rect.left
|
||||
let top = rect.bottom + 6
|
||||
|
||||
pop.style.left = '0px'
|
||||
pop.style.top = '0px'
|
||||
pop.classList.add('open')
|
||||
const pw = pop.offsetWidth
|
||||
const ph = pop.offsetHeight
|
||||
|
||||
if (left + pw > window.innerWidth - pad) left = window.innerWidth - pw - pad
|
||||
if (left < pad) left = pad
|
||||
if (top + ph > window.innerHeight - pad) top = rect.top - ph - 6
|
||||
if (top < pad) top = pad
|
||||
|
||||
pop.style.left = left + 'px'
|
||||
pop.style.top = top + 'px'
|
||||
}
|
||||
|
||||
document.getElementById('popOk').addEventListener('click', applyDraft)
|
||||
document.getElementById('popCancel').addEventListener('click', closePopover)
|
||||
document.getElementById('popClear').addEventListener('click', clearCurrentCol)
|
||||
document.getElementById('btnClearAll').addEventListener('click', clearAllFilters)
|
||||
document.getElementById('btnResetData').addEventListener('click', () => {
|
||||
clearAllFilters()
|
||||
})
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
const pop = document.getElementById('popover')
|
||||
if (!pop.classList.contains('open')) return
|
||||
if (pop.contains(e.target)) return
|
||||
if (e.target.closest('.filter-btn')) return
|
||||
closePopover()
|
||||
})
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') closePopover()
|
||||
})
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
if (document.getElementById('popover').classList.contains('open')) closePopover()
|
||||
})
|
||||
|
||||
renderTable()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,897 @@
|
||||
# 报销申请批量导入(Excel 下拉模板)Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 在报销申请页提供「下载模板 / 批量导入」:模板内置费用项与申请人 Excel 下拉、填写说明与示例行;导入按分组号合成多明细草稿,支持部分成功。
|
||||
|
||||
**Architecture:** 后端用 openpyxl 动态生成含隐藏选项表 + DataValidation 的 xlsx;导入服务解析行、按分组号分桶、校验单头一致后复用 `create_with_details` 写 `draft`。纯函数(路径文案、申请人文案、分组、示例识别)单独可测;API 薄封装。前端工具栏 + 导入结果弹窗,blob/FormData 走已有 `bxRequest`(`/api/reimburses` 已在加密排除列表)。
|
||||
|
||||
**Tech Stack:** FastAPI、SQLAlchemy、openpyxl、pytest、Vue 3、Element Plus、`bxRequest`
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-08-18-reimburse-apply-batch-import-design.md`
|
||||
|
||||
---
|
||||
|
||||
## 文件结构
|
||||
|
||||
| 操作 | 文件 | 职责 |
|
||||
|------|------|------|
|
||||
| Create | `backend/app/utils/excel_list_validation.py` | 隐藏选项表 + list DataValidation 辅助 |
|
||||
| Create | `backend/app/services/bx/reimburse_import.py` | 文案/分组纯函数、建模板、导入写库 |
|
||||
| Create | `backend/tests/test_bx_reimburse_import.py` | 纯函数与模板/解析单测 |
|
||||
| Modify | `backend/app/api/bx/reimburse.py` | `GET import-template`、`POST import` |
|
||||
| Modify | `web-admin/src/api/bx/index.ts` | `downloadImportTemplate`、`importExcel` |
|
||||
| Modify | `web-admin/src/views/bx/reimburse/apply/index.vue` | 工具栏按钮 + 导入弹窗 |
|
||||
|
||||
**不变:** 发票关联、自动提交、bx-mobile、CRM 导入、加密排除列表(已含 `/api/reimburses`)。
|
||||
|
||||
**常量约定(实现时写在 `reimburse_import.py` 顶部):**
|
||||
|
||||
```python
|
||||
IMPORT_SHEET = "报销导入"
|
||||
GUIDE_SHEET = "填写说明"
|
||||
EXPENSE_OPT_SHEET = "_选项_费用项目"
|
||||
APPLICANT_OPT_SHEET = "_选项_申请人"
|
||||
EXAMPLE_REMARK_PREFIX = "【示例】"
|
||||
MAX_IMPORT_ROWS = 500
|
||||
PATH_SEP = " / "
|
||||
HEADERS = [
|
||||
"分组号", "申请人", "报销日期", "报销标题",
|
||||
"费用项目", "金额(元)", "报销事由", "发生时间",
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 纯函数 — 费用路径 / 申请人文案 / 示例行 / 分组
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/app/services/bx/reimburse_import.py`(先只放纯函数)
|
||||
- Create: `backend/tests/test_bx_reimburse_import.py`
|
||||
|
||||
- [ ] **Step 1: 写失败单测**
|
||||
|
||||
```python
|
||||
# backend/tests/test_bx_reimburse_import.py
|
||||
from app.services.bx.reimburse_import import (
|
||||
build_applicant_label,
|
||||
build_expense_path_labels,
|
||||
is_example_row,
|
||||
partition_import_groups,
|
||||
validate_group_headers,
|
||||
)
|
||||
|
||||
|
||||
def test_build_expense_path_labels_unique_and_enabled_only():
|
||||
# flat nodes: id, parent_id, name, status, item_code
|
||||
nodes = [
|
||||
{"id": "1", "parent_id": "0", "item_name": "差旅费", "status": 1, "item_code": "CL"},
|
||||
{"id": "2", "parent_id": "1", "item_name": "市内交通", "status": 1, "item_code": "JT"},
|
||||
{"id": "3", "parent_id": "1", "item_name": "住宿费", "status": 0, "item_code": "ZS"},
|
||||
{"id": "4", "parent_id": "0", "item_name": "办公费", "status": 1, "item_code": "BG"},
|
||||
]
|
||||
labels = build_expense_path_labels(nodes)
|
||||
assert labels["2"] == "差旅费 / 市内交通"
|
||||
assert labels["1"] == "差旅费"
|
||||
assert labels["4"] == "办公费"
|
||||
assert "3" not in labels # 停用
|
||||
|
||||
|
||||
def test_build_expense_path_labels_disambiguate_collision():
|
||||
nodes = [
|
||||
{"id": "a", "parent_id": "0", "item_name": "同名", "status": 1, "item_code": "A1"},
|
||||
{"id": "b", "parent_id": "0", "item_name": "同名", "status": 1, "item_code": "B1"},
|
||||
]
|
||||
labels = build_expense_path_labels(nodes)
|
||||
assert labels["a"] != labels["b"]
|
||||
assert "编码" in labels["a"] or "编码" in labels["b"] or labels["a"].endswith(")")
|
||||
|
||||
|
||||
def test_build_applicant_label():
|
||||
assert build_applicant_label("张三", "z001", "uid-abcdef") == "张三(z001)"
|
||||
assert build_applicant_label("李四", "", "abcdefghij") == "李四(efghij)"
|
||||
|
||||
|
||||
def test_is_example_row():
|
||||
assert is_example_row({"报销事由": "【示例】地铁"}) is True
|
||||
assert is_example_row({"报销事由": "正常事由"}) is False
|
||||
|
||||
|
||||
def test_partition_empty_group_is_solo():
|
||||
rows = [
|
||||
{"_row": 2, "分组号": "G1", "申请人": "A", "报销日期": "2026-08-18", "报销标题": "", "费用项目": "x", "金额(元)": 1, "报销事由": "r1", "发生时间": ""},
|
||||
{"_row": 3, "分组号": "G1", "申请人": "A", "报销日期": "2026-08-18", "报销标题": "", "费用项目": "y", "金额(元)": 2, "报销事由": "r2", "发生时间": ""},
|
||||
{"_row": 4, "分组号": "", "申请人": "B", "报销日期": "2026-08-18", "报销标题": "", "费用项目": "z", "金额(元)": 3, "报销事由": "r3", "发生时间": ""},
|
||||
]
|
||||
groups = partition_import_groups(rows)
|
||||
assert len(groups) == 2 # G1 一组 + 空分组号一行一组
|
||||
g1 = next(g for g in groups if g["group_no"] == "G1")
|
||||
assert len(g1["rows"]) == 2
|
||||
solo = next(g for g in groups if g["group_no"] == "")
|
||||
assert len(solo["rows"]) == 1
|
||||
|
||||
|
||||
def test_validate_group_headers_mismatch():
|
||||
rows = [
|
||||
{"_row": 2, "申请人": "A", "报销日期": "2026-08-18", "报销标题": ""},
|
||||
{"_row": 3, "申请人": "B", "报销日期": "2026-08-18", "报销标题": ""},
|
||||
]
|
||||
err = validate_group_headers(rows)
|
||||
assert err and "申请人" in err
|
||||
|
||||
|
||||
def test_validate_group_headers_title_partial_empty():
|
||||
rows = [
|
||||
{"_row": 2, "申请人": "A", "报销日期": "2026-08-18", "报销标题": "有标题"},
|
||||
{"_row": 3, "申请人": "A", "报销日期": "2026-08-18", "报销标题": ""},
|
||||
]
|
||||
err = validate_group_headers(rows)
|
||||
assert err and "标题" in err
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 跑测确认失败**
|
||||
|
||||
```bash
|
||||
cd backend && source venv/bin/activate && pytest tests/test_bx_reimburse_import.py -v
|
||||
```
|
||||
|
||||
Expected: FAIL(模块/函数不存在)
|
||||
|
||||
- [ ] **Step 3: 实现纯函数**
|
||||
|
||||
在 `backend/app/services/bx/reimburse_import.py` 实现:
|
||||
|
||||
```python
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
EXAMPLE_REMARK_PREFIX = "【示例】"
|
||||
PATH_SEP = " / "
|
||||
MAX_IMPORT_ROWS = 500
|
||||
|
||||
HEADERS = [
|
||||
"分组号", "申请人", "报销日期", "报销标题",
|
||||
"费用项目", "金额(元)", "报销事由", "发生时间",
|
||||
]
|
||||
|
||||
|
||||
def build_applicant_label(nick_name: str, user_no: str, user_id: str) -> str:
|
||||
name = (nick_name or user_no or user_id or "").strip() or user_id
|
||||
suffix = (user_no or "").strip() or str(user_id)[-6:]
|
||||
return f"{name}({suffix})"
|
||||
|
||||
|
||||
def build_expense_path_labels(nodes: List[Dict[str, Any]]) -> Dict[str, str]:
|
||||
"""返回 {id: 展示文案},仅 status==1;冲突时追加 (编码:xxx)。"""
|
||||
by_id = {str(n["id"]): n for n in nodes}
|
||||
enabled = [n for n in nodes if int(n.get("status") or 0) == 1]
|
||||
|
||||
def path_of(nid: str) -> str:
|
||||
parts: List[str] = []
|
||||
cur = by_id.get(nid)
|
||||
guard = 0
|
||||
while cur and guard < 20:
|
||||
parts.append(str(cur.get("item_name") or "").strip())
|
||||
pid = str(cur.get("parent_id") or "0")
|
||||
if not pid or pid == "0":
|
||||
break
|
||||
cur = by_id.get(pid)
|
||||
guard += 1
|
||||
return PATH_SEP.join(reversed([p for p in parts if p]))
|
||||
|
||||
raw: Dict[str, str] = {str(n["id"]): path_of(str(n["id"])) for n in enabled}
|
||||
# 冲突消歧
|
||||
path_to_ids: Dict[str, List[str]] = {}
|
||||
for iid, p in raw.items():
|
||||
path_to_ids.setdefault(p, []).append(iid)
|
||||
out: Dict[str, str] = {}
|
||||
for p, ids in path_to_ids.items():
|
||||
if len(ids) == 1:
|
||||
out[ids[0]] = p
|
||||
continue
|
||||
for iid in ids:
|
||||
code = (by_id[iid].get("item_code") or "").strip() or iid[-6:]
|
||||
out[iid] = f"{p}(编码:{code})"
|
||||
return out
|
||||
|
||||
|
||||
def is_example_row(row: Dict[str, Any]) -> bool:
|
||||
remark = str(row.get("报销事由") or "").strip()
|
||||
return remark.startswith(EXAMPLE_REMARK_PREFIX)
|
||||
|
||||
|
||||
def partition_import_groups(rows: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
"""空分组号:每行独立一组(group_no=\"\");非空:同号合并。保持首次出现顺序。"""
|
||||
ordered: List[Dict[str, Any]] = []
|
||||
index: Dict[str, int] = {}
|
||||
for row in rows:
|
||||
g = str(row.get("分组号") or "").strip()
|
||||
if not g:
|
||||
ordered.append({"group_no": "", "rows": [row]})
|
||||
continue
|
||||
if g not in index:
|
||||
index[g] = len(ordered)
|
||||
ordered.append({"group_no": g, "rows": [row]})
|
||||
else:
|
||||
ordered[index[g]]["rows"].append(row)
|
||||
return ordered
|
||||
|
||||
|
||||
def validate_group_headers(rows: List[Dict[str, Any]]) -> Optional[str]:
|
||||
if not rows:
|
||||
return "分组无有效行"
|
||||
apps = {str(r.get("申请人") or "").strip() for r in rows}
|
||||
dates = {str(r.get("报销日期") or "").strip() for r in rows}
|
||||
titles = [str(r.get("报销标题") or "").strip() for r in rows]
|
||||
if len(apps) != 1 or not next(iter(apps)):
|
||||
return "同组申请人不一致或为空"
|
||||
if len(dates) != 1 or not next(iter(dates)):
|
||||
return "同组报销日期不一致或为空"
|
||||
empty_n = sum(1 for t in titles if not t)
|
||||
if 0 < empty_n < len(titles):
|
||||
return "同组报销标题不能部分为空"
|
||||
if empty_n == 0 and len(set(titles)) != 1:
|
||||
return "同组报销标题不一致"
|
||||
return None
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 跑测通过**
|
||||
|
||||
```bash
|
||||
cd backend && source venv/bin/activate && pytest tests/test_bx_reimburse_import.py -v
|
||||
```
|
||||
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 5: Commit(若用户要求提交时再执行)**
|
||||
|
||||
```bash
|
||||
git add backend/app/services/bx/reimburse_import.py backend/tests/test_bx_reimburse_import.py
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(bx): add reimburse import pure helpers for labels and grouping
|
||||
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Excel list validation 工具
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/app/utils/excel_list_validation.py`
|
||||
- Modify: `backend/tests/test_bx_reimburse_import.py`(追加模板下拉存在性测)
|
||||
|
||||
- [ ] **Step 1: 实现工具**
|
||||
|
||||
```python
|
||||
# backend/app/utils/excel_list_validation.py
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Iterable, Sequence
|
||||
|
||||
from openpyxl.utils import get_column_letter
|
||||
from openpyxl.workbook.workbook import Workbook
|
||||
from openpyxl.worksheet.datavalidation import DataValidation
|
||||
from openpyxl.worksheet.worksheet import Worksheet
|
||||
|
||||
|
||||
def write_option_sheet(
|
||||
wb: Workbook,
|
||||
sheet_title: str,
|
||||
labels: Sequence[str],
|
||||
ids: Sequence[str] | None = None,
|
||||
*,
|
||||
hidden: bool = True,
|
||||
) -> str:
|
||||
"""写入选项表:A=label,可选 B=id。返回 sheet_title。"""
|
||||
if sheet_title in wb.sheetnames:
|
||||
ws = wb[sheet_title]
|
||||
wb.remove(ws)
|
||||
ws = wb.create_sheet(sheet_title)
|
||||
ws.append(["label", "id"] if ids is not None else ["label"])
|
||||
for i, lab in enumerate(labels):
|
||||
if ids is not None:
|
||||
ws.append([lab, ids[i] if i < len(ids) else ""])
|
||||
else:
|
||||
ws.append([lab])
|
||||
if hidden:
|
||||
ws.sheet_state = "hidden"
|
||||
return sheet_title
|
||||
|
||||
|
||||
def add_list_validation(
|
||||
ws: Worksheet,
|
||||
*,
|
||||
option_sheet: str,
|
||||
option_rows: int,
|
||||
target_col: int,
|
||||
start_row: int,
|
||||
end_row: int,
|
||||
allow_blank: bool = True,
|
||||
) -> None:
|
||||
"""option_rows 为选项数据行数(不含表头)。option 在 A2:A{1+option_rows}。"""
|
||||
if option_rows <= 0:
|
||||
return
|
||||
last = 1 + option_rows
|
||||
formula = f"='{option_sheet}'!$A$2:$A${last}"
|
||||
col = get_column_letter(target_col)
|
||||
dv = DataValidation(
|
||||
type="list",
|
||||
formula1=formula,
|
||||
allow_blank=allow_blank,
|
||||
showErrorMessage=True,
|
||||
showInputMessage=True,
|
||||
)
|
||||
dv.error = "请从下拉列表选择"
|
||||
dv.errorTitle = "无效值"
|
||||
dv.add(f"{col}{start_row}:{col}{end_row}")
|
||||
ws.add_data_validation(dv)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 最小冒烟测(内存 workbook)**
|
||||
|
||||
在 `test_bx_reimburse_import.py` 追加:
|
||||
|
||||
```python
|
||||
from openpyxl import Workbook
|
||||
from app.utils.excel_list_validation import add_list_validation, write_option_sheet
|
||||
|
||||
|
||||
def test_write_option_sheet_and_validation():
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
ws.title = "报销导入"
|
||||
ws.append(["申请人"])
|
||||
write_option_sheet(wb, "_选项_申请人", ["张三(z001)"], ["u1"], hidden=True)
|
||||
add_list_validation(
|
||||
ws,
|
||||
option_sheet="_选项_申请人",
|
||||
option_rows=1,
|
||||
target_col=1,
|
||||
start_row=2,
|
||||
end_row=100,
|
||||
)
|
||||
assert wb["_选项_申请人"].sheet_state == "hidden"
|
||||
assert len(ws.data_validations.dataValidation) == 1
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 跑测**
|
||||
|
||||
```bash
|
||||
cd backend && source venv/bin/activate && pytest tests/test_bx_reimburse_import.py::test_write_option_sheet_and_validation -v
|
||||
```
|
||||
|
||||
Expected: PASS
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 生成导入模板(含说明 + 示例 + 下拉)
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/app/services/bx/reimburse_import.py`
|
||||
- Modify: `backend/tests/test_bx_reimburse_import.py`
|
||||
|
||||
- [ ] **Step 1: 实现 `build_import_template_bytes`**
|
||||
|
||||
签名与要点:
|
||||
|
||||
```python
|
||||
def build_import_template_bytes(
|
||||
*,
|
||||
expense_nodes: List[Dict[str, Any]],
|
||||
applicants: List[Dict[str, Any]], # id, nick_name, user_no
|
||||
current_applicant_label: str,
|
||||
today: date,
|
||||
) -> bytes:
|
||||
```
|
||||
|
||||
行为:
|
||||
|
||||
1. `Workbook()`;先建 `填写说明`,写入 spec §2.5 九条说明(每条一行)。
|
||||
2. 建 `报销导入`,`append(HEADERS)`。
|
||||
3. `expense_labels = build_expense_path_labels(expense_nodes)`;按 path 排序写入 `_选项_费用项目`(A=label B=id)。
|
||||
4. 申请人 labels:`build_applicant_label(...)`,写入 `_选项_申请人`。
|
||||
5. 示例 3 行:事由以 `【示例】` 开头;申请人=`current_applicant_label`;日期=`today.isoformat()`;费用取选项前 1~2 个 label(无选项则费用列留空并在说明中提示先配置费用项);分组号 G1/G1/空;浅灰底 `PatternFill(start_color="F1F5F9", ...)`。
|
||||
6. 对「申请人」列(B)、「费用项目」列(E)从行 2 到行 501 加 `add_list_validation`。
|
||||
7. `wb.save(BytesIO)` 返回 bytes。
|
||||
|
||||
费用节点加载(供 API 调用,可同文件):
|
||||
|
||||
```python
|
||||
def load_expense_nodes_flat(db: Session, tenant_no: str) -> List[Dict[str, Any]]:
|
||||
rows = (
|
||||
db.query(BxExpenseItem)
|
||||
.filter(BxExpenseItem.del_flag == 0, BxExpenseItem.tenant_no == tenant_no)
|
||||
.order_by(BxExpenseItem.sort_order.asc(), BxExpenseItem.create_time.desc())
|
||||
.all()
|
||||
)
|
||||
return [
|
||||
{
|
||||
"id": r.id,
|
||||
"parent_id": r.parent_id or "0",
|
||||
"item_name": r.item_name,
|
||||
"status": r.status,
|
||||
"item_code": r.item_code,
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
```
|
||||
|
||||
申请人列表:查询 `SysUser`(同 `/users/options` 过滤);若 `not ReimburseService.can_change_applicant(...)` 则仅当前 user。
|
||||
|
||||
- [ ] **Step 2: 单测模板结构(不连库)**
|
||||
|
||||
```python
|
||||
from datetime import date
|
||||
from io import BytesIO
|
||||
from openpyxl import load_workbook
|
||||
from app.services.bx.reimburse_import import build_import_template_bytes
|
||||
|
||||
|
||||
def test_build_import_template_has_guide_examples_and_dv():
|
||||
raw = build_import_template_bytes(
|
||||
expense_nodes=[
|
||||
{"id": "1", "parent_id": "0", "item_name": "差旅费", "status": 1, "item_code": "CL"},
|
||||
{"id": "2", "parent_id": "1", "item_name": "市内交通", "status": 1, "item_code": "JT"},
|
||||
],
|
||||
applicants=[{"id": "u1", "nick_name": "张三", "user_no": "z001"}],
|
||||
current_applicant_label="张三(z001)",
|
||||
today=date(2026, 8, 18),
|
||||
)
|
||||
wb = load_workbook(BytesIO(raw))
|
||||
assert "填写说明" in wb.sheetnames
|
||||
assert "报销导入" in wb.sheetnames
|
||||
assert wb["_选项_费用项目"].sheet_state == "hidden"
|
||||
ws = wb["报销导入"]
|
||||
assert [c.value for c in ws[1]][:8] == [
|
||||
"分组号", "申请人", "报销日期", "报销标题",
|
||||
"费用项目", "金额(元)", "报销事由", "发生时间",
|
||||
]
|
||||
assert str(ws["G2"].value).startswith("【示例】")
|
||||
assert len(ws.data_validations.dataValidation) >= 1
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 跑测**
|
||||
|
||||
```bash
|
||||
cd backend && source venv/bin/activate && pytest tests/test_bx_reimburse_import.py -v
|
||||
```
|
||||
|
||||
Expected: PASS
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 导入解析与按组写库
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/app/services/bx/reimburse_import.py`
|
||||
- Modify: `backend/tests/test_bx_reimburse_import.py`
|
||||
|
||||
- [ ] **Step 1: 实现解析辅助**
|
||||
|
||||
```python
|
||||
def cell_str(v: Any) -> str:
|
||||
if v is None:
|
||||
return ""
|
||||
if isinstance(v, datetime):
|
||||
return v.date().isoformat()
|
||||
if isinstance(v, date):
|
||||
return v.isoformat()
|
||||
return str(v).strip()
|
||||
|
||||
|
||||
def parse_amount(v: Any) -> Tuple[Optional[Decimal], Optional[str]]:
|
||||
if v is None or cell_str(v) == "":
|
||||
return None, "金额不能为空"
|
||||
try:
|
||||
amt = Decimal(str(v).strip())
|
||||
except Exception:
|
||||
return None, "金额格式无效"
|
||||
if amt <= 0:
|
||||
return None, "金额必须大于 0"
|
||||
return amt, None
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 实现 `import_reimburse_workbook`**
|
||||
|
||||
签名:
|
||||
|
||||
```python
|
||||
def import_reimburse_workbook(
|
||||
db: Session,
|
||||
*,
|
||||
file_bytes: bytes,
|
||||
user: BxUserContext,
|
||||
) -> Dict[str, Any]:
|
||||
```
|
||||
|
||||
逻辑:
|
||||
|
||||
1. `load_workbook(BytesIO(file_bytes), data_only=True)`;无 `报销导入` → `raise ValueError("缺少工作表「报销导入」")`。
|
||||
2. 读第 1 行表头,必须包含 `HEADERS` 全部列名(允许顺序与生成模板一致;缺列 raise)。
|
||||
3. 从第 2 行起读到空行结束;转 dict;跳过全空行;`is_example_row` 跳过。
|
||||
4. 有效行数 > `MAX_IMPORT_ROWS` → `raise ValueError(...)`。
|
||||
5. `tenant_no = ReimburseService._tenant_no(db, user)`。
|
||||
6. 构建匹配表:`label→expense_id`(`build_expense_path_labels` + 反查)、`label→user_id`(按权限加载申请人后 `build_applicant_label`)。
|
||||
7. `partition_import_groups`。
|
||||
8. 对每个 group:
|
||||
- `validate_group_headers` 失败 → errors,continue。
|
||||
- 解析每行费用/金额/事由/发生时间;任一行失败 → 整组 errors。
|
||||
- 申请人 label → id;`resolve_applicant`;日期用 `parse_reimburse_date`(注意:组内已保证非空字符串;Excel datetime 先 `cell_str`)。
|
||||
- 发生时间:`parse_optional_date`(先把 cell 规范成 date/str)。
|
||||
- `details_data` 与 API `_build_details_data` 同形:查 `ExpenseItemService.get_by_id`,写 `expense_id/name/amount/remark/occur_date`。
|
||||
- `data = {apply_code, status:"draft", reimburse_date, remark:""}`;若标题非空则 `title=...`;挂 `wf_id` 同 `create_reimburse`。
|
||||
- `create_with_details(...)`;**立即 `db.commit()`**(部分成功);`created` 追加 `{groupNo, applyId, applyCode}`。
|
||||
- 单组异常:`db.rollback()`,记入 errors,继续下一组。
|
||||
9. 返回:
|
||||
|
||||
```python
|
||||
{
|
||||
"successGroups": ...,
|
||||
"successRows": ...,
|
||||
"failGroups": ...,
|
||||
"failRows": ...,
|
||||
"created": [...],
|
||||
"errors": [{"groupNo": ..., "rows": [...], "message": ...}],
|
||||
}
|
||||
```
|
||||
|
||||
空分组号在结果里 `groupNo` 用 `""`。
|
||||
|
||||
- [ ] **Step 3: 纯解析测(不写库)— 可选抽出 `parse_sheet_rows`**
|
||||
|
||||
为便于测,先抽:
|
||||
|
||||
```python
|
||||
def iter_data_rows(ws) -> List[Dict[str, Any]]:
|
||||
...
|
||||
```
|
||||
|
||||
单测:内存 workbook 含示例行 + 数据行,断言示例被跳过、分组正确。
|
||||
|
||||
- [ ] **Step 4: 跑测**
|
||||
|
||||
```bash
|
||||
cd backend && source venv/bin/activate && pytest tests/test_bx_reimburse_import.py -v
|
||||
```
|
||||
|
||||
Expected: PASS(写库可用 mock/`MagicMock` 测分组失败不调用 create,或跳过集成测;至少保证解析/跳过示例/表头校验有测)
|
||||
|
||||
---
|
||||
|
||||
### Task 5: API 路由
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/app/api/bx/reimburse.py`
|
||||
|
||||
- [ ] **Step 1: 增加依赖导入**
|
||||
|
||||
```python
|
||||
import io
|
||||
from urllib.parse import quote
|
||||
|
||||
from fastapi import File, UploadFile
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
from app.services.bx.reimburse_import import (
|
||||
build_applicant_label,
|
||||
build_import_template_bytes,
|
||||
import_reimburse_workbook,
|
||||
load_expense_nodes_flat,
|
||||
)
|
||||
from app.models.models import SysUser
|
||||
from app.services.bx.reimburse import ReimburseService
|
||||
```
|
||||
|
||||
(若 `ReimburseService` 已导入则勿重复。)
|
||||
|
||||
- [ ] **Step 2: 实现 `GET /import-template`**
|
||||
|
||||
注意:放在 `/{reimburse_id}` 类路径**之前**,避免被当成 id。建议紧接在 `generate-title` 附近。
|
||||
|
||||
```python
|
||||
@router.get("/import-template")
|
||||
def download_reimburse_import_template(
|
||||
db: Session = Depends(get_db),
|
||||
user: BxUserContext = Depends(get_bx_current_user),
|
||||
):
|
||||
tenant_no = _tenant(db, user)
|
||||
nodes = load_expense_nodes_flat(db, tenant_no)
|
||||
q = db.query(SysUser).filter(
|
||||
SysUser.del_flag == 0, SysUser.status == "1", SysUser.tenant_no == tenant_no
|
||||
)
|
||||
if not ReimburseService.can_change_applicant(db, user):
|
||||
q = q.filter(SysUser.id == user.id)
|
||||
users = q.order_by(SysUser.create_time.desc()).all()
|
||||
applicants = [
|
||||
{"id": u.id, "nick_name": u.nick_name or "", "user_no": u.user_no or ""}
|
||||
for u in users
|
||||
]
|
||||
# 当前用户展示名
|
||||
me = next((a for a in applicants if a["id"] == user.id), None)
|
||||
if me:
|
||||
cur_label = build_applicant_label(me["nick_name"], me["user_no"], me["id"])
|
||||
else:
|
||||
cur_label = build_applicant_label(user.name or "", "", user.id)
|
||||
applicants = [{"id": user.id, "nick_name": user.name or "", "user_no": ""}] + applicants
|
||||
from datetime import date as date_cls
|
||||
raw = build_import_template_bytes(
|
||||
expense_nodes=nodes,
|
||||
applicants=applicants,
|
||||
current_applicant_label=cur_label,
|
||||
today=date_cls.today(),
|
||||
)
|
||||
filename = quote("报销申请导入模板.xlsx")
|
||||
return StreamingResponse(
|
||||
io.BytesIO(raw),
|
||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
headers={
|
||||
"Content-Disposition": f"attachment; filename={filename}; filename*=UTF-8''{filename}"
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 实现 `POST /import`**
|
||||
|
||||
```python
|
||||
@router.post("/import")
|
||||
def import_reimburses(
|
||||
file: UploadFile = File(...),
|
||||
db: Session = Depends(get_db),
|
||||
user: BxUserContext = Depends(get_bx_current_user),
|
||||
):
|
||||
content = file.file.read()
|
||||
if not content:
|
||||
raise HTTPException(status_code=400, detail="文件为空")
|
||||
try:
|
||||
result = import_reimburse_workbook(db, file_bytes=content, user=user)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
return ApiResponse(data=result)
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 手工冒烟(本地服务已启动时)**
|
||||
|
||||
```bash
|
||||
# 登录后带 token
|
||||
curl -o /tmp/bx-tpl.xlsx -H "Authorization: Bearer $TOKEN" \
|
||||
http://127.0.0.1:8000/api/reimburses/import-template
|
||||
file /tmp/bx-tpl.xlsx
|
||||
```
|
||||
|
||||
Expected: Excel 文件;用 Excel/WPS 打开可见下拉与说明。
|
||||
|
||||
---
|
||||
|
||||
### Task 6: 前端 API
|
||||
|
||||
**Files:**
|
||||
- Modify: `web-admin/src/api/bx/index.ts`
|
||||
|
||||
- [ ] **Step 1: 在 `reimburseApi` 增加方法**
|
||||
|
||||
```typescript
|
||||
downloadImportTemplate() {
|
||||
return bxRequest.get('/reimburses/import-template', { responseType: 'blob' })
|
||||
},
|
||||
importExcel(file: File) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
return bxRequest.post('/reimburses/import', formData)
|
||||
},
|
||||
```
|
||||
|
||||
(`bx.ts` 已对 FormData 去掉 Content-Type,勿强制 `multipart/form-data` 字符串以免丢 boundary。)
|
||||
|
||||
---
|
||||
|
||||
### Task 7: 前端工具栏 + 导入弹窗
|
||||
|
||||
**Files:**
|
||||
- Modify: `web-admin/src/views/bx/reimburse/apply/index.vue`
|
||||
|
||||
- [ ] **Step 1: 工具栏按钮**
|
||||
|
||||
在「新增报销申请」按钮后插入(`btn-ghost`,带图标):
|
||||
|
||||
```vue
|
||||
<el-button class="btn-ghost" :loading="templateLoading" @click="downloadImportTemplate">
|
||||
<el-icon><Download /></el-icon>下载模板
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" @click="openImportDialog">
|
||||
<el-icon><Upload /></el-icon>批量导入
|
||||
</el-button>
|
||||
```
|
||||
|
||||
注意:页面已有「提交审批」用 `Upload` 图标;批量导入可改用 `UploadFilled` 或 `DocumentAdd`,从 `@element-plus/icons-vue` 显式 import,避免两按钮同图标。推荐:`Download` + `FolderAdd`。
|
||||
|
||||
- [ ] **Step 2: 导入弹窗模板**
|
||||
|
||||
```vue
|
||||
<el-dialog
|
||||
v-model="importDialogVisible"
|
||||
title="批量导入报销申请"
|
||||
width="560px"
|
||||
class="dialog-form-layout"
|
||||
align-center
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<p class="import-hint">
|
||||
请先下载最新模板填写。灰色【示例】行导入时自动跳过。导入结果为「待提交」草稿,发票请事后在系统中关联。
|
||||
</p>
|
||||
<el-upload
|
||||
drag
|
||||
:auto-upload="false"
|
||||
:limit="1"
|
||||
accept=".xlsx,.xls"
|
||||
:file-list="importFileList"
|
||||
:on-change="onImportFileChange"
|
||||
:on-remove="onImportFileRemove"
|
||||
>
|
||||
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
|
||||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||||
</el-upload>
|
||||
<div v-if="importResult" class="import-result">
|
||||
<p>
|
||||
成功 {{ importResult.successGroups }} 组({{ importResult.successRows }} 行),
|
||||
失败 {{ importResult.failGroups }} 组({{ importResult.failRows }} 行)
|
||||
</p>
|
||||
<el-table v-if="importResult.errors?.length" :data="importResult.errors" size="small" max-height="240">
|
||||
<el-table-column prop="groupNo" label="分组号" min-width="80" />
|
||||
<el-table-column label="行号" min-width="100">
|
||||
<template #default="{ row }">{{ (row.rows || []).join(', ') }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="message" label="原因" min-width="200" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="importDialogVisible = false">
|
||||
<el-icon><Close /></el-icon>关闭
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-main"
|
||||
type="primary"
|
||||
:loading="importLoading"
|
||||
:disabled="!importFileList.length"
|
||||
@click="submitImport"
|
||||
>
|
||||
<el-icon><Check /></el-icon>开始导入
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
```
|
||||
|
||||
- [ ] **Step 3: script 逻辑**
|
||||
|
||||
```typescript
|
||||
const templateLoading = ref(false)
|
||||
const importDialogVisible = ref(false)
|
||||
const importLoading = ref(false)
|
||||
const importFileList = ref<UploadFile[]>([])
|
||||
const importResult = ref<any>(null)
|
||||
|
||||
async function downloadImportTemplate() {
|
||||
templateLoading.value = true
|
||||
try {
|
||||
const res: any = await reimburseApi.downloadImportTemplate()
|
||||
const blob =
|
||||
res instanceof Blob
|
||||
? res
|
||||
: new Blob([res as BlobPart], {
|
||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
})
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = '报销申请导入模板.xlsx'
|
||||
link.click()
|
||||
URL.revokeObjectURL(url)
|
||||
} catch {
|
||||
ElMessage.error('下载模板失败')
|
||||
} finally {
|
||||
templateLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openImportDialog() {
|
||||
importFileList.value = []
|
||||
importResult.value = null
|
||||
importDialogVisible.value = true
|
||||
}
|
||||
|
||||
function onImportFileChange(file: UploadFile, files: UploadFile[]) {
|
||||
importFileList.value = files.slice(-1)
|
||||
}
|
||||
|
||||
function onImportFileRemove() {
|
||||
importFileList.value = []
|
||||
}
|
||||
|
||||
async function submitImport() {
|
||||
const raw = importFileList.value[0]?.raw
|
||||
if (!raw) {
|
||||
ElMessage.warning('请选择要导入的文件')
|
||||
return
|
||||
}
|
||||
importLoading.value = true
|
||||
importResult.value = null
|
||||
try {
|
||||
const res: any = await reimburseApi.importExcel(raw)
|
||||
const data = pickBxData(res) || res?.data || res
|
||||
importResult.value = data
|
||||
const ok = Number(data?.successGroups || 0)
|
||||
const fail = Number(data?.failGroups || 0)
|
||||
if (ok > 0 && fail === 0) {
|
||||
ElMessage.success(`成功导入 ${ok} 组`)
|
||||
} else if (ok > 0) {
|
||||
ElMessage.warning(`部分成功:成功 ${ok} 组,失败 ${fail} 组`)
|
||||
} else {
|
||||
ElMessage.error('导入失败,请查看错误清单')
|
||||
}
|
||||
if (ok > 0) await refreshList()
|
||||
} finally {
|
||||
importLoading.value = false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
补全 `UploadFile`、`UploadFilled`、`Download`、`FolderAdd`、`Close`、`Check` 的 import;`refreshList` 用页面已有 `refresh`/`search`。
|
||||
|
||||
- [ ] **Step 4: 手工验收(对照 spec §5.2)**
|
||||
|
||||
1. 打开 `/#/bx/reimburse/apply`,下载模板 → 有说明、示例、下拉。
|
||||
2. 不改示例直接导入 → 0 成功(或仅跳过),无脏数据。
|
||||
3. 复制示例改事由去掉【示例】、金额合理 → 成功草稿。
|
||||
4. G1 两行同头不同费用 → 一单两明细。
|
||||
5. 同组申请人故意不一致 → 该组失败,其它组仍成功。
|
||||
|
||||
---
|
||||
|
||||
### Task 8: 收尾
|
||||
|
||||
- [ ] **Step 1: 后端全量相关测**
|
||||
|
||||
```bash
|
||||
cd backend && source venv/bin/activate && pytest tests/test_bx_reimburse_import.py -v
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 更新图谱(改代码后)**
|
||||
|
||||
```bash
|
||||
./scripts/update-graph.sh backend
|
||||
# 若改了 web-admin:
|
||||
./scripts/update-graph.sh frontend
|
||||
```
|
||||
|
||||
或按仓库规则:`graphify update .`(在对应目录)。
|
||||
|
||||
- [ ] **Step 3: 将 spec 状态改为「已实现」**(实现全部验收后)
|
||||
|
||||
修改 `docs/superpowers/specs/2026-08-18-reimburse-apply-batch-import-design.md` 顶部状态行。
|
||||
|
||||
---
|
||||
|
||||
## Spec 覆盖自检
|
||||
|
||||
| Spec 项 | Task |
|
||||
|---------|------|
|
||||
| D1 后端模板+导入 | 2–5 |
|
||||
| D2 分组号行模型 | 1, 4 |
|
||||
| D3 单头一致 | 1 `validate_group_headers`, 4 |
|
||||
| D4 空分组号 | 1 `partition_import_groups` |
|
||||
| D5 仅草稿 | 4 `status: draft` |
|
||||
| D6 标题可空 | 1, 4 |
|
||||
| D7 启用费用项含父级 | 1 `build_expense_path_labels` |
|
||||
| D8 部分成功 | 4 按组 commit |
|
||||
| D9 无发票 | 4 不传 invoice |
|
||||
| D10 说明+示例 | 3 |
|
||||
| API GET/POST | 5 |
|
||||
| 前端按钮+弹窗 | 6–7 |
|
||||
| 申请人权限 | 3 模板选项 + 4 `resolve_applicant` |
|
||||
|
||||
**占位符扫描:** 无 TBD;Vue 需与页面现有 `refresh` 函数名对齐(实现时搜 `refresh`/`handleSearch`)。
|
||||
|
||||
**类型一致:** `groupNo` / `successGroups` / `errors[].rows` 前后端统一驼峰(Pydantic/`ApiResponse` 若默认 camelCase,service 返回可用 dict 驼峰键,与现有 bx 出参习惯一致)。
|
||||
@@ -0,0 +1,297 @@
|
||||
# 报销申请批量导入(Excel 下拉模板)— 设计文档
|
||||
|
||||
> **日期**: 2026-08-18
|
||||
> **状态**: 已实现(待联调验收)
|
||||
> **仓库**: shanxia2.0(`backend/` + `web-admin/`)
|
||||
> **入口页**: `http://localhost:3006/#/bx/reimburse/apply`
|
||||
> **界面模式**: P01 标准列表扩展(工具栏 + 导入弹窗);参考 Fresh Mint 工具栏按钮规则
|
||||
> **实现路径**: 方案 A — 后端生成模板 + 后端导入
|
||||
|
||||
## 背景与问题
|
||||
|
||||
报销申请页已支持新增 / 编辑 / 删除。用户希望增加**批量导入**,并特别要求:
|
||||
|
||||
1. **费用项目**、**申请人**在 Excel 中为下拉框(非自由文本)。
|
||||
2. 可**下载模板**,模板内**内置下拉数据**,并带**填写说明与示例行**,便于理解。
|
||||
|
||||
现状难点:
|
||||
|
||||
- 费用项目在页面是树形选择;Excel 只能做扁平下拉。
|
||||
- 申请人选项接口 label 仅为昵称,易重名。
|
||||
- 一单多明细;导入行模型需显式分组。
|
||||
- 仓库内 CRM「下载模板」仅有表头,**无** DataValidation 下拉;本需求需新增能力。
|
||||
|
||||
## 已确认决策
|
||||
|
||||
| # | 决策 | 说明 |
|
||||
|---|------|------|
|
||||
| D1 | **实现路径 A** | 后端 `openpyxl` 动态生成模板(隐藏选项表 + DataValidation);后端解析导入;前端只做下载/上传与结果展示 |
|
||||
| D2 | **行模型 B** | Excel **一行 = 一条明细**;相同「分组号」合成一张报销单 |
|
||||
| D3 | **单头一致性** | 同组每一行都须填写申请人 / 报销日期 / 标题(标题可空),且**必须完全一致**;不一致则**整组失败** |
|
||||
| D4 | **空分组号** | 允许为空;空则该行**单独成一张单** |
|
||||
| D5 | **仅草稿** | 导入成功写入「待提交」草稿;不自动提交审批 |
|
||||
| D6 | **标题可空** | 同组标题都空 → 按现有规则自动生成;若填写则同组必须相同 |
|
||||
| D7 | **费用项范围** | 下拉含当前租户全部**启用**节点(含一级父节点),与页面 `check-strictly` 一致 |
|
||||
| D8 | **部分成功** | 合法分组写入;失败组/行返回错误清单;不因单组失败回滚整文件 |
|
||||
| D9 | **不做发票/附件** | v1 不导入关联发票与附件;用户可事后编辑草稿补票 |
|
||||
| D10 | **模板含说明+示例** | 「填写说明」工作表 + 数据表内示例行(导入时跳过) |
|
||||
|
||||
---
|
||||
|
||||
## 一、目标与范围
|
||||
|
||||
### 目标
|
||||
|
||||
在报销申请列表提供「下载模板」「批量导入」,使用户能通过带下拉的 Excel 批量创建多张(含多明细)草稿报销单。
|
||||
|
||||
### 范围内
|
||||
|
||||
- 后端:导入模板生成、导入解析/分组/校验/写库、结果结构
|
||||
- Web:`#/bx/reimburse/apply` 工具栏按钮 + 导入结果弹窗
|
||||
- 费用项目全路径下拉、申请人唯一展示名下拉
|
||||
- 与现有 `create_with_details` / `resolve_applicant` 对齐
|
||||
|
||||
### 范围外(v1)
|
||||
|
||||
- 发票、附件导入
|
||||
- 导入后自动提交审批
|
||||
- 按单号更新已有单据(仅新建)
|
||||
- Excel 级联下拉(`INDIRECT`)
|
||||
- 前端本地解析后循环调用单条创建 API
|
||||
- `bx-mobile` / 小程序导入
|
||||
|
||||
---
|
||||
|
||||
## 二、Excel 模板形态
|
||||
|
||||
### 2.1 工作表
|
||||
|
||||
| 工作表 | 可见性 | 用途 |
|
||||
|--------|--------|------|
|
||||
| `报销导入` | 可见 | 用户填写;含表头、示例行、数据验证下拉 |
|
||||
| `填写说明` | 可见 | 规则说明、列说明、常见错误 |
|
||||
| `_选项_费用项目` | 隐藏 | A 列:下拉展示文案;B 列:id(内部) |
|
||||
| `_选项_申请人` | 隐藏 | A 列:下拉展示文案;B 列:userId(内部) |
|
||||
|
||||
生成时按**当前登录用户**与租户实时查询选项,不使用静态 xlsx 文件。
|
||||
|
||||
### 2.2 `报销导入` 列定义
|
||||
|
||||
| 列名 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| 分组号 | 否 | 相同非空值合成一张单;空 = 该行单独成单 |
|
||||
| 申请人 | 是 | 下拉;展示 `昵称(工号)`(见 2.4) |
|
||||
| 报销日期 | 是 | `YYYY-MM-DD`;同组必须一致 |
|
||||
| 报销标题 | 否 | 同组都空则自动生成;填写则同组必须相同 |
|
||||
| 费用项目 | 是 | 下拉;全路径,分隔符 ` / ` |
|
||||
| 金额(元) | 是 | 数值且 > 0 |
|
||||
| 报销事由 | 是 | 对应明细 `remark` |
|
||||
| 发生时间 | 否 | `YYYY-MM-DD`;对应明细 `occur_date` |
|
||||
|
||||
### 2.3 下拉实现(openpyxl)
|
||||
|
||||
- 使用 `DataValidation(type="list")`,`formula1` 引用隐藏表列区域(如 `=_选项_费用项目!$A$2:$A$N`)。
|
||||
- **禁止**把选项拼进公式字符串(255 字符限制)。
|
||||
- `showErrorMessage=True`,提示「请从下拉列表选择」。
|
||||
- 下拉仅为 UX;导入以服务端当前库精确匹配为准。
|
||||
|
||||
### 2.4 展示文案与匹配键
|
||||
|
||||
**费用项目**
|
||||
|
||||
- 展示:祖先全路径,分隔符为 ` / `(半角斜杠两侧各一空格),例如 `差旅费 / 市内交通`;一级节点仅本级名称。
|
||||
- 仅 `status=启用` 的节点进入选项表。
|
||||
- 若规范化后全路径冲突:在文案末尾追加 `(编码:xxx)` 或短 id 后缀保证唯一;导入匹配同一套规则。
|
||||
- 匹配:导入单元格文本 trim 后与选项 A 列精确相等 → 得到 id;失败则该行错误。
|
||||
|
||||
**申请人**
|
||||
|
||||
- 展示:`{nickName}({userNo})`;`userNo` 为空时用稳定后缀(如用户 id 后 6 位)保证唯一。
|
||||
- 有 `bx:reimburseApply:changeApplicant` 或 `super_admin`:选项 = 本租户全部有效用户。
|
||||
- 无权限:选项仅当前用户;导入仍走 `ReimburseService.resolve_applicant`(无权限强制本人)。
|
||||
- 匹配:精确匹配展示文案 → userId。
|
||||
|
||||
### 2.5 填写说明与示例(D10)
|
||||
|
||||
**`填写说明` 工作表**(要点,实现时可微调文案):
|
||||
|
||||
1. 请使用本系统「下载模板」获取最新文件;费用项/人员变更后请重新下载。
|
||||
2. 推荐 Excel 或 WPS **桌面版**;在线表格可能丢失下拉。
|
||||
3. **一行一条费用明细**;相同「分组号」合成一张报销单。
|
||||
4. 分组号可空:空则该行单独成单。
|
||||
5. 同一分组内:申请人、报销日期、报销标题必须每行都填且完全一致(标题可都留空以自动生成)。
|
||||
6. 费用项目、申请人请用下拉选择,勿手改名称。
|
||||
7. 灰色**示例行仅供参考,导入时自动跳过**,请从示例下方空行开始填写。
|
||||
8. 导入结果为「待提交」草稿;发票请导入后在系统内关联。
|
||||
9. 请勿修改或删除隐藏的 `_选项_*` 工作表。
|
||||
|
||||
**`报销导入` 示例行**(建议 2~3 行,浅灰底 +「示例」标识,导入跳过):
|
||||
|
||||
| 分组号 | 申请人 | 报销日期 | 报销标题 | 费用项目 | 金额 | 报销事由 | 发生时间 |
|
||||
|--------|--------|----------|----------|----------|------|----------|----------|
|
||||
| G1 | (当前用户展示名) | (今日) | | (选项表第一项路径) | 50.00 | 【示例】地铁通勤,请删除本行 | |
|
||||
| G1 | (同上) | (同上) | | (选项表第二项或同第一项) | 300.00 | 【示例】住宿,请删除本行 | (昨日) |
|
||||
| | (同上) | (同上) | | (任选项) | 20.00 | 【示例】空分组号单独成单 | |
|
||||
|
||||
示例行识别规则(按优先级):
|
||||
|
||||
1. 首列或隐藏标记列含 `__EXAMPLE__` / 约定标记;或
|
||||
2. 「报销事由」以 `【示例】` 开头;或
|
||||
3. 生成时写入的固定行号范围(模板内注释说明)。
|
||||
|
||||
推荐采用 **事由前缀 `【示例】` + 行底色**,解析简单、用户可见。
|
||||
|
||||
---
|
||||
|
||||
## 三、导入校验与写库
|
||||
|
||||
### 3.1 流程
|
||||
|
||||
```
|
||||
上传 xlsx
|
||||
→ 读「报销导入」表,校验表头
|
||||
→ 跳过空行、示例行
|
||||
→ 按分组号分桶(空分组号 → 每行独立桶)
|
||||
→ 对每个分组:
|
||||
校验单头一致(申请人/日期/标题)
|
||||
解析申请人、费用项、金额、事由、发生时间
|
||||
任一行失败 → 整组失败,不写库
|
||||
全部通过 → create_with_details(draft)
|
||||
→ 汇总 success / errors 返回
|
||||
```
|
||||
|
||||
### 3.2 分组单头规则(D3 / D6)
|
||||
|
||||
对分组内所有数据行:
|
||||
|
||||
- `申请人`、`报销日期` 字符串 trim 后必须完全一致。
|
||||
- `报销标题`:要么全部为空(自动生成),要么全部非空且文本完全一致;**不允许**组内「有的空有的不空」。
|
||||
- 不一致 → 整组错误,例如:`同组申请人不一致` / `同组报销标题不能部分为空`。
|
||||
|
||||
### 3.3 字段校验
|
||||
|
||||
| 字段 | 规则 |
|
||||
|------|------|
|
||||
| 申请人 | 选项匹配成功;再 `resolve_applicant` |
|
||||
| 报销日期 | 可解析为日期;建议规范为 `YYYY-MM-DD` |
|
||||
| 费用项目 | 选项匹配且仍为启用 |
|
||||
| 金额 | 数值且 > 0 |
|
||||
| 报销事由 | 非空 trim |
|
||||
| 发生时间 | 空或合法日期 |
|
||||
|
||||
### 3.4 写库
|
||||
|
||||
- 复用 `ReimburseService.create_with_details`(及 API 层 `_build_details_data` 同类逻辑)。
|
||||
- 状态:待提交草稿(与页面「保存草稿」一致)。
|
||||
- 不调用 submit;不写 `invoice_ids` / `invoice_links`。
|
||||
- `create_by` = 当前操作人;`applicant_*` = 解析后的申请人。
|
||||
- **部分成功(D8)**:成功组 commit(或按组 flush+commit 策略保证已成功组不因后续失败回滚);失败组仅进 `errors`。
|
||||
- 建议上限:单次最多 **500** 行有效数据(可常量配置);超出返回 400 提示拆分。
|
||||
|
||||
### 3.5 错误与结果结构
|
||||
|
||||
整文件无法解析(非 xlsx、缺必要表头)→ HTTP 400。
|
||||
|
||||
可解析时业务成功响应(示意):
|
||||
|
||||
```json
|
||||
{
|
||||
"successGroups": 2,
|
||||
"successRows": 3,
|
||||
"failGroups": 1,
|
||||
"failRows": 2,
|
||||
"created": [
|
||||
{ "groupNo": "G1", "applyId": "...", "applyCode": "..." }
|
||||
],
|
||||
"errors": [
|
||||
{ "groupNo": "G2", "rows": [5, 6], "message": "同组申请人不一致" },
|
||||
{ "groupNo": "", "rows": [8], "message": "费用项目「xxx」不存在或已停用" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`rows` 为 Excel 行号(含表头时从 1 计,与用户所见一致;实现时在说明中写明)。
|
||||
|
||||
---
|
||||
|
||||
## 四、API 与前端
|
||||
|
||||
### 4.1 后端 API
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/api/reimburses/import-template` | 下载模板(StreamingResponse xlsx) |
|
||||
| POST | `/api/reimburses/import` | `multipart` 文件字段(如 `file`) |
|
||||
|
||||
- 认证:与报销申请其它接口相同(`get_bx_current_user`)。
|
||||
- 菜单权限:与报销申请列表入口一致;不新开「导入」权限码(若产品后续要单独控权可再加)。
|
||||
- 改申请人:不新增权限码,沿用 `bx:reimburseApply:changeApplicant`。
|
||||
|
||||
落点建议:
|
||||
|
||||
- 路由:`backend/app/api/bx/reimburse.py`
|
||||
- 服务:`backend/app/services/bx/reimburse_import.py`(或同目录等价命名)
|
||||
- Excel 工具:可抽 `backend/app/utils/excel_data_validation.py`(隐藏表 + list 验证),供后续其它导入复用
|
||||
|
||||
### 4.2 前端
|
||||
|
||||
页面:`web-admin/src/views/bx/reimburse/apply/index.vue`
|
||||
|
||||
工具栏(「新增报销申请」之后,**中性描边** `btn-ghost`):
|
||||
|
||||
1. **下载模板**
|
||||
2. **批量导入**
|
||||
|
||||
导入弹窗:
|
||||
|
||||
- 选择 `.xlsx` / `.xls`
|
||||
- 文案提示:请先下载最新模板;示例行自动跳过;结果为待提交草稿
|
||||
- 上传后展示成功组数/行数、失败清单(分组号、行号、原因)
|
||||
- 关闭后刷新列表
|
||||
|
||||
API 封装:`web-admin/src/api/bx`(或现有 `reimburseApi`)增加 `downloadImportTemplate`、`importExcel`。
|
||||
|
||||
**不做**:用 `ArtExcelImport` 前端解析后循环 `create`。
|
||||
|
||||
UI 约束:无遮罩模糊;按钮带图标;符合 Fresh Mint「仅首主按钮用主色」。
|
||||
|
||||
---
|
||||
|
||||
## 五、风险与验收
|
||||
|
||||
### 5.1 风险对策
|
||||
|
||||
| 风险 | 对策 |
|
||||
|------|------|
|
||||
| 下拉被绕过 | 服务端精确匹配当前库 |
|
||||
| 昵称重名 | `昵称(工号)` 唯一展示 |
|
||||
| 费用项同名 | 全路径;冲突加后缀 |
|
||||
| 旧模板 | 说明要求重新下载;导入以当前主数据为准 |
|
||||
| 无权限改申请人 | `resolve_applicant` 强制本人 |
|
||||
| 示例入库 | `【示例】` 前缀跳过 |
|
||||
| WPS/在线表丢失验证 | 说明推荐桌面版;服务端仍校验 |
|
||||
|
||||
### 5.2 验收标准
|
||||
|
||||
1. 可下载模板:含填写说明、灰色示例行、费用项与申请人下拉。
|
||||
2. 示例行不入库。
|
||||
3. 相同分组号且单头一致 → 一张多明细草稿。
|
||||
4. 空分组号 → 每行一张单。
|
||||
5. 同组单头不一致或主数据无法匹配 → 该组失败,其它组合法写入。
|
||||
6. 无改申请人权限时,入库申请人仍为操作人本人。
|
||||
7. 状态为待提交;不关联发票、不自动提交。
|
||||
8. 工具栏按钮样式符合 Fresh Mint。
|
||||
|
||||
---
|
||||
|
||||
## 六、实现落点预览
|
||||
|
||||
| 层 | 路径 |
|
||||
|----|------|
|
||||
| API | `backend/app/api/bx/reimburse.py` |
|
||||
| Service | `backend/app/services/bx/reimburse_import.py` |
|
||||
| Excel 工具 | `backend/app/utils/`(DataValidation 辅助) |
|
||||
| 前端页 | `web-admin/src/views/bx/reimburse/apply/index.vue` |
|
||||
| 前端 API | `web-admin/src/api/bx`(reimburse 相关) |
|
||||
|
||||
下一步:经用户确认本 spec 后,使用 writing-plans 产出实现计划(`docs/superpowers/plans/`)。
|
||||
+4750
-990
File diff suppressed because it is too large.
Load diff
@@ -191,40 +191,36 @@
|
||||
"189": "Community 189",
|
||||
"190": "error-handle.ts",
|
||||
"191": "formatYmd",
|
||||
"192": "loadDefaultCustomerOptions",
|
||||
"193": "fetchData",
|
||||
"194": "fetchData",
|
||||
"192": "prepareLineAliasAndBrandForSubmit",
|
||||
"193": "crm-product-display.ts",
|
||||
"194": "loadDefaultCustomerOptions",
|
||||
"195": "Community 195",
|
||||
"196": "Community 196",
|
||||
"197": "handleEdit",
|
||||
"198": "fetchData",
|
||||
"199": "fetchData",
|
||||
"199": "price-letter-template.ts",
|
||||
"200": "index.js",
|
||||
"201": "fetchCustomerDetail",
|
||||
"201": "contractType.ts",
|
||||
"202": "Community 202",
|
||||
"203": "menuFavorite.ts",
|
||||
"203": "price-letter.ts",
|
||||
"204": "fetchData",
|
||||
"205": "index.vue",
|
||||
"206": "price-letter.ts",
|
||||
"205": "menuFavorite.ts",
|
||||
"206": "index.vue",
|
||||
"207": "emit",
|
||||
"208": "handleSearch",
|
||||
"208": "v2.5.7 - 2025-09-14",
|
||||
"209": "Community 209",
|
||||
"210": "Community 210",
|
||||
"211": "loadUserOptions",
|
||||
"212": "index.js",
|
||||
"213": "Community 213",
|
||||
"214": "Community 214",
|
||||
"215": "handleDelete",
|
||||
"216": "handleSearch",
|
||||
"215": "index.vue",
|
||||
"216": "zh_CN.js",
|
||||
"217": "index.js",
|
||||
"218": "index.js",
|
||||
"219": "handleAdd",
|
||||
"220": "index.vue",
|
||||
"221": "getInventoryStatCellValue",
|
||||
"222": "handleEdit",
|
||||
"223": "Community 223",
|
||||
"224": "Community 224",
|
||||
"225": "zh_CN.js",
|
||||
"226": "form.vue",
|
||||
"227": "Community 227",
|
||||
"228": "Community 228",
|
||||
|
||||
@@ -1 +1 @@
|
||||
.
|
||||
/Users/wuzilong/work/00code/shanxia2.0/web-admin
|
||||
@@ -0,0 +1,801 @@
|
||||
{
|
||||
"0": "Community 0",
|
||||
"1": "Community 1",
|
||||
"2": "Community 2",
|
||||
"3": "Community 3",
|
||||
"4": "Community 4",
|
||||
"5": "Community 5",
|
||||
"6": "Community 6",
|
||||
"7": "Community 7",
|
||||
"8": "Community 8",
|
||||
"9": "Community 9",
|
||||
"10": "Community 10",
|
||||
"11": "Community 11",
|
||||
"12": "Community 12",
|
||||
"13": "Community 13",
|
||||
"14": "Community 14",
|
||||
"15": "Community 15",
|
||||
"16": "Community 16",
|
||||
"17": "Ha",
|
||||
"18": "Community 18",
|
||||
"19": "Community 19",
|
||||
"20": "Community 20",
|
||||
"21": "Community 21",
|
||||
"22": "Community 22",
|
||||
"23": "Community 23",
|
||||
"24": "Community 24",
|
||||
"25": "Community 25",
|
||||
"26": "theme.min.js",
|
||||
"27": "Community 27",
|
||||
"28": "useCommon",
|
||||
"29": "Community 29",
|
||||
"30": "loader.ts",
|
||||
"31": "RenewDialog.vue",
|
||||
"32": "index.vue",
|
||||
"33": "Community 33",
|
||||
"34": "main.ts",
|
||||
"35": "Community 35",
|
||||
"36": "VoidDialog.vue",
|
||||
"37": "pl-batch-print.ts",
|
||||
"38": "view-fields.ts",
|
||||
"39": "Community 39",
|
||||
"40": "Community 40",
|
||||
"41": "colors.ts",
|
||||
"42": "Community 42",
|
||||
"43": "Community 43",
|
||||
"44": "Community 44",
|
||||
"45": "Community 45",
|
||||
"46": "Community 46",
|
||||
"47": "fetchCustomerDetail",
|
||||
"48": "Community 48",
|
||||
"49": "Community 49",
|
||||
"50": "Community 50",
|
||||
"51": "price-letter-template.ts",
|
||||
"52": "Community 52",
|
||||
"53": "Community 53",
|
||||
"54": "Community 54",
|
||||
"55": "Community 55",
|
||||
"56": "Community 56",
|
||||
"57": "Community 57",
|
||||
"58": "Community 58",
|
||||
"59": "fetchData",
|
||||
"60": "index.vue",
|
||||
"61": "Community 61",
|
||||
"62": "highlight.ts",
|
||||
"63": "price-letter.ts",
|
||||
"64": "Community 64",
|
||||
"65": "detail.vue",
|
||||
"66": "Community 66",
|
||||
"67": "index.vue",
|
||||
"68": "Community 68",
|
||||
"69": "Community 69",
|
||||
"70": "Community 70",
|
||||
"71": "Community 71",
|
||||
"72": "Community 72",
|
||||
"73": "Community 73",
|
||||
"74": "Community 74",
|
||||
"75": "openEdit",
|
||||
"76": "handleSearch",
|
||||
"77": "loadUserOptions",
|
||||
"78": "customerPool.ts",
|
||||
"79": "Community 79",
|
||||
"80": "product.ts",
|
||||
"81": "Community 81",
|
||||
"82": "Community 82",
|
||||
"83": "useDictStore",
|
||||
"84": "Community 84",
|
||||
"85": "index.vue",
|
||||
"86": "Community 86",
|
||||
"87": "canDeleteByCreator",
|
||||
"88": "index.vue",
|
||||
"89": "detail.vue",
|
||||
"90": "theme.min.js",
|
||||
"91": "Community 91",
|
||||
"92": "Community 92",
|
||||
"93": "ArtUserMenu.vue",
|
||||
"94": "formatYmd",
|
||||
"95": "Community 95",
|
||||
"96": "Community 96",
|
||||
"97": "stringifyBody",
|
||||
"98": "index.vue",
|
||||
"99": "stringifyBody",
|
||||
"100": "v2.4.2.1 - 2025-06-16",
|
||||
"101": "price-letter.ts",
|
||||
"102": "price-letter-template.ts",
|
||||
"103": "SettingDrawer.vue",
|
||||
"104": "contractType.ts",
|
||||
"105": "index.vue",
|
||||
"106": "Community 106",
|
||||
"107": "Community 107",
|
||||
"108": "Community 108",
|
||||
"109": "Community 109",
|
||||
"110": "Community 110",
|
||||
"111": "format.ts",
|
||||
"112": "inventory.ts",
|
||||
"113": "Community 113",
|
||||
"114": "index.vue",
|
||||
"115": "useDictStore",
|
||||
"116": "Community 116",
|
||||
"117": "Community 117",
|
||||
"118": "zh_CN.js",
|
||||
"119": "Community 119",
|
||||
"120": "Community 120",
|
||||
"121": "chart.ts",
|
||||
"122": "handleDisburse",
|
||||
"123": "detail.vue",
|
||||
"124": "index.vue",
|
||||
"125": "Community 125",
|
||||
"126": "Community 126",
|
||||
"127": "selectContractRow",
|
||||
"128": "Community 128",
|
||||
"129": "Community 129",
|
||||
"130": "system.ts",
|
||||
"131": "index.vue",
|
||||
"132": "Community 132",
|
||||
"133": "Community 133",
|
||||
"134": "index.vue",
|
||||
"135": "detail.vue",
|
||||
"136": "Community 136",
|
||||
"137": "Community 137",
|
||||
"138": "FollowDialog.vue",
|
||||
"139": "useTableColumns.ts",
|
||||
"140": "Community 140",
|
||||
"141": "Community 141",
|
||||
"142": "Community 142",
|
||||
"143": "index.js",
|
||||
"144": "Community 144",
|
||||
"145": "api",
|
||||
"146": "price-letter-template.ts",
|
||||
"147": "contractType.ts",
|
||||
"148": "Community 148",
|
||||
"149": "fetchCustomerDetail",
|
||||
"150": "Community 150",
|
||||
"151": "Community 151",
|
||||
"152": "SettingDrawer.vue",
|
||||
"153": "index.vue",
|
||||
"154": "index.vue",
|
||||
"155": "error.ts",
|
||||
"156": "v2.4.2.1 - 2025-06-16",
|
||||
"157": "form.vue",
|
||||
"158": "removeProductsNotAllowedByHazard",
|
||||
"159": "detail.vue",
|
||||
"160": "index.vue",
|
||||
"161": "Community 161",
|
||||
"162": "Community 162",
|
||||
"163": "system.ts",
|
||||
"164": "Community 164",
|
||||
"165": "handleDisburse",
|
||||
"166": "Community 166",
|
||||
"167": "Community 167",
|
||||
"168": "crypto.ts",
|
||||
"169": "Community 169",
|
||||
"170": "Community 170",
|
||||
"171": "index.vue",
|
||||
"172": "index.js",
|
||||
"173": "index.vue",
|
||||
"174": "Community 174",
|
||||
"175": "handleEdit",
|
||||
"176": "fetchData",
|
||||
"177": "fetchCustomerDetail",
|
||||
"178": "Community 178",
|
||||
"179": "Community 179",
|
||||
"180": "index.vue",
|
||||
"181": "Community 181",
|
||||
"182": "Community 182",
|
||||
"183": "Ha",
|
||||
"184": "highlight.ts",
|
||||
"185": "handleConfirmApproval",
|
||||
"186": "StorageKeyManager",
|
||||
"187": "Community 187",
|
||||
"188": "Community 188",
|
||||
"189": "Community 189",
|
||||
"190": "error-handle.ts",
|
||||
"191": "formatYmd",
|
||||
"195": "Community 195",
|
||||
"196": "Community 196",
|
||||
"197": "handleEdit",
|
||||
"198": "fetchData",
|
||||
"200": "index.js",
|
||||
"202": "Community 202",
|
||||
"204": "fetchData",
|
||||
"207": "emit",
|
||||
"209": "Community 209",
|
||||
"210": "Community 210",
|
||||
"211": "loadUserOptions",
|
||||
"212": "index.js",
|
||||
"213": "Community 213",
|
||||
"214": "Community 214",
|
||||
"217": "index.js",
|
||||
"218": "index.js",
|
||||
"219": "handleAdd",
|
||||
"223": "Community 223",
|
||||
"224": "Community 224",
|
||||
"226": "form.vue",
|
||||
"227": "Community 227",
|
||||
"228": "Community 228",
|
||||
"233": "Community 233",
|
||||
"234": "Community 234",
|
||||
"239": "Community 239",
|
||||
"242": "Community 242",
|
||||
"243": "Community 243",
|
||||
"247": "Community 247",
|
||||
"248": "Community 248",
|
||||
"251": "handleDisburse",
|
||||
"253": "Community 253",
|
||||
"254": "Community 254",
|
||||
"255": "Community 255",
|
||||
"259": "Community 259",
|
||||
"260": "Community 260",
|
||||
"261": "Community 261",
|
||||
"265": "Community 265",
|
||||
"269": "Community 269",
|
||||
"270": "Community 270",
|
||||
"280": "Community 280",
|
||||
"283": "Community 283",
|
||||
"284": "Community 284",
|
||||
"285": "Community 285",
|
||||
"286": "Community 286",
|
||||
"292": "Community 292",
|
||||
"295": "Community 295",
|
||||
"296": "Community 296",
|
||||
"302": "Community 302",
|
||||
"303": "Community 303",
|
||||
"304": "Community 304",
|
||||
"305": "isIoPlanFrozen",
|
||||
"306": "handleArrange",
|
||||
"307": "Community 307",
|
||||
"308": "Community 308",
|
||||
"309": "Community 309",
|
||||
"310": "Community 310",
|
||||
"315": "Community 315",
|
||||
"316": "Community 316",
|
||||
"317": "Community 317",
|
||||
"318": "Community 318",
|
||||
"319": "Community 319",
|
||||
"320": "Community 320",
|
||||
"321": "Community 321",
|
||||
"322": "Community 322",
|
||||
"323": "Community 323",
|
||||
"324": "Community 324",
|
||||
"325": "Community 325",
|
||||
"326": "Community 326",
|
||||
"327": "Community 327",
|
||||
"328": "Community 328",
|
||||
"329": "Community 329",
|
||||
"330": "Community 330",
|
||||
"331": "Community 331",
|
||||
"332": "Community 332",
|
||||
"333": "Community 333",
|
||||
"334": "Community 334",
|
||||
"335": "Community 335",
|
||||
"336": "Community 336",
|
||||
"337": "Community 337",
|
||||
"338": "Community 338",
|
||||
"339": "Community 339",
|
||||
"340": "Community 340",
|
||||
"341": "Community 341",
|
||||
"342": "error-handle.ts",
|
||||
"344": "ArtException.vue",
|
||||
"346": "Community 346",
|
||||
"347": "Community 347",
|
||||
"349": "Community 349",
|
||||
"350": "Community 350",
|
||||
"354": "Community 354",
|
||||
"355": "Community 355",
|
||||
"362": "Community 362",
|
||||
"364": "Community 364",
|
||||
"367": "Community 367",
|
||||
"372": "detail.vue",
|
||||
"377": "Community 377",
|
||||
"379": "Community 379",
|
||||
"380": "Community 380",
|
||||
"381": "Community 381",
|
||||
"391": "Community 391",
|
||||
"392": "Community 392",
|
||||
"393": "Community 393",
|
||||
"405": "zh_CN.js",
|
||||
"406": "Community 406",
|
||||
"408": "Community 408",
|
||||
"410": "Community 410",
|
||||
"412": "Community 412",
|
||||
"416": "Community 416",
|
||||
"417": "Community 417",
|
||||
"422": "Community 422",
|
||||
"423": "Community 423",
|
||||
"424": "Community 424",
|
||||
"425": "Community 425",
|
||||
"426": "Community 426",
|
||||
"427": "Community 427",
|
||||
"428": "Community 428",
|
||||
"435": "index.vue",
|
||||
"443": "index.vue",
|
||||
"444": "index.vue",
|
||||
"445": "Community 445",
|
||||
"459": "Community 459",
|
||||
"460": "Community 460",
|
||||
"462": "Community 462",
|
||||
"471": "Community 471",
|
||||
"472": "Community 472",
|
||||
"473": "Community 473",
|
||||
"474": "Community 474",
|
||||
"475": "Community 475",
|
||||
"476": "Community 476",
|
||||
"487": "Community 487",
|
||||
"488": "Community 488",
|
||||
"496": "Community 496",
|
||||
"499": "Community 499",
|
||||
"521": "Community 521",
|
||||
"522": "Community 522",
|
||||
"528": "Community 528",
|
||||
"534": "Community 534",
|
||||
"535": "Community 535",
|
||||
"536": "Community 536",
|
||||
"537": "Community 537",
|
||||
"539": "Community 539",
|
||||
"540": "Community 540",
|
||||
"550": "Community 550",
|
||||
"551": "Community 551",
|
||||
"552": "Community 552",
|
||||
"553": "Community 553",
|
||||
"555": "Community 555",
|
||||
"556": "Community 556",
|
||||
"557": "Community 557",
|
||||
"559": "Community 559",
|
||||
"560": "Community 560",
|
||||
"561": "Community 561",
|
||||
"562": "Community 562",
|
||||
"571": "Community 571",
|
||||
"572": "Community 572",
|
||||
"573": "Community 573",
|
||||
"574": "Community 574",
|
||||
"575": "Community 575",
|
||||
"576": "Community 576",
|
||||
"580": "Community 580",
|
||||
"581": "Community 581",
|
||||
"582": "Community 582",
|
||||
"583": "Community 583",
|
||||
"584": "Community 584",
|
||||
"585": "Community 585",
|
||||
"586": "Community 586",
|
||||
"597": "Community 597",
|
||||
"598": "Community 598",
|
||||
"599": "Community 599",
|
||||
"600": "Community 600",
|
||||
"601": "Community 601",
|
||||
"602": "Community 602",
|
||||
"603": "Community 603",
|
||||
"604": "Community 604",
|
||||
"607": "Community 607",
|
||||
"608": "Community 608",
|
||||
"609": "Community 609",
|
||||
"610": "Community 610",
|
||||
"611": "Community 611",
|
||||
"612": "Community 612",
|
||||
"613": "Community 613",
|
||||
"614": "Community 614",
|
||||
"615": "Community 615",
|
||||
"630": "Community 630",
|
||||
"631": "Community 631",
|
||||
"632": "Community 632",
|
||||
"633": "Community 633",
|
||||
"634": "Community 634",
|
||||
"635": "Community 635",
|
||||
"636": "Community 636",
|
||||
"637": "Community 637",
|
||||
"638": "Community 638",
|
||||
"639": "Community 639",
|
||||
"640": "Community 640",
|
||||
"641": "Community 641",
|
||||
"642": "Community 642",
|
||||
"643": "Community 643",
|
||||
"644": "Community 644",
|
||||
"645": "Community 645",
|
||||
"650": "Community 650",
|
||||
"651": "Community 651",
|
||||
"652": "Community 652",
|
||||
"653": "Community 653",
|
||||
"654": "Community 654",
|
||||
"655": "Community 655",
|
||||
"658": "Community 658",
|
||||
"659": "Community 659",
|
||||
"660": "Community 660",
|
||||
"662": "Community 662",
|
||||
"663": "Community 663",
|
||||
"664": "Community 664",
|
||||
"665": "Community 665",
|
||||
"666": "Community 666",
|
||||
"667": "Community 667",
|
||||
"668": "Community 668",
|
||||
"669": "Community 669",
|
||||
"670": "Community 670",
|
||||
"671": "Community 671",
|
||||
"700": "Community 700",
|
||||
"701": "Fixed",
|
||||
"702": "Community 702",
|
||||
"703": "Community 703",
|
||||
"704": "Community 704",
|
||||
"705": "Community 705",
|
||||
"706": "Community 706",
|
||||
"707": "Community 707",
|
||||
"708": "Community 708",
|
||||
"709": "Community 709",
|
||||
"710": "Community 710",
|
||||
"711": "Community 711",
|
||||
"713": "Community 713",
|
||||
"715": "Community 715",
|
||||
"716": "Community 716",
|
||||
"717": "Community 717",
|
||||
"718": "Community 718",
|
||||
"720": "Community 720",
|
||||
"722": "Community 722",
|
||||
"723": "Community 723",
|
||||
"724": "Community 724",
|
||||
"725": "Community 725",
|
||||
"726": "Community 726",
|
||||
"727": "Community 727",
|
||||
"729": "Community 729",
|
||||
"730": "Community 730",
|
||||
"731": "Community 731",
|
||||
"732": "Community 732",
|
||||
"733": "Community 733",
|
||||
"734": "修复",
|
||||
"766": "新增",
|
||||
"770": "移除",
|
||||
"772": "重构",
|
||||
"779": "重构",
|
||||
"785": "⚠️ 重要提示",
|
||||
"786": "⚠️ 重要提示",
|
||||
"788": "⚠️ 重要提示",
|
||||
"796": "OpportunityForm.vue",
|
||||
"806": "index.vue",
|
||||
"836": "BxInvoiceFileSelect.vue",
|
||||
"838": "CustomerSelector.vue",
|
||||
"840": "index.vue",
|
||||
"857": "index.ts",
|
||||
"886": "index.vue",
|
||||
"889": "QuoteSelector.vue",
|
||||
"892": "index.vue",
|
||||
"896": "MarkWonDialog.vue",
|
||||
"924": "OpportunitySelector.vue",
|
||||
"950": "BatchUpdateStageDialog.vue",
|
||||
"1010": "BxApprovalFlowTabs.vue",
|
||||
"1015": "DealDialog.vue",
|
||||
"1016": "TransferDialog.vue",
|
||||
"1017": "BatchUpdateDateDialog.vue",
|
||||
"1027": "IntoPoolDialog.vue",
|
||||
"1115": "v2.4.2.7 - 2025-06-25",
|
||||
"1117": "v2.5.2 - 2025-07-13",
|
||||
"1118": "v2.4.2.2 - 2025-06-16",
|
||||
"1123": "v2.4.1.1 - 2025-06-07",
|
||||
"1126": "v2.4.2.1 - 2025-06-16",
|
||||
"1127": "v2.4.2.4 - 2025-06-18",
|
||||
"1197": "icons.js",
|
||||
"1198": "icons.min.js",
|
||||
"1199": "index.js",
|
||||
"1200": "index.js",
|
||||
"1201": "index.js",
|
||||
"1202": "index.js",
|
||||
"1203": "index.js",
|
||||
"1204": "plugin.js",
|
||||
"1205": "plugin.min.js",
|
||||
"1206": "index.js",
|
||||
"1207": "plugin.js",
|
||||
"1208": "plugin.min.js",
|
||||
"1209": "index.js",
|
||||
"1210": "plugin.js",
|
||||
"1211": "plugin.min.js",
|
||||
"1212": "index.js",
|
||||
"1213": "plugin.js",
|
||||
"1214": "plugin.min.js",
|
||||
"1215": "index.js",
|
||||
"1216": "index.js",
|
||||
"1217": "plugin.js",
|
||||
"1218": "plugin.min.js",
|
||||
"1219": "index.js",
|
||||
"1220": "index.js",
|
||||
"1222": "emojiimages.js",
|
||||
"1223": "emojiimages.min.js",
|
||||
"1224": "emojis.js",
|
||||
"1225": "emojis.min.js",
|
||||
"1228": "ar.js",
|
||||
"1229": "bg_BG.js",
|
||||
"1230": "ca.js",
|
||||
"1231": "cs.js",
|
||||
"1232": "da.js",
|
||||
"1233": "de.js",
|
||||
"1234": "el.js",
|
||||
"1235": "es.js",
|
||||
"1236": "eu.js",
|
||||
"1237": "fa.js",
|
||||
"1238": "fi.js",
|
||||
"1239": "fr_FR.js",
|
||||
"1240": "he_IL.js",
|
||||
"1241": "hi.js",
|
||||
"1242": "hr.js",
|
||||
"1243": "hu_HU.js",
|
||||
"1244": "id.js",
|
||||
"1245": "it.js",
|
||||
"1246": "ja.js",
|
||||
"1247": "kk.js",
|
||||
"1248": "ko_KR.js",
|
||||
"1249": "ms.js",
|
||||
"1250": "nb_NO.js",
|
||||
"1251": "nl.js",
|
||||
"1252": "pl.js",
|
||||
"1253": "pt_BR.js",
|
||||
"1254": "pt_PT.js",
|
||||
"1255": "ro.js",
|
||||
"1256": "ru.js",
|
||||
"1257": "sk.js",
|
||||
"1258": "sl_SI.js",
|
||||
"1259": "sv_SE.js",
|
||||
"1260": "th_TH.js",
|
||||
"1261": "tr.js",
|
||||
"1262": "uk.js",
|
||||
"1263": "vi.js",
|
||||
"1265": "zh_TW.js",
|
||||
"1266": "index.js",
|
||||
"1267": "index.js",
|
||||
"1268": "plugin.js",
|
||||
"1269": "_",
|
||||
"1270": "index.js",
|
||||
"1271": "plugin.js",
|
||||
"1272": "plugin.min.js",
|
||||
"1275": "index.js",
|
||||
"1277": "plugin.js",
|
||||
"1278": "plugin.min.js",
|
||||
"1280": "plugin.js",
|
||||
"1281": "plugin.min.js",
|
||||
"1282": "index.js",
|
||||
"1283": "plugin.js",
|
||||
"1284": "plugin.min.js",
|
||||
"1287": "plugin.js",
|
||||
"1288": "plugin.min.js",
|
||||
"1289": "index.js",
|
||||
"1290": "index.js",
|
||||
"1291": "index.js",
|
||||
"1292": "index.js",
|
||||
"1293": "plugin.js",
|
||||
"1294": "plugin.min.js",
|
||||
"1295": "index.js",
|
||||
"1297": "plugin.js",
|
||||
"1298": "plugin.min.js",
|
||||
"1299": "content.js",
|
||||
"1300": "content.js",
|
||||
"1301": "content.js",
|
||||
"1302": "content.js",
|
||||
"1303": "content.js",
|
||||
"1304": "content.js",
|
||||
"1305": "content.inline.js",
|
||||
"1306": "content.js",
|
||||
"1307": "skin.js",
|
||||
"1308": "skin.shadowdom.js",
|
||||
"1309": "content.inline.js",
|
||||
"1310": "content.js",
|
||||
"1311": "skin.js",
|
||||
"1312": "skin.shadowdom.js",
|
||||
"1313": "content.inline.js",
|
||||
"1314": "content.js",
|
||||
"1315": "skin.js",
|
||||
"1316": "skin.shadowdom.js",
|
||||
"1317": "content.inline.js",
|
||||
"1318": "content.js",
|
||||
"1319": "skin.js",
|
||||
"1320": "skin.shadowdom.js",
|
||||
"1323": "index.vue",
|
||||
"1324": "index.vue",
|
||||
"1325": "index.vue",
|
||||
"1326": "route-parent.vue",
|
||||
"1327": "setupSubpackSlipEditorTypeCheckboxes",
|
||||
"1328": "修复",
|
||||
"1329": "优化",
|
||||
"1330": "新增",
|
||||
"1331": "⚠️ 重要提示",
|
||||
"1332": "重构",
|
||||
"1333": "优化",
|
||||
"1334": "修复",
|
||||
"1335": "新增",
|
||||
"1336": "升级",
|
||||
"1337": "⚠️ 重要提示",
|
||||
"1338": "修复",
|
||||
"1339": "优化",
|
||||
"1340": "新增",
|
||||
"1341": "优化",
|
||||
"1342": "修复",
|
||||
"1343": "⚠️ 重要提示",
|
||||
"1344": "升级",
|
||||
"1345": "修复",
|
||||
"1346": "优化",
|
||||
"1347": "⚠️ 重要提示",
|
||||
"1348": "新增",
|
||||
"1349": "修复",
|
||||
"1350": "优化",
|
||||
"1351": "新增",
|
||||
"1352": "优化",
|
||||
"1353": "修复",
|
||||
"1354": "新增",
|
||||
"1355": "重构",
|
||||
"1356": "优化",
|
||||
"1357": "修复",
|
||||
"1358": "移除",
|
||||
"1359": "新增",
|
||||
"1360": "优化",
|
||||
"1361": "修复",
|
||||
"1362": "新增",
|
||||
"1363": "重构",
|
||||
"1364": "升级",
|
||||
"1365": "优化",
|
||||
"1366": "修复",
|
||||
"1367": "新增",
|
||||
"1368": "优化",
|
||||
"1369": "修复",
|
||||
"1370": "优化",
|
||||
"1371": "修复",
|
||||
"1372": "新增",
|
||||
"1373": "重构",
|
||||
"1374": "升级",
|
||||
"1375": "移除",
|
||||
"1376": "修复",
|
||||
"1377": "⚠️ 重要提示",
|
||||
"1378": "重构",
|
||||
"1379": "优化",
|
||||
"1380": "修复",
|
||||
"1381": "修复",
|
||||
"1382": "新增",
|
||||
"1383": "优化",
|
||||
"1384": "修复",
|
||||
"1385": "重构",
|
||||
"1386": "优化",
|
||||
"1387": "新增",
|
||||
"1388": "重构",
|
||||
"1389": "重构",
|
||||
"1390": "优化",
|
||||
"1391": "重构",
|
||||
"1392": "优化",
|
||||
"1393": "重构",
|
||||
"1394": "重构",
|
||||
"1395": "修复",
|
||||
"1396": "重构",
|
||||
"1397": "新增",
|
||||
"1398": "优化",
|
||||
"1399": "修复",
|
||||
"1400": "⚠️ 重要提示",
|
||||
"1401": "优化",
|
||||
"1402": "修复",
|
||||
"1403": "新增",
|
||||
"1404": "优化",
|
||||
"1405": "修复",
|
||||
"1406": "重构",
|
||||
"1407": "新增",
|
||||
"1408": "优化",
|
||||
"1409": "⚠️ 重要提示",
|
||||
"1410": "code:block1 (crm/clue/)",
|
||||
"1411": "code:typescript ({)",
|
||||
"1412": "code:block3 (crm:clue:list # 线索列表查询)",
|
||||
"1413": "code:bash (mysql -u root -p your_database < crm/sql/crm/12-CRM字典数据汇总.md)",
|
||||
"9890": "README.md",
|
||||
"9959": "Fixed",
|
||||
"9960": "Fixed",
|
||||
"9961": "Fixed",
|
||||
"9962": "Fixed",
|
||||
"9963": "Fixed",
|
||||
"9964": "Fixed",
|
||||
"9965": "Fixed",
|
||||
"9966": "Fixed",
|
||||
"9967": "Fixed",
|
||||
"9968": "Fixed",
|
||||
"9969": "Fixed",
|
||||
"9970": "Fixed",
|
||||
"9971": "Fixed",
|
||||
"9972": "Fixed",
|
||||
"9973": "Fixed",
|
||||
"9974": "⚠️ Important Notice",
|
||||
"9975": "⚠️ Important Notice",
|
||||
"9976": "⚠️ Important Notice",
|
||||
"9977": "⚠️ Important Notice",
|
||||
"9978": "⚠️ Important Notice",
|
||||
"9979": "⚠️ Important Notice",
|
||||
"9980": "⚠️ Important Notice",
|
||||
"9981": "Optimized",
|
||||
"9982": "Optimized",
|
||||
"9983": "Optimized",
|
||||
"9984": "Optimized",
|
||||
"9985": "Optimized",
|
||||
"9986": "Optimized",
|
||||
"9987": "Optimized",
|
||||
"9988": "Optimized",
|
||||
"9989": "Optimized",
|
||||
"9990": "Optimized",
|
||||
"9991": "Optimized",
|
||||
"9992": "Optimized",
|
||||
"9993": "Optimized",
|
||||
"9994": "Optimized",
|
||||
"9995": "Optimized",
|
||||
"9996": "Optimized",
|
||||
"9997": "Optimized",
|
||||
"9998": "Optimized",
|
||||
"9999": "Optimized",
|
||||
"10000": "Optimized",
|
||||
"10001": "Refactored",
|
||||
"10002": "Refactored",
|
||||
"10003": "Refactored",
|
||||
"10004": "Refactored",
|
||||
"10005": "Refactored",
|
||||
"10006": "Refactored",
|
||||
"10007": "Refactored",
|
||||
"10008": "Refactored",
|
||||
"10009": "Refactored",
|
||||
"10010": "Refactored",
|
||||
"10011": "Refactored",
|
||||
"10012": "Refactored",
|
||||
"10013": "Removed",
|
||||
"10014": "Removed",
|
||||
"10015": "Upgraded",
|
||||
"10016": "Upgraded",
|
||||
"10017": "Upgraded",
|
||||
"10018": "Upgraded",
|
||||
"10019": "优化",
|
||||
"10020": "修复",
|
||||
"10021": "⚠️ 重要提示",
|
||||
"10022": "修复",
|
||||
"10023": "优化",
|
||||
"10024": "优化",
|
||||
"10025": "⚠️ 重要提示",
|
||||
"10026": "修复",
|
||||
"10027": "优化",
|
||||
"10028": "优化",
|
||||
"10029": "修复",
|
||||
"10030": "重构",
|
||||
"10031": "优化",
|
||||
"10032": "修复",
|
||||
"10033": "新增",
|
||||
"10034": "移除",
|
||||
"10035": "优化",
|
||||
"10036": "修复",
|
||||
"10037": "重构",
|
||||
"10038": "升级",
|
||||
"10039": "优化",
|
||||
"10040": "修复",
|
||||
"10041": "优化",
|
||||
"10042": "修复",
|
||||
"10043": "修复",
|
||||
"10044": "重构",
|
||||
"10045": "升级",
|
||||
"10046": "移除",
|
||||
"10047": "修复",
|
||||
"10048": "⚠️ 重要提示",
|
||||
"10049": "⚠️ 重要提示",
|
||||
"10050": "优化",
|
||||
"10051": "修复",
|
||||
"10052": "优化",
|
||||
"10053": "修复",
|
||||
"10054": "优化",
|
||||
"10055": "新增",
|
||||
"10056": "重构",
|
||||
"10057": "优化",
|
||||
"10058": "优化",
|
||||
"10059": "重构",
|
||||
"10060": "修复",
|
||||
"10061": "新增",
|
||||
"10062": "优化",
|
||||
"10063": "修复",
|
||||
"10064": "优化",
|
||||
"10065": "⚠️ 重要提示",
|
||||
"10066": "优化",
|
||||
"10067": "修复",
|
||||
"10068": "优化",
|
||||
"10069": "修复",
|
||||
"10070": "新增",
|
||||
"10071": "优化",
|
||||
"10072": "⚠️ 重要提示",
|
||||
"10073": "修复",
|
||||
"10074": "新增",
|
||||
"10075": "升级",
|
||||
"10076": "⚠️ 重要提示",
|
||||
"10077": "修复",
|
||||
"10078": "优化",
|
||||
"10079": "code:bash (# Install dependencies)",
|
||||
"10080": "code:bash (pnpm clean:dev)",
|
||||
"10081": "code:bash (# 安装依赖)",
|
||||
"10082": "code:bash (pnpm clean:dev)"
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+3833
-2361
File diff suppressed because it is too large.
Load diff
@@ -195,6 +195,14 @@ export const reimburseApi = {
|
||||
},
|
||||
delete(id: BxId) {
|
||||
return bxRequest.delete(`/reimburses/${id}`)
|
||||
},
|
||||
downloadImportTemplate() {
|
||||
return bxRequest.get('/reimburses/import-template', { responseType: 'blob' })
|
||||
},
|
||||
importExcel(file: File) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
return bxRequest.post('/reimburses/import', formData)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,512 @@
|
||||
<!-- 表头 Excel 风格列过滤:漏斗靠右,支持文本/枚举/数字/时间 -->
|
||||
<template>
|
||||
<el-popover
|
||||
:visible="visible"
|
||||
placement="bottom-end"
|
||||
:width="300"
|
||||
trigger="click"
|
||||
:teleported="true"
|
||||
popper-class="column-header-filter-popper"
|
||||
@update:visible="onVisibleChange"
|
||||
>
|
||||
<template #reference>
|
||||
<button
|
||||
type="button"
|
||||
class="column-header-filter-btn"
|
||||
:class="{ active: isActive }"
|
||||
:aria-label="`过滤${label}`"
|
||||
>
|
||||
<el-icon :size="14"><Filter /></el-icon>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<div class="column-header-filter-panel" @click.stop>
|
||||
<div class="panel-hd">
|
||||
<div class="panel-title">过滤 · {{ label }}</div>
|
||||
<div class="panel-sub">{{ typeHint }}</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-bd">
|
||||
<!-- 文本 / 枚举 -->
|
||||
<template v-if="type === 'text' || type === 'enum'">
|
||||
<div class="field">
|
||||
<label>搜索可选值</label>
|
||||
<el-input
|
||||
v-model="draftKeyword"
|
||||
clearable
|
||||
placeholder="输入关键字过滤下方列表"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
<div class="select-all-row">
|
||||
<span>本页可选值</span>
|
||||
<span>
|
||||
<el-button link type="primary" size="small" @click="selectAllVisible">全选</el-button>
|
||||
<el-button link type="primary" size="small" @click="selectNoneVisible"
|
||||
>全不选</el-button
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
<div class="value-list">
|
||||
<label v-for="opt in visibleOptions" :key="opt.value" class="value-item">
|
||||
<el-checkbox
|
||||
:model-value="draftSelected.includes(opt.value)"
|
||||
@change="(v) => toggleSelected(opt.value, !!v)"
|
||||
>
|
||||
<span class="value-label">{{ opt.label }}</span>
|
||||
</el-checkbox>
|
||||
<span class="value-count">{{ opt.count }}</span>
|
||||
</label>
|
||||
<div v-if="!visibleOptions.length" class="empty-tip">无匹配项</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 数字 -->
|
||||
<template v-else-if="type === 'number'">
|
||||
<div class="field">
|
||||
<label>条件</label>
|
||||
<el-select v-model="draftOp" size="small" style="width: 100%">
|
||||
<el-option label="介于" value="between" />
|
||||
<el-option label="等于" value="eq" />
|
||||
<el-option label="大于" value="gt" />
|
||||
<el-option label="大于等于" value="gte" />
|
||||
<el-option label="小于" value="lt" />
|
||||
<el-option label="小于等于" value="lte" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div v-if="draftOp === 'between'" class="field">
|
||||
<label>数值区间</label>
|
||||
<div class="range-row">
|
||||
<el-input
|
||||
v-model="draftMin"
|
||||
type="number"
|
||||
size="small"
|
||||
placeholder="最小值"
|
||||
clearable
|
||||
/>
|
||||
<span class="sep">至</span>
|
||||
<el-input
|
||||
v-model="draftMax"
|
||||
type="number"
|
||||
size="small"
|
||||
placeholder="最大值"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="field">
|
||||
<label>数值</label>
|
||||
<el-input
|
||||
v-model="draftValue"
|
||||
type="number"
|
||||
size="small"
|
||||
placeholder="输入数值"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 时间 -->
|
||||
<template v-else-if="type === 'datetime'">
|
||||
<div class="field">
|
||||
<label>开始日期</label>
|
||||
<el-date-picker
|
||||
v-model="draftFrom"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="开始日期"
|
||||
size="small"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>结束日期</label>
|
||||
<el-date-picker
|
||||
v-model="draftTo"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="结束日期"
|
||||
size="small"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
/>
|
||||
</div>
|
||||
<p class="field-tip">闭区间:含起止当天</p>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="panel-ft">
|
||||
<el-button size="small" @click="clearColumn">
|
||||
<el-icon><Delete /></el-icon>清空本列
|
||||
</el-button>
|
||||
<div class="panel-ft-end">
|
||||
<el-button size="small" @click="close">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button type="primary" size="small" @click="confirm">
|
||||
<el-icon><Check /></el-icon>确定
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-popover>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Filter, Delete, Close, Check } from '@element-plus/icons-vue'
|
||||
import {
|
||||
isColumnFilterActive,
|
||||
type ColumnFilterType,
|
||||
type ColumnFilterState,
|
||||
type TextColumnFilterState,
|
||||
type NumberColumnFilterState,
|
||||
type DatetimeColumnFilterState,
|
||||
type NumberFilterOp
|
||||
} from '@/composables/useClientColumnFilters'
|
||||
|
||||
defineOptions({ name: 'ColumnHeaderFilter' })
|
||||
|
||||
const props = defineProps<{
|
||||
prop: string
|
||||
label: string
|
||||
type: ColumnFilterType
|
||||
isActive: boolean
|
||||
modelValue?: ColumnFilterState | null
|
||||
options?: Array<{ value: string; label: string; count: number }>
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: ColumnFilterState | null]
|
||||
clear: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const draftKeyword = ref('')
|
||||
const draftSelected = ref<string[]>([])
|
||||
const draftOp = ref<NumberFilterOp>('between')
|
||||
const draftValue = ref('')
|
||||
const draftMin = ref('')
|
||||
const draftMax = ref('')
|
||||
const draftFrom = ref('')
|
||||
const draftTo = ref('')
|
||||
|
||||
const typeHint = computed(() => {
|
||||
if (props.type === 'text') return '文本:搜索并勾选本页出现的值'
|
||||
if (props.type === 'enum') return '枚举:勾选固定选项'
|
||||
if (props.type === 'number') return '数字:运算符 + 单值或区间'
|
||||
return '时间:按日期闭区间过滤'
|
||||
})
|
||||
|
||||
const visibleOptions = computed(() => {
|
||||
const list = props.options || []
|
||||
const q = draftKeyword.value.trim().toLowerCase()
|
||||
if (!q) return list
|
||||
return list.filter(
|
||||
(opt) => opt.label.toLowerCase().includes(q) || String(opt.value).toLowerCase().includes(q)
|
||||
)
|
||||
})
|
||||
|
||||
function resetDraftFromModel() {
|
||||
const m = props.modelValue
|
||||
if (props.type === 'text' || props.type === 'enum') {
|
||||
const s = (m || { selected: [], keyword: '' }) as TextColumnFilterState
|
||||
draftSelected.value = [...(s.selected || [])]
|
||||
draftKeyword.value = s.keyword || ''
|
||||
return
|
||||
}
|
||||
if (props.type === 'number') {
|
||||
const s = (m || { op: 'between', value: '', min: '', max: '' }) as NumberColumnFilterState
|
||||
draftOp.value = s.op || 'between'
|
||||
draftValue.value = s.value ?? ''
|
||||
draftMin.value = s.min ?? ''
|
||||
draftMax.value = s.max ?? ''
|
||||
return
|
||||
}
|
||||
if (props.type === 'datetime') {
|
||||
const s = (m || { from: '', to: '' }) as DatetimeColumnFilterState
|
||||
draftFrom.value = s.from || ''
|
||||
draftTo.value = s.to || ''
|
||||
}
|
||||
}
|
||||
|
||||
function onVisibleChange(next: boolean) {
|
||||
if (next) resetDraftFromModel()
|
||||
visible.value = next
|
||||
}
|
||||
|
||||
function close() {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
function toggleSelected(value: string, checked: boolean) {
|
||||
if (checked) {
|
||||
if (!draftSelected.value.includes(value)) draftSelected.value.push(value)
|
||||
} else {
|
||||
draftSelected.value = draftSelected.value.filter((x) => x !== value)
|
||||
}
|
||||
}
|
||||
|
||||
function selectAllVisible() {
|
||||
for (const opt of visibleOptions.value) {
|
||||
if (!draftSelected.value.includes(opt.value)) draftSelected.value.push(opt.value)
|
||||
}
|
||||
}
|
||||
|
||||
function selectNoneVisible() {
|
||||
const hide = new Set(visibleOptions.value.map((o) => o.value))
|
||||
draftSelected.value = draftSelected.value.filter((v) => !hide.has(v))
|
||||
}
|
||||
|
||||
function buildState(): ColumnFilterState {
|
||||
if (props.type === 'text' || props.type === 'enum') {
|
||||
return {
|
||||
selected: [...draftSelected.value],
|
||||
keyword: draftKeyword.value
|
||||
}
|
||||
}
|
||||
if (props.type === 'number') {
|
||||
return {
|
||||
op: draftOp.value,
|
||||
value: draftValue.value,
|
||||
min: draftMin.value,
|
||||
max: draftMax.value
|
||||
}
|
||||
}
|
||||
return {
|
||||
from: draftFrom.value || '',
|
||||
to: draftTo.value || ''
|
||||
}
|
||||
}
|
||||
|
||||
function confirm() {
|
||||
if (props.type === 'number' && draftOp.value === 'between') {
|
||||
if (
|
||||
draftMin.value !== '' &&
|
||||
draftMax.value !== '' &&
|
||||
Number(draftMin.value) > Number(draftMax.value)
|
||||
) {
|
||||
ElMessage.warning('最小值不能大于最大值')
|
||||
return
|
||||
}
|
||||
}
|
||||
if (props.type === 'datetime') {
|
||||
if (draftFrom.value && draftTo.value && draftFrom.value > draftTo.value) {
|
||||
ElMessage.warning('开始日期不能晚于结束日期')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const state = buildState()
|
||||
if (isColumnFilterActive(props.type, state)) {
|
||||
emit('update:modelValue', state)
|
||||
} else {
|
||||
emit('update:modelValue', null)
|
||||
emit('clear')
|
||||
}
|
||||
close()
|
||||
}
|
||||
|
||||
function clearColumn() {
|
||||
emit('update:modelValue', null)
|
||||
emit('clear')
|
||||
close()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
() => {
|
||||
if (visible.value) resetDraftFromModel()
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.column-header-filter-btn {
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: #94a3b8;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
color: var(--mint-text, #1e293b);
|
||||
background: #e2e8f0;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #fff;
|
||||
background: var(--primary, #10b981);
|
||||
}
|
||||
}
|
||||
|
||||
.column-header-filter-panel {
|
||||
margin: -4px;
|
||||
}
|
||||
|
||||
.panel-hd {
|
||||
padding: 4px 4px 10px;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter, #e2e8f0);
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
margin-bottom: 2px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--mint-text, #1e293b);
|
||||
}
|
||||
|
||||
.panel-sub {
|
||||
font-size: 11px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
}
|
||||
|
||||
.panel-bd {
|
||||
max-height: 280px;
|
||||
padding: 10px 4px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.field {
|
||||
margin-bottom: 10px;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
}
|
||||
}
|
||||
|
||||
.field-tip {
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
}
|
||||
|
||||
.range-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sep {
|
||||
font-size: 12px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
}
|
||||
|
||||
.select-all-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
}
|
||||
|
||||
.value-list {
|
||||
max-height: 180px;
|
||||
padding: 4px;
|
||||
overflow: auto;
|
||||
border: 1px solid var(--el-border-color-lighter, #e2e8f0);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.value-item {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding: 4px 6px;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
|
||||
&:hover {
|
||||
background: var(--primary-bg, #ecfdf5);
|
||||
}
|
||||
|
||||
:deep(.el-checkbox) {
|
||||
flex: 1;
|
||||
height: auto;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.value-label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.value-count {
|
||||
flex-shrink: 0;
|
||||
font-size: 11px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
}
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
padding: 16px 8px;
|
||||
font-size: 12px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.panel-ft {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 4px 4px;
|
||||
border-top: 1px solid var(--el-border-color-lighter, #e2e8f0);
|
||||
}
|
||||
|
||||
.panel-ft-end {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.column-header-filter-popper.el-popover {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.col-header-with-filter {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.col-header-with-filter__label {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: 600;
|
||||
color: inherit;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.col-header-with-filter__label.is-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.col-header-with-filter__label.is-right {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
@@ -18,7 +18,7 @@ import { PL_PLAN_TYPE_TABS } from '@/utils/pl-display'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string
|
||||
/** 覆盖默认单据类型 Tab(如危化品证上传页追加「未上传证书」) */
|
||||
/** 覆盖默认单据类型 Tab(如运输计划追加「待安排」、危化品证上传页追加「未上传证书」) */
|
||||
tabs?: { label: string; name: string }[]
|
||||
}>()
|
||||
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
import { computed, reactive, type Ref, type ComputedRef } from 'vue'
|
||||
|
||||
export type ColumnFilterType = 'text' | 'enum' | 'number' | 'datetime'
|
||||
|
||||
export type NumberFilterOp = 'between' | 'eq' | 'gt' | 'gte' | 'lt' | 'lte'
|
||||
|
||||
export interface ColumnFilterEnumOption {
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
|
||||
export interface ColumnFilterDef {
|
||||
prop: string
|
||||
label: string
|
||||
type: ColumnFilterType
|
||||
/** 枚举固定选项;未提供时从当前页去重 */
|
||||
enumOptions?: ColumnFilterEnumOption[]
|
||||
/** 自定义取值(展示值 / 比较值) */
|
||||
getValue?: (row: any) => unknown
|
||||
}
|
||||
|
||||
export interface TextColumnFilterState {
|
||||
selected: string[]
|
||||
keyword: string
|
||||
}
|
||||
|
||||
export interface NumberColumnFilterState {
|
||||
op: NumberFilterOp
|
||||
value: string
|
||||
min: string
|
||||
max: string
|
||||
}
|
||||
|
||||
export interface DatetimeColumnFilterState {
|
||||
from: string
|
||||
to: string
|
||||
}
|
||||
|
||||
export type ColumnFilterState =
|
||||
| TextColumnFilterState
|
||||
| NumberColumnFilterState
|
||||
| DatetimeColumnFilterState
|
||||
|
||||
function parseDateMs(value: unknown): number | null {
|
||||
const text = String(value ?? '').trim()
|
||||
if (!text) return null
|
||||
const t = Date.parse(text.replace(/-/g, '/'))
|
||||
return Number.isNaN(t) ? null : t
|
||||
}
|
||||
|
||||
function startOfDayMs(yyyyMmDd: string): number | null {
|
||||
return parseDateMs(`${yyyyMmDd} 00:00:00`)
|
||||
}
|
||||
|
||||
function endOfDayMs(yyyyMmDd: string): number | null {
|
||||
return parseDateMs(`${yyyyMmDd} 23:59:59`)
|
||||
}
|
||||
|
||||
function defaultState(type: ColumnFilterType): ColumnFilterState {
|
||||
if (type === 'number') {
|
||||
return { op: 'between', value: '', min: '', max: '' }
|
||||
}
|
||||
if (type === 'datetime') {
|
||||
return { from: '', to: '' }
|
||||
}
|
||||
return { selected: [], keyword: '' }
|
||||
}
|
||||
|
||||
export function isColumnFilterActive(
|
||||
type: ColumnFilterType,
|
||||
state?: ColumnFilterState | null
|
||||
): boolean {
|
||||
if (!state) return false
|
||||
if (type === 'text' || type === 'enum') {
|
||||
const s = state as TextColumnFilterState
|
||||
return Array.isArray(s.selected) && s.selected.length > 0
|
||||
}
|
||||
if (type === 'number') {
|
||||
const s = state as NumberColumnFilterState
|
||||
if (s.op === 'between') {
|
||||
return (s.min !== '' && s.min != null) || (s.max !== '' && s.max != null)
|
||||
}
|
||||
return s.value !== '' && s.value != null
|
||||
}
|
||||
if (type === 'datetime') {
|
||||
const s = state as DatetimeColumnFilterState
|
||||
return !!(s.from || s.to)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function resolveRawValue(row: any, def: ColumnFilterDef): unknown {
|
||||
if (def.getValue) return def.getValue(row)
|
||||
return row?.[def.prop]
|
||||
}
|
||||
|
||||
function resolveCompareKey(row: any, def: ColumnFilterDef): string {
|
||||
const raw = resolveRawValue(row, def)
|
||||
if (raw == null) return ''
|
||||
return String(raw)
|
||||
}
|
||||
|
||||
function matchOne(row: any, def: ColumnFilterDef, state: ColumnFilterState): boolean {
|
||||
if (!isColumnFilterActive(def.type, state)) return true
|
||||
|
||||
if (def.type === 'text' || def.type === 'enum') {
|
||||
const s = state as TextColumnFilterState
|
||||
const key = resolveCompareKey(row, def)
|
||||
return s.selected.includes(key)
|
||||
}
|
||||
|
||||
if (def.type === 'number') {
|
||||
const s = state as NumberColumnFilterState
|
||||
const val = Number(resolveRawValue(row, def))
|
||||
if (Number.isNaN(val)) return false
|
||||
if (s.op === 'eq') return s.value !== '' && val === Number(s.value)
|
||||
if (s.op === 'gt') return s.value !== '' && val > Number(s.value)
|
||||
if (s.op === 'gte') return s.value !== '' && val >= Number(s.value)
|
||||
if (s.op === 'lt') return s.value !== '' && val < Number(s.value)
|
||||
if (s.op === 'lte') return s.value !== '' && val <= Number(s.value)
|
||||
const minOk = s.min === '' || s.min == null || val >= Number(s.min)
|
||||
const maxOk = s.max === '' || s.max == null || val <= Number(s.max)
|
||||
return minOk && maxOk
|
||||
}
|
||||
|
||||
if (def.type === 'datetime') {
|
||||
const s = state as DatetimeColumnFilterState
|
||||
const t = parseDateMs(resolveRawValue(row, def))
|
||||
if (t == null) return false
|
||||
const fromOk =
|
||||
!s.from ||
|
||||
(() => {
|
||||
const from = startOfDayMs(s.from)
|
||||
return from != null && t >= from
|
||||
})()
|
||||
const toOk =
|
||||
!s.to ||
|
||||
(() => {
|
||||
const to = endOfDayMs(s.to)
|
||||
return to != null && t <= to
|
||||
})()
|
||||
return fromOk && toOk
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export function useClientColumnFilters(
|
||||
sourceRows: Ref<any[]> | ComputedRef<any[]>,
|
||||
columnDefs: ColumnFilterDef[]
|
||||
) {
|
||||
const defMap = new Map(columnDefs.map((d) => [d.prop, d]))
|
||||
const filters = reactive<Record<string, ColumnFilterState>>({})
|
||||
|
||||
const filteredRows = computed(() => {
|
||||
const list = sourceRows.value || []
|
||||
const activeProps = Object.keys(filters).filter((prop) => {
|
||||
const def = defMap.get(prop)
|
||||
return def && isColumnFilterActive(def.type, filters[prop])
|
||||
})
|
||||
if (!activeProps.length) return list
|
||||
return list.filter((row) =>
|
||||
activeProps.every((prop) => {
|
||||
const def = defMap.get(prop)!
|
||||
return matchOne(row, def, filters[prop])
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
const activeFilterProps = computed(() =>
|
||||
Object.keys(filters).filter((prop) => {
|
||||
const def = defMap.get(prop)
|
||||
return def && isColumnFilterActive(def.type, filters[prop])
|
||||
})
|
||||
)
|
||||
|
||||
const hasActiveFilters = computed(() => activeFilterProps.value.length > 0)
|
||||
|
||||
function getFilterState(prop: string): ColumnFilterState {
|
||||
const def = defMap.get(prop)
|
||||
if (!def) return defaultState('text')
|
||||
if (!filters[prop]) {
|
||||
filters[prop] = defaultState(def.type)
|
||||
}
|
||||
return filters[prop]
|
||||
}
|
||||
|
||||
function setFilterState(prop: string, state: ColumnFilterState | null) {
|
||||
const def = defMap.get(prop)
|
||||
if (!def) return
|
||||
if (!state || !isColumnFilterActive(def.type, state)) {
|
||||
delete filters[prop]
|
||||
return
|
||||
}
|
||||
filters[prop] = JSON.parse(JSON.stringify(state))
|
||||
}
|
||||
|
||||
function clearFilter(prop: string) {
|
||||
delete filters[prop]
|
||||
}
|
||||
|
||||
function clearAllFilters() {
|
||||
Object.keys(filters).forEach((k) => delete filters[k])
|
||||
}
|
||||
|
||||
function isFilterActive(prop: string): boolean {
|
||||
const def = defMap.get(prop)
|
||||
if (!def) return false
|
||||
return isColumnFilterActive(def.type, filters[prop])
|
||||
}
|
||||
|
||||
/** 当前页去重可选值(含计数) */
|
||||
function collectUniqueOptions(
|
||||
prop: string
|
||||
): Array<{ value: string; label: string; count: number }> {
|
||||
const def = defMap.get(prop)
|
||||
if (!def) return []
|
||||
|
||||
if (def.type === 'enum' && def.enumOptions?.length) {
|
||||
const counts = new Map<string, number>()
|
||||
for (const row of sourceRows.value || []) {
|
||||
const key = resolveCompareKey(row, def)
|
||||
counts.set(key, (counts.get(key) || 0) + 1)
|
||||
}
|
||||
return def.enumOptions.map((opt) => ({
|
||||
value: opt.value,
|
||||
label: opt.label,
|
||||
count: counts.get(opt.value) || 0
|
||||
}))
|
||||
}
|
||||
|
||||
const map = new Map<string, { label: string; count: number }>()
|
||||
for (const row of sourceRows.value || []) {
|
||||
const key = resolveCompareKey(row, def)
|
||||
const label = key || '(空)'
|
||||
const prev = map.get(key)
|
||||
if (prev) prev.count += 1
|
||||
else map.set(key, { label, count: 1 })
|
||||
}
|
||||
return [...map.entries()]
|
||||
.map(([value, meta]) => ({ value, label: meta.label, count: meta.count }))
|
||||
.sort((a, b) => a.label.localeCompare(b.label, 'zh-CN'))
|
||||
}
|
||||
|
||||
function getColumnDef(prop: string): ColumnFilterDef | undefined {
|
||||
return defMap.get(prop)
|
||||
}
|
||||
|
||||
return {
|
||||
filters,
|
||||
filteredRows,
|
||||
activeFilterProps,
|
||||
hasActiveFilters,
|
||||
getFilterState,
|
||||
setFilterState,
|
||||
clearFilter,
|
||||
clearAllFilters,
|
||||
isFilterActive,
|
||||
collectUniqueOptions,
|
||||
getColumnDef
|
||||
}
|
||||
}
|
||||
@@ -324,6 +324,9 @@ export function canPrintSlipCommand(planType: string | undefined, command: PlPri
|
||||
|
||||
export type PlPlanTypeTab = 'all' | 'inbound' | 'outbound' | 'direct'
|
||||
|
||||
/** 运输计划列表 Tab(在单据类型后增加「待安排」) */
|
||||
export type PlPlanListTab = PlPlanTypeTab | 'pending_arrange'
|
||||
|
||||
/** 危化品证上传页 Tab(在单据类型后增加「未上传证书」) */
|
||||
export type PlPlanCertTab = PlPlanTypeTab | 'cert_incomplete'
|
||||
|
||||
@@ -334,6 +337,11 @@ export const PL_PLAN_TYPE_TABS: { label: string; name: PlPlanTypeTab }[] = [
|
||||
{ label: '直销单', name: 'direct' }
|
||||
]
|
||||
|
||||
export const PL_PLAN_LIST_TABS: { label: string; name: PlPlanListTab }[] = [
|
||||
...PL_PLAN_TYPE_TABS,
|
||||
{ label: '待安排', name: 'pending_arrange' }
|
||||
]
|
||||
|
||||
export const PL_PLAN_CERT_TABS: { label: string; name: PlPlanCertTab }[] = [
|
||||
...PL_PLAN_TYPE_TABS,
|
||||
{ label: '未上传证书', name: 'cert_incomplete' }
|
||||
@@ -343,6 +351,16 @@ export function plPlanTypeTabToFilter(tab: PlPlanTypeTab): string {
|
||||
return tab === 'all' ? '' : tab
|
||||
}
|
||||
|
||||
export function plPlanListTabToFilters(tab: PlPlanListTab): {
|
||||
planType: string
|
||||
followStatus?: string
|
||||
} {
|
||||
if (tab === 'pending_arrange') {
|
||||
return { planType: '', followStatus: 'pending_arrange' }
|
||||
}
|
||||
return { planType: plPlanTypeTabToFilter(tab) }
|
||||
}
|
||||
|
||||
export function plPlanCertTabToFilters(tab: PlPlanCertTab): {
|
||||
planType: string
|
||||
certUploadStatus?: string
|
||||
|
||||
@@ -57,6 +57,12 @@
|
||||
<el-button class="btn-main" type="primary" @click="openAddDialog">
|
||||
<el-icon><Plus /></el-icon>新增报销申请
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" :loading="templateLoading" @click="downloadImportTemplate">
|
||||
<el-icon><Download /></el-icon>下载模板
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" @click="openImportDialog">
|
||||
<el-icon><FolderAdd /></el-icon>批量导入
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
:disabled="selectedRows.length !== 1 || !isDraftEditable(selectedRows[0])"
|
||||
@@ -83,6 +89,13 @@
|
||||
<el-button class="btn-ghost" @click="openFlowDialog()">
|
||||
<el-icon><View /></el-icon>查看审批流
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasActiveFilters"
|
||||
class="btn-ghost"
|
||||
@click="clearAllFilters"
|
||||
>
|
||||
<el-icon><Filter /></el-icon>清空列过滤
|
||||
</el-button>
|
||||
<ListToolbarEndActions
|
||||
:loading="loading"
|
||||
:column-defs="listColumnDefs"
|
||||
@@ -100,7 +113,7 @@
|
||||
<div ref="reimburseApplyTableWrapRef" class="table-scroll bx-list-table-wrap">
|
||||
<ListTable table-key="bx-reimburse-apply"
|
||||
class="list-table"
|
||||
:data="rows"
|
||||
:data="filteredRows"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
@selection-change="onSelectionChange"
|
||||
@@ -113,20 +126,51 @@
|
||||
v-if="isColumnVisible('applyTime')"
|
||||
prop="applyTime"
|
||||
label="申请时间"
|
||||
min-width="160"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="applyCode"
|
||||
label="报销单号"
|
||||
class-name="col-apply-code"
|
||||
min-width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #header>
|
||||
<div class="col-header-with-filter">
|
||||
<span class="col-header-with-filter__label">申请时间</span>
|
||||
<ColumnHeaderFilter
|
||||
prop="applyTime"
|
||||
label="申请时间"
|
||||
type="datetime"
|
||||
:is-active="isFilterActive('applyTime')"
|
||||
:model-value="filters.applyTime || null"
|
||||
@update:model-value="(v) => setFilterState('applyTime', v)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
{{ formatApplyDateOnly(row.applyTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="applyCode"
|
||||
label="报销单号"
|
||||
class-name="col-apply-code"
|
||||
min-width="160"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #header>
|
||||
<div class="col-header-with-filter">
|
||||
<span class="col-header-with-filter__label">报销单号</span>
|
||||
<ColumnHeaderFilter
|
||||
prop="applyCode"
|
||||
label="报销单号"
|
||||
type="text"
|
||||
:is-active="isFilterActive('applyCode')"
|
||||
:model-value="filters.applyCode || null"
|
||||
:options="collectUniqueOptions('applyCode')"
|
||||
@update:model-value="(v) => setFilterState('applyCode', v)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="openView(row)">{{ row.applyCode }}</el-link>
|
||||
</template>
|
||||
@@ -135,20 +179,49 @@
|
||||
v-if="isColumnVisible('title')"
|
||||
prop="title"
|
||||
label="报销标题"
|
||||
min-width="180"
|
||||
align="left"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('expenseName')"
|
||||
prop="expenseName"
|
||||
label="费用项目"
|
||||
min-width="260"
|
||||
min-width="200"
|
||||
align="left"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #header>
|
||||
<div class="col-header-with-filter">
|
||||
<span class="col-header-with-filter__label">报销标题</span>
|
||||
<ColumnHeaderFilter
|
||||
prop="title"
|
||||
label="报销标题"
|
||||
type="text"
|
||||
:is-active="isFilterActive('title')"
|
||||
:model-value="filters.title || null"
|
||||
:options="collectUniqueOptions('title')"
|
||||
@update:model-value="(v) => setFilterState('title', v)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('expenseName')"
|
||||
prop="expenseName"
|
||||
label="费用项目"
|
||||
min-width="280"
|
||||
align="left"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #header>
|
||||
<div class="col-header-with-filter">
|
||||
<span class="col-header-with-filter__label">费用项目</span>
|
||||
<ColumnHeaderFilter
|
||||
prop="expenseName"
|
||||
label="费用项目"
|
||||
type="text"
|
||||
:is-active="isFilterActive('expenseName')"
|
||||
:model-value="filters.expenseName || null"
|
||||
:options="collectUniqueOptions('expenseName')"
|
||||
@update:model-value="(v) => setFilterState('expenseName', v)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<div class="expense-items-lines">
|
||||
<div
|
||||
@@ -167,18 +240,46 @@
|
||||
label="报销金额(元)"
|
||||
:formatter="moneyTableFormatter"
|
||||
class-name="col-money"
|
||||
min-width="120"
|
||||
min-width="150"
|
||||
align="right"
|
||||
header-align="center"
|
||||
/>
|
||||
>
|
||||
<template #header>
|
||||
<div class="col-header-with-filter">
|
||||
<span class="col-header-with-filter__label is-right">报销金额(元)</span>
|
||||
<ColumnHeaderFilter
|
||||
prop="amount"
|
||||
label="报销金额(元)"
|
||||
type="number"
|
||||
:is-active="isFilterActive('amount')"
|
||||
:model-value="filters.amount || null"
|
||||
@update:model-value="(v) => setFilterState('amount', v)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('status')"
|
||||
prop="status"
|
||||
label="状态"
|
||||
min-width="110"
|
||||
min-width="130"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #header>
|
||||
<div class="col-header-with-filter">
|
||||
<span class="col-header-with-filter__label">状态</span>
|
||||
<ColumnHeaderFilter
|
||||
prop="status"
|
||||
label="状态"
|
||||
type="enum"
|
||||
:is-active="isFilterActive('status')"
|
||||
:model-value="filters.status || null"
|
||||
:options="collectUniqueOptions('status')"
|
||||
@update:model-value="(v) => setFilterState('status', v)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusTagType(row.status)" size="small">
|
||||
{{ statusLabel(row.status) }}
|
||||
@@ -189,10 +290,24 @@
|
||||
v-if="isColumnVisible('currentNode')"
|
||||
prop="currentNode"
|
||||
label="当前节点"
|
||||
min-width="120"
|
||||
min-width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #header>
|
||||
<div class="col-header-with-filter">
|
||||
<span class="col-header-with-filter__label">当前节点</span>
|
||||
<ColumnHeaderFilter
|
||||
prop="currentNode"
|
||||
label="当前节点"
|
||||
type="text"
|
||||
:is-active="isFilterActive('currentNode')"
|
||||
:model-value="filters.currentNode || null"
|
||||
:options="collectUniqueOptions('currentNode')"
|
||||
@update:model-value="(v) => setFilterState('currentNode', v)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
v-if="shouldShowWorkflowNodeTag(row.currentNode, row.status)"
|
||||
@@ -209,7 +324,7 @@
|
||||
ref="reimburseApplyStatsRowRef"
|
||||
storage-key="bx-reimburse-apply-column-stats"
|
||||
:columns="reimburseApplyStatsRowColumns"
|
||||
:data="rows"
|
||||
:data="filteredRows"
|
||||
:table-wrap-el="reimburseApplyTableWrapRef"
|
||||
:get-cell-value="getReimburseApplyStatCellValue"
|
||||
/>
|
||||
@@ -459,6 +574,81 @@
|
||||
@confirm="onInvoicesPicked"
|
||||
/>
|
||||
|
||||
<el-dialog
|
||||
v-model="importDialogVisible"
|
||||
title="批量导入报销申请"
|
||||
width="720px"
|
||||
class="dialog-form-layout"
|
||||
align-center
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<p class="import-hint">
|
||||
请先下载最新模板填写。灰色【示例】行导入时自动跳过。导入结果为「待提交」草稿,发票请事后在系统中关联。导入完成后可下载带「导入结果」列的结果表。
|
||||
</p>
|
||||
<el-upload
|
||||
drag
|
||||
:auto-upload="false"
|
||||
:limit="1"
|
||||
accept=".xlsx,.xls"
|
||||
:file-list="importFileList"
|
||||
:on-change="onImportFileChange"
|
||||
:on-remove="onImportFileRemove"
|
||||
>
|
||||
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
|
||||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||||
</el-upload>
|
||||
<div v-if="importResult" class="import-result">
|
||||
<p v-if="importResult.tip" class="import-tip">{{ importResult.tip }}</p>
|
||||
<p>
|
||||
成功 {{ importResult.successGroups || 0 }} 组({{ importResult.successRows || 0 }} 行),失败
|
||||
{{ importResult.failGroups || 0 }} 组({{ importResult.failRows || 0 }} 行)
|
||||
<span v-if="importResult.skippedRows">,跳过 {{ importResult.skippedRows }} 行</span>
|
||||
</p>
|
||||
<el-table
|
||||
v-if="importResult.rowResults?.length"
|
||||
:data="importResult.rowResults"
|
||||
size="small"
|
||||
max-height="280"
|
||||
>
|
||||
<el-table-column prop="excelRow" label="行号" min-width="70" align="center" />
|
||||
<el-table-column prop="groupNo" label="分组号" min-width="80" />
|
||||
<el-table-column prop="status" label="导入结果" min-width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="row.status === '成功' ? 'success' : row.status === '失败' ? 'danger' : 'info'"
|
||||
size="small"
|
||||
>
|
||||
{{ row.status }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="message" label="说明" min-width="220" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="importDialogVisible = false">
|
||||
<el-icon><Close /></el-icon>关闭
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="importResult?.resultFileBase64"
|
||||
class="btn-ghost"
|
||||
@click="downloadImportResultFile"
|
||||
>
|
||||
<el-icon><Download /></el-icon>下载结果表
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-main"
|
||||
type="primary"
|
||||
:loading="importLoading"
|
||||
:disabled="!importFileList.length"
|
||||
@click="submitImport"
|
||||
>
|
||||
<el-icon><Check /></el-icon>开始导入
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 详情 -->
|
||||
<el-dialog
|
||||
v-model="dialogs.view"
|
||||
@@ -600,8 +790,13 @@
|
||||
<script setup lang="ts">
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
|
||||
import ColumnHeaderFilter from '@/components/business/column-header-filter/index.vue'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import {
|
||||
useClientColumnFilters,
|
||||
type ColumnFilterDef
|
||||
} from '@/composables/useClientColumnFilters'
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
import { reactive, ref, computed, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
@@ -619,8 +814,13 @@ import {
|
||||
Promotion,
|
||||
VideoPlay,
|
||||
Document,
|
||||
Download
|
||||
Download,
|
||||
FolderAdd,
|
||||
UploadFilled,
|
||||
Check,
|
||||
Filter
|
||||
} from '@element-plus/icons-vue'
|
||||
import type { UploadFile } from 'element-plus'
|
||||
import { reimburseApi, invoiceWalletApi } from '@/api/bx'
|
||||
import { buildApplyDateRangeQuery, type ApplyDateRange } from '@/utils/bx/apply-date-filter'
|
||||
import { canDeleteByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
@@ -680,6 +880,49 @@ const { rows, total, currentPage, pageSize, rangeText, loading, refresh, search,
|
||||
}
|
||||
)
|
||||
|
||||
const columnFilterDefs: ColumnFilterDef[] = [
|
||||
{ prop: 'applyTime', label: '申请时间', type: 'datetime' },
|
||||
{ prop: 'applyCode', label: '报销单号', type: 'text' },
|
||||
{ prop: 'title', label: '报销标题', type: 'text' },
|
||||
{
|
||||
prop: 'expenseName',
|
||||
label: '费用项目',
|
||||
type: 'text',
|
||||
getValue: (row) => {
|
||||
const lines = getReimburseExpenseItemLines(row.details, row)
|
||||
return lines?.length ? lines.join('\n') : row.expenseName || ''
|
||||
}
|
||||
},
|
||||
{ prop: 'amount', label: '报销金额(元)', type: 'number' },
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
type: 'enum',
|
||||
enumOptions: [
|
||||
{ value: 'draft', label: '待提交' },
|
||||
{ value: 'pending', label: '审批中' },
|
||||
{ value: 'approved', label: '审批通过' },
|
||||
{ value: 'rejected', label: '审批驳回' }
|
||||
]
|
||||
},
|
||||
{
|
||||
prop: 'currentNode',
|
||||
label: '当前节点',
|
||||
type: 'text',
|
||||
getValue: (row) => workflowNodeLabel(row.currentNode, row.status) || ''
|
||||
}
|
||||
]
|
||||
|
||||
const {
|
||||
filters,
|
||||
filteredRows,
|
||||
hasActiveFilters,
|
||||
setFilterState,
|
||||
clearAllFilters,
|
||||
isFilterActive,
|
||||
collectUniqueOptions
|
||||
} = useClientColumnFilters(rows, columnFilterDefs)
|
||||
|
||||
const { expenseTree } = useExpenseItemTree()
|
||||
const { applicantOptions, loadApplicantOptions } = useApplicantOptions()
|
||||
const userStore = useUserStore()
|
||||
@@ -742,11 +985,20 @@ const reimburseApplyStatsRowColumns = computed((): StatsRowColumn[] => {
|
||||
return cols
|
||||
})
|
||||
|
||||
function formatApplyDateOnly(value: unknown): string {
|
||||
const text = String(value ?? '').trim()
|
||||
if (!text) return '-'
|
||||
return text.slice(0, 10)
|
||||
}
|
||||
|
||||
function getReimburseApplyStatCellValue(row: any, prop: string): unknown {
|
||||
if (prop === 'expenseName') {
|
||||
const lines = getReimburseExpenseItemLines(row.details, row)
|
||||
return lines?.length ? lines.join('\n') : row.expenseName
|
||||
}
|
||||
if (prop === 'applyTime') {
|
||||
return formatApplyDateOnly(row?.applyTime)
|
||||
}
|
||||
return row?.[prop]
|
||||
}
|
||||
|
||||
@@ -837,6 +1089,12 @@ const walletBalance = ref(0)
|
||||
const editingId = ref<number | null>(null)
|
||||
const titleAutoLinked = ref(true)
|
||||
|
||||
const templateLoading = ref(false)
|
||||
const importDialogVisible = ref(false)
|
||||
const importLoading = ref(false)
|
||||
const importFileList = ref<UploadFile[]>([])
|
||||
const importResult = ref<Record<string, any> | null>(null)
|
||||
|
||||
/** 同一发票共用边框色(与 BxInvoiceSelectDialog / bx-mobile 一致) */
|
||||
const INVOICE_GROUP_COLORS = [
|
||||
'#0891b2',
|
||||
@@ -1113,7 +1371,8 @@ async function openEditDialog(row: any) {
|
||||
titleAutoLinked.value = isAutoReimburseTitle(addForm.title)
|
||||
addForm.remark = data.remark || ''
|
||||
addForm.details = (data.details || []).map((d: any) => ({
|
||||
expenseItemId: d.expenseId,
|
||||
// 费用项 id 可能是纯数字字符串;必须 String,否则 el-tree-select 对不上树节点会直接显示数字
|
||||
expenseItemId: d.expenseId != null && d.expenseId !== '' ? String(d.expenseId) : '',
|
||||
amount: d.amount,
|
||||
remark: d.remark || '',
|
||||
occurDate: d.occurDate || ''
|
||||
@@ -1150,6 +1409,94 @@ async function refreshList() {
|
||||
await refresh()
|
||||
}
|
||||
|
||||
async function downloadImportTemplate() {
|
||||
templateLoading.value = true
|
||||
try {
|
||||
const res: any = await reimburseApi.downloadImportTemplate()
|
||||
const blob =
|
||||
res instanceof Blob
|
||||
? res
|
||||
: new Blob([res as BlobPart], {
|
||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
})
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = '报销申请导入模板.xlsx'
|
||||
link.click()
|
||||
URL.revokeObjectURL(url)
|
||||
} catch {
|
||||
ElMessage.error('下载模板失败')
|
||||
} finally {
|
||||
templateLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openImportDialog() {
|
||||
importFileList.value = []
|
||||
importResult.value = null
|
||||
importDialogVisible.value = true
|
||||
}
|
||||
|
||||
function onImportFileChange(_file: UploadFile, files: UploadFile[]) {
|
||||
importFileList.value = files.slice(-1)
|
||||
}
|
||||
|
||||
function onImportFileRemove() {
|
||||
importFileList.value = []
|
||||
}
|
||||
|
||||
async function submitImport() {
|
||||
const raw = importFileList.value[0]?.raw
|
||||
if (!raw) {
|
||||
ElMessage.warning('请选择要导入的文件')
|
||||
return
|
||||
}
|
||||
importLoading.value = true
|
||||
importResult.value = null
|
||||
try {
|
||||
const res: any = await reimburseApi.importExcel(raw)
|
||||
const data = pickBxData(res) || res?.data || res
|
||||
importResult.value = data
|
||||
const ok = Number(data?.successGroups || 0)
|
||||
const fail = Number(data?.failGroups || 0)
|
||||
if (data?.tip) {
|
||||
ElMessage.warning(String(data.tip))
|
||||
} else if (ok > 0 && fail === 0) {
|
||||
ElMessage.success(`成功导入 ${ok} 组`)
|
||||
} else if (ok > 0) {
|
||||
ElMessage.warning(`部分成功:成功 ${ok} 组,失败 ${fail} 组`)
|
||||
} else if (fail > 0) {
|
||||
ElMessage.error('导入失败,请查看结果清单或下载结果表')
|
||||
} else {
|
||||
ElMessage.warning('没有导入任何数据')
|
||||
}
|
||||
if (ok > 0) await refreshList()
|
||||
} finally {
|
||||
importLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function downloadImportResultFile() {
|
||||
const b64 = importResult.value?.resultFileBase64
|
||||
if (!b64) {
|
||||
ElMessage.warning('暂无结果文件可下载')
|
||||
return
|
||||
}
|
||||
const binary = atob(String(b64))
|
||||
const bytes = new Uint8Array(binary.length)
|
||||
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i)
|
||||
const blob = new Blob([bytes], {
|
||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
})
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = String(importResult.value?.resultFileName || '报销申请导入结果.xlsx')
|
||||
link.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
refreshList()
|
||||
})
|
||||
@@ -1484,6 +1831,24 @@ function viewDetailSummary({ columns, data }: { columns: any[]; data: any[] }) {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.import-hint {
|
||||
margin: 0 0 12px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.import-tip {
|
||||
margin: 0 0 8px;
|
||||
color: #d97706;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.import-result {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.invoice-balance-tip {
|
||||
margin: 0 0 10px;
|
||||
font-size: 13px;
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<section class="panel">
|
||||
<PlPlanTypeTabs v-model="activePlanTypeTab" @change="handlePlanTypeTabChange" />
|
||||
<PlPlanTypeTabs
|
||||
:model-value="activePlanTypeTab"
|
||||
:tabs="PL_PLAN_LIST_TABS"
|
||||
@change="handlePlanTypeTabChange"
|
||||
/>
|
||||
|
||||
<!-- 筛选区 -->
|
||||
<div class="filter-block">
|
||||
@@ -17,7 +21,12 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="跟进状态">
|
||||
<el-select v-model="searchForm.followStatus" placeholder="全部" clearable>
|
||||
<el-select
|
||||
v-model="searchForm.followStatus"
|
||||
placeholder="全部"
|
||||
clearable
|
||||
:disabled="activePlanTypeTab === 'pending_arrange'"
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in PL_PLAN_FOLLOW_STATUS_OPTIONS"
|
||||
:key="opt.value"
|
||||
@@ -226,9 +235,7 @@
|
||||
align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getPlanTypeTagType(row.planType)" size="small">
|
||||
{{ getPlanTypeLabel(row.planType) }}
|
||||
</el-tag>
|
||||
{{ getPlanTypeLabel(row.planType) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
@@ -557,8 +564,9 @@ import {
|
||||
getPlPlanFollowStatusTagType,
|
||||
normalizePlPersonnelSummaryDisplay,
|
||||
PL_PLAN_FOLLOW_STATUS_OPTIONS,
|
||||
plPlanTypeTabToFilter,
|
||||
type PlPlanTypeTab,
|
||||
PL_PLAN_LIST_TABS,
|
||||
plPlanListTabToFilters,
|
||||
type PlPlanListTab,
|
||||
type PlPrintSlipCommand
|
||||
} from '@/utils/pl-display'
|
||||
import { syncViewRowFromFlatList } from '@/utils/view-dialog-refresh'
|
||||
@@ -742,7 +750,7 @@ async function handlePlanMutationSuccess(payload?: { planId?: string; openArrang
|
||||
const selectedPlan = computed(() => (selectedRows.value.length === 1 ? selectedRows.value[0] : null))
|
||||
const dateRange = ref<string[]>([])
|
||||
const quickSearch = ref('')
|
||||
const activePlanTypeTab = ref<PlPlanTypeTab>('all')
|
||||
const activePlanTypeTab = ref<PlPlanListTab>('all')
|
||||
|
||||
const searchForm = reactive({
|
||||
planNo: '',
|
||||
@@ -759,11 +767,6 @@ const pagination = reactive({
|
||||
total: 0
|
||||
})
|
||||
|
||||
function getPlanTypeTagType(type: string) {
|
||||
const map: Record<string, string> = { direct: 'primary', inbound: 'success', outbound: 'warning' }
|
||||
return map[type] || 'info'
|
||||
}
|
||||
|
||||
function getPlanTypeLabel(type?: string | null) {
|
||||
if (!type) return '—'
|
||||
const map: Record<string, string> = { direct: '直销单', inbound: '入库单', outbound: '出库单' }
|
||||
@@ -788,11 +791,12 @@ async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const keyword = quickSearch.value.trim()
|
||||
const tabFilters = plPlanListTabToFilters(activePlanTypeTab.value)
|
||||
const res = await planApi.list({
|
||||
q: keyword || undefined,
|
||||
planNo: keyword ? undefined : searchForm.planNo || undefined,
|
||||
planType: searchForm.planType || undefined,
|
||||
followStatus: searchForm.followStatus || undefined,
|
||||
planType: tabFilters.planType || undefined,
|
||||
followStatus: tabFilters.followStatus || searchForm.followStatus || undefined,
|
||||
customerName: keyword ? undefined : searchForm.customerName || undefined,
|
||||
productName: keyword ? undefined : searchForm.productName || undefined,
|
||||
licensePlate: keyword ? undefined : searchForm.licensePlate || undefined,
|
||||
@@ -814,8 +818,17 @@ function handleSearch() {
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function handlePlanTypeTabChange(tab: PlPlanTypeTab) {
|
||||
searchForm.planType = plPlanTypeTabToFilter(tab)
|
||||
function handlePlanTypeTabChange(tab: PlPlanListTab | string) {
|
||||
const nextTab = tab as PlPlanListTab
|
||||
const prevTab = activePlanTypeTab.value
|
||||
activePlanTypeTab.value = nextTab
|
||||
const filters = plPlanListTabToFilters(nextTab)
|
||||
searchForm.planType = filters.planType
|
||||
if (nextTab === 'pending_arrange') {
|
||||
searchForm.followStatus = 'pending_arrange'
|
||||
} else if (prevTab === 'pending_arrange') {
|
||||
searchForm.followStatus = ''
|
||||
}
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user