Files
obsidian-wiki/docs/business-facts.md
T
Codex 317b0e6feb feat(business): add staff queries and incremental sync
Add order_staff normalization, salesperson query templates, guarded read-only SQL fallback, and incremental Kingdee sync cursor handling.

Add a user-level systemd timer manager for daily Kingdee business sync and document the new workflow.

Validation: python3 -m unittest discover -v; python3 -m compileall obsidian_wiki tests scripts; git diff --check.
2026-07-06 21:08:00 +00:00

8.4 KiB

Business Facts

Business facts are structured operational data used for exact counts, sums, and rankings. They complement the markdown wiki; they do not replace it.

Purpose

Use <vault>/_meta/business-facts.sqlite for questions where markdown is the wrong source of truth:

  • how many sales orders were received in a month
  • monthly sales amount
  • top customers for a month
  • customer order trend for a fixed period
  • sales order counts, lists, and amounts by salesperson

The wiki remains the place for context: customer background, project history, sales notes, email-derived signals, and later human-readable summaries.

Commands

obsidian-wiki business sync kingdee --input ./kingdee-orders.json

obsidian-wiki business config kingdee
# Fill .runtime/kingdee.env, then test the login/refresh flow:
obsidian-wiki business sync kingdee --dry-run-auth

obsidian-wiki business sync kingdee \
  --start-bizdate 2026-06-01 \
  --end-bizdate 2026-06-30 \
  --billstatus C

obsidian-wiki business sync kingdee --start-modifytime "2026-07-01 00:00:00"
obsidian-wiki business sync kingdee --incremental
obsidian-wiki business query "2026年6月接了多少单"
obsidian-wiki business query "2026-06 sales amount"
obsidian-wiki business query "2026-06 top customer"
obsidian-wiki business query "徐丽最近一周订单"
obsidian-wiki business query "徐丽最近一周接了多少单"
obsidian-wiki business query "徐丽最近一周销售额" --all-staff
obsidian-wiki business query "按组织列出订单" --fallback-sql "SELECT org_name, COUNT(*) AS order_count FROM orders GROUP BY org_name"
obsidian-wiki business sql "SELECT COUNT(*) AS order_count FROM orders"

When --input is omitted, sync reads .runtime/kingdee.env, gets or refreshes an access token, calls the official Kingdee sales order API, and stores the returned rows. If the file does not exist, business sync kingdee creates the template and asks you to fill it before retrying.

For unattended sync, install the user-level systemd timer:

bash scripts/business_sync_service.sh start --vault /path/to/vault
bash scripts/business_sync_service.sh run-now --vault /path/to/vault
bash scripts/business_sync_service.sh status

The timer defaults to daily 07:10 with a 15-minute randomized delay. It runs incremental Kingdee sync with a 48-hour lookback window.

The config template includes:

  • Token defaults follow Kingdee's enhanced OpenAPI token authentication doc: https://vip.kingdee.com/knowledge/specialDetail/226337046514476288?category=239331354741842688&id=489812471545485056&productLineId=29.
  • KINGDEE_SALORDER_API_URL: defaults to the official querySalOrder endpoint.
  • KINGDEE_TOKEN_URL: defaults to the official enhanced-token endpoint /kapi/oauth2/getToken.
  • KINGDEE_CLIENT_ID, KINGDEE_CLIENT_SECRET, KINGDEE_USERNAME, KINGDEE_ACCOUNT_ID, KINGDEE_LANGUAGE: fixed fields used to build the official getToken request body.
  • nonce and timestamp: generated automatically for every token request; timestamp uses the official zero-time-zone format by default.
  • KINGDEE_TOKEN_BODY_EXTRA_JSON: optional fixed extra fields merged into the generated token request body.
  • KINGDEE_TOKEN_BODY_JSON: advanced override for nonstandard token request bodies.
  • KINGDEE_TOKEN_HEADERS_JSON: optional token request headers, such as app-specific headers.
  • KINGDEE_TOKEN_FIELD, KINGDEE_TOKEN_EXPIRES_IN_FIELD, KINGDEE_TOKEN_EXPIRES_IN_UNIT, KINGDEE_REFRESH_TOKEN_FIELD: dot paths and units used to read token response fields. The official expires_in unit is milliseconds.
  • KINGDEE_REFRESH_URL and KINGDEE_REFRESH_BODY_JSON: optional refresh request. Use {refresh_token} in the JSON body template.
  • KINGDEE_ACGW_IDENTITY: value sent as the required x-acgw-identity sales-order API header.
  • KINGDEE_PAGE_SIZE: default sales-order page size.

KINGDEE_LOGIN_URL, KINGDEE_LOGIN_BODY_JSON, and KINGDEE_LOGIN_HEADERS_JSON are accepted as aliases for the token settings. KINGDEE_ACCESS_TOKEN can also be set for one-off manual testing. Fetched tokens are cached in .runtime/kingdee-token.json; both local files are ignored by git and should not be copied into .env.example.

The API response can still be imported from a JSON file for replay or testing. JSON may be a list of order objects or an object containing orders, rows, data, records, or items.

Database

Default path: <vault>/_meta/business-facts.sqlite.

Tables:

  • sync_runs: one row per Kingdee sync attempt, including status, cursor, record counts, and error text.
  • raw_snapshots: content-hashed raw JSON payloads for audit and replay.
  • orders: one normalized row per sales order. It keeps the query-critical fields (order_id, order_no, order_date, customer, status, organization, department, operator, currency, total amount, net amount, tax amount, address, comment) plus raw_json for the full original order header.
  • order_items: normalized material detail lines. It keeps material number/name/model, quantity, unit, price, tax-inclusive price, net amount, tax-inclusive amount, tax amount, delivery date, project, warehouse, and raw_json for the full original line.
  • order_deliveries: normalized delivery-plan rows from billentry[].orderdeliverentry[]. It keeps planned demand date, planned delivery date, planned quantity, planned base quantity, unit, receive address/place, remark, transport lead time, and raw_json for the full original delivery-plan row.
  • order_staff: sales staff roles extracted from the order header. Roles are primary_sales, sales_2, sales_3, sales_4, and followup.
  • sync_state: saved Kingdee incremental cursor.
  • customers: customer lookup data observed from orders.
  • products: product lookup data observed from order lines.

Order upsert key: order_id, falling back to order_no when no explicit ID is present. Re-syncing the same order updates the existing row and replaces that order's detail lines and delivery-plan rows.

total_amount is the statistic field used by monthly sales queries. For Kingdee sales orders it prefers totalallamount (价税合计) and falls back to other total fields only when the tax-inclusive total is absent. total_net_amount, total_tax_amount, and total_all_amount are stored separately so later reports can choose a different accounting basis without re-fetching historical payloads.

Counting Basis

V1 statistics use fixed templates, not free-form SQL generated by an LLM.

  • Monthly order count: COUNT(DISTINCT order_no) by order_date.
  • Monthly sales amount: SUM(total_amount) by order_date.
  • Monthly top customers: group by customer name or ID and rank by sales amount.
  • Customer trend: group matching customer orders by order month.
  • Staff order list/count/amount: join orders to order_staff, defaulting to primary_sales.

Every answer includes the statistic, counting basis, data source, and last successful sync time. If the last successful sync is before the query date, the answer warns that newer data may be missing.

Salesperson queries default to the main Kingdee salesperson (operator_operatorname). Use --all-staff, or phrases such as "全部角色" or "含跟单", to include sales 2/3/4 and follow-up roles.

If no fixed template matches, callers may provide guarded SQL through business sql or business query --fallback-sql. The SQL fallback is still restricted:

  • only one SELECT or WITH ... SELECT statement
  • only structured tables: orders, order_items, order_deliveries, order_staff, customers, products, sync_runs
  • no raw_json columns and no raw_snapshots
  • no write, schema, attach, pragma, or vacuum statements
  • default result limit is 100 rows, capped at 1000

Incremental Sync

obsidian-wiki business sync kingdee --incremental uses Kingdee start_modifytime.

  • First run without a cursor: fetches the last 7 days.
  • Later runs: starts from the saved cursor minus 48 hours.
  • Explicit --start-modifytime or --since wins over the computed cursor.

The 48-hour lookback intentionally re-fetches recent orders so late edits and boundary cases are updated rather than missed. Upserts keep the database deduplicated by order_id.

Boundary With Wiki Ingest

Do not create one markdown page per order or order line. That makes the wiki noisy and produces unreliable statistics.

If business summaries are added later, generate compact daily or monthly summary sources from this database and pass those summaries through wiki-ingest. The database remains the fact source for exact numbers.