feat(business): add Kingdee sales order API sync

Implement private Kingdee config generation, token login/refresh caching, paginated sales order fetching, official response-field normalization, and CLI filters for deterministic business fact sync. Update docs to keep credentials in .runtime and cover dry-run auth.

Validation: python3 -m unittest discover -v; python3 -m compileall obsidian_wiki tests; git diff --check.
This commit is contained in:
Codex
2026-07-06 19:47:40 +00:00
parent 4dfef7e099
commit 5f257a7ccb
9 changed files with 785 additions and 43 deletions
+80 -3
View File
@@ -34,6 +34,13 @@ from obsidian_wiki.file_assets import (
search_assets,
stage_files,
)
from obsidian_wiki.kingdee_api import (
build_sales_order_filters,
ensure_kingdee_config,
get_kingdee_access_token,
load_kingdee_config,
token_cache_path_for_config,
)
HOME = Path.home()
GLOBAL_CONFIG_DIR = HOME / ".obsidian-wiki"
@@ -506,7 +513,27 @@ def cmd_business_sync_kingdee(args: argparse.Namespace) -> int:
if args.input:
records, payload = load_kingdee_payload_from_json(args.input)
else:
records, payload = fetch_kingdee_payload(api_url=args.api_url, token=args.token, since=args.since)
config_path = getattr(args, "config", None)
created = ensure_kingdee_config(config_path)
overrides = _kingdee_cli_overrides(args)
config = load_kingdee_config(config_path)
config.update(overrides)
if created and not (config.get("KINGDEE_ACCESS_TOKEN") or config.get("KINGDEE_TOKEN_URL")):
raise RuntimeError(f"Kingdee config template created at {Path(config_path or '.runtime/kingdee.env').expanduser()}; fill it and rerun")
if args.dry_run_auth:
token = get_kingdee_access_token(config, cache_path=token_cache_path_for_config(config_path))
print(f"✅ Kingdee auth OK: token length {len(token)}")
return 0
records, payload = fetch_kingdee_payload(
api_url=args.api_url,
token=args.token or args.access_token,
since=args.since,
config_path=config_path,
config_overrides=overrides,
filters=build_sales_order_filters(args),
page_size=args.page_size,
max_pages=args.max_pages,
)
result = sync_kingdee_orders(
vault_path,
records,
@@ -523,6 +550,29 @@ def cmd_business_sync_kingdee(args: argparse.Namespace) -> int:
return 0
def cmd_business_config_kingdee(args: argparse.Namespace) -> int:
config_path = Path(args.config or ".runtime/kingdee.env").expanduser()
created = ensure_kingdee_config(config_path)
state = "created" if created else "exists"
print(f"✅ Kingdee config {state}: {config_path}")
return 0
def _kingdee_cli_overrides(args: argparse.Namespace) -> dict[str, str]:
overrides: dict[str, str] = {}
if getattr(args, "api_url", None):
overrides["KINGDEE_SALORDER_API_URL"] = args.api_url
if getattr(args, "token", None):
overrides["KINGDEE_ACCESS_TOKEN"] = args.token
if getattr(args, "access_token", None):
overrides["KINGDEE_ACCESS_TOKEN"] = args.access_token
if getattr(args, "identity", None):
overrides["KINGDEE_ACGW_IDENTITY"] = args.identity
if getattr(args, "page_size", None):
overrides["KINGDEE_PAGE_SIZE"] = str(args.page_size)
return overrides
def cmd_business_query(args: argparse.Namespace) -> int:
vault_path = _require_vault(args)
question = " ".join(args.question).strip()
@@ -663,6 +713,13 @@ def build_parser() -> argparse.ArgumentParser:
bp = sub.add_parser("business", help="sync and query structured business facts")
business_sub = bp.add_subparsers(dest="business_command")
bcp = business_sub.add_parser("config", help="create local business integration config files")
business_config_sub = bcp.add_subparsers(dest="business_config_target")
bckp = business_config_sub.add_parser("kingdee", help="create a local Kingdee API config template")
bckp.add_argument("--config", metavar="PATH", help="Kingdee config path (defaults to .runtime/kingdee.env)")
bckp.set_defaults(func=cmd_business_config_kingdee)
bsp = business_sub.add_parser("sync", help="sync business facts from an external system")
business_sync_sub = bsp.add_subparsers(dest="business_sync_source")
@@ -670,9 +727,29 @@ def build_parser() -> argparse.ArgumentParser:
_add_vault_arg(bkp)
bkp.add_argument("--db", metavar="PATH", help="SQLite DB path (defaults to <vault>/_meta/business-facts.sqlite)")
bkp.add_argument("--input", metavar="PATH", help="JSON payload exported from or fetched from Kingdee")
bkp.add_argument("--api-url", help="Kingdee-compatible JSON API URL; defaults to KINGDEE_API_URL")
bkp.add_argument("--token", help="API bearer token; defaults to KINGDEE_API_TOKEN")
bkp.add_argument("--config", metavar="PATH", help="Kingdee config path (defaults to .runtime/kingdee.env)")
bkp.add_argument("--api-url", help="Kingdee sales order API URL; defaults to KINGDEE_SALORDER_API_URL")
bkp.add_argument("--token", help="direct access token override; compatibility alias for --access-token")
bkp.add_argument("--access-token", help="direct access token override")
bkp.add_argument("--identity", help="x-acgw-identity override")
bkp.add_argument("--since", help="optional updated_since cursor passed to the API and sync log")
bkp.add_argument("--start-bizdate")
bkp.add_argument("--end-bizdate")
bkp.add_argument("--start-createtime")
bkp.add_argument("--end-createtime")
bkp.add_argument("--start-modifytime")
bkp.add_argument("--end-modifytime")
bkp.add_argument("--start-auditdate")
bkp.add_argument("--end-auditdate")
bkp.add_argument("--id", action="append", help="sales order ID; repeat or comma-separate")
bkp.add_argument("--billno", action="append", help="sales order bill number; repeat or comma-separate")
bkp.add_argument("--billstatus", action="append", help="A/B/C status; repeat or comma-separate")
bkp.add_argument("--org-number", action="append", help="sales org code; repeat or comma-separate")
bkp.add_argument("--customer-number", action="append", help="customer code; repeat or comma-separate")
bkp.add_argument("--customer-createorg-number", action="append", help="customer create org code; repeat or comma-separate")
bkp.add_argument("--page-size", type=int, help="Kingdee pageSize")
bkp.add_argument("--max-pages", type=int, default=100, help="maximum pages to fetch")
bkp.add_argument("--dry-run-auth", action="store_true", help="fetch/refresh access token and exit without syncing orders")
bkp.add_argument("--json", action="store_true", help="print machine-readable sync result")
bkp.set_defaults(func=cmd_business_sync_kingdee)