Files
obsidian-wiki/tests/test_business_facts.py
T
Codex 4c5454da90 feat(business): exclude intercompany transfer orders
Allow operators to configure internal company names and identifiers, then apply the filter consistently across business templates and query routing. Preserve direct external-customer orders and provide explicit CLI and environment overrides for internal-flow audits.

Validation: python -m unittest tests.test_business_facts tests.test_wiki_skills tests.test_user_ops -v (25 tests)
2026-07-10 05:40:54 +00:00

1033 lines
43 KiB
Python

import json
import os
import sqlite3
import tempfile
import unittest
from contextlib import redirect_stdout
from io import StringIO
from pathlib import Path
from unittest.mock import patch
from obsidian_wiki.business_facts import (
business_schema,
business_db_path_for_vault,
compute_incremental_start_modifytime,
execute_business_sql,
execute_business_template,
query_business_question,
sync_kingdee_orders,
)
class BusinessFactsTests(unittest.TestCase):
def setUp(self):
self.tempdir = tempfile.TemporaryDirectory()
self.vault = Path(self.tempdir.name)
(self.vault / "_meta").mkdir()
self.db_path = business_db_path_for_vault(self.vault)
def tearDown(self):
self.tempdir.cleanup()
def seed_template_fixture(self):
sync_kingdee_orders(
self.vault,
[
{
"id": "1001",
"billno": "SO-001",
"bizdate": "2026-06-03",
"customer_number": "C01",
"customer_name": "Customer A",
"totalallamount": "100.00",
"settlecurrency_number": "CNY",
"billstatus": "C",
"orderstatus": "D",
"closestatus": "A",
"org_name": "Org A",
"dept_name": "Dept A",
"operator_operatorname": "徐丽",
"billentry": [
{
"id": "L1",
"material_masterid_number": "P01",
"material_masterid_name": "Part 1",
"qty": "2",
"amountandtax": "60",
"project_name": "Project A",
"warehouse_name": "Warehouse A",
"orderdeliverentry": [
{
"id": "D1",
"d_plandate": "2026-06-10",
"d_plandeliverydate": "2026-06-08",
"d_planqty": "2",
}
],
},
{
"id": "L2",
"material_masterid_number": "P02",
"material_masterid_name": "Part 2",
"qty": "1",
"amountandtax": "40",
"project_name": "Project B",
"warehouse_name": "Warehouse B",
},
],
},
{
"id": "1002",
"billno": "SO-002",
"bizdate": "2026-06-04",
"customer_number": "C02",
"customer_name": "Customer B",
"totalallamount": "300.00",
"settlecurrency_number": "CNY",
"billstatus": "B",
"orderstatus": "C",
"closestatus": "A",
"org_name": "Org B",
"dept_name": "Dept B",
"operator_operatorname": "王五",
"h5a3_basedatafield2_operatorname": "徐丽",
"billentry": [
{
"id": "L1",
"material_masterid_number": "P01",
"material_masterid_name": "Part 1",
"qty": "3",
"amountandtax": "300",
"project_name": "Project A",
"warehouse_name": "Warehouse A",
"orderdeliverentry": [
{
"id": "D2",
"d_plandate": "2026-06-11",
"d_planqty": "3",
}
],
}
],
},
],
synced_at="2026-07-06T09:00:00Z",
)
def test_monthly_order_count_deduplicates_order_lines(self):
sync_kingdee_orders(
self.vault,
[
{
"order_id": "1001",
"order_no": "SO-001",
"order_date": "2026-06-03",
"customer_id": "C01",
"customer_name": "Customer A",
"total_amount": 1200,
"updated_at": "2026-06-03T08:00:00Z",
"items": [
{"line_id": "1", "product_id": "P01", "product_name": "Part 1", "quantity": 10, "amount": 500},
{"line_id": "2", "product_id": "P02", "product_name": "Part 2", "quantity": 20, "amount": 700},
],
},
{
"order_id": "1002",
"order_no": "SO-002",
"order_date": "2026-06-15",
"customer_id": "C02",
"customer_name": "Customer B",
"total_amount": 900,
"updated_at": "2026-06-15T08:00:00Z",
"items": [
{"line_id": "1", "product_id": "P03", "product_name": "Part 3", "quantity": 5, "amount": 900},
],
},
{
"order_id": "1003",
"order_no": "SO-003",
"order_date": "2026-07-01",
"customer_id": "C01",
"customer_name": "Customer A",
"total_amount": 300,
"updated_at": "2026-07-01T08:00:00Z",
},
],
source_payload={"orders": 3},
synced_at="2026-07-06T09:00:00Z",
)
result = query_business_question(self.vault, "2026-06 order count")
self.assertEqual(result["template"], "monthly_order_count")
self.assertEqual(result["data"]["order_count"], 2)
self.assertIn("2", result["answer"])
self.assertIn("sales order number", result["answer"])
self.assertIn("2026-07-06T09:00:00Z", result["answer"])
def test_repeated_sync_updates_existing_orders_without_duplicates(self):
first = [
{
"order_id": "1001",
"order_no": "SO-001",
"order_date": "2026-06-03",
"customer_id": "C01",
"customer_name": "Customer A",
"total_amount": 1200,
"updated_at": "2026-06-03T08:00:00Z",
}
]
second = [{**first[0], "total_amount": 1500, "updated_at": "2026-06-04T08:00:00Z"}]
sync_kingdee_orders(self.vault, first, source_payload={"orders": first}, synced_at="2026-07-06T09:00:00Z")
sync_kingdee_orders(self.vault, second, source_payload={"orders": second}, synced_at="2026-07-06T10:00:00Z")
count = query_business_question(self.vault, "2026-06 order count")
amount = query_business_question(self.vault, "2026-06 sales amount")
self.assertEqual(count["data"]["order_count"], 1)
self.assertEqual(amount["data"]["sales_amount"], 1500.0)
self.assertIn("2026-07-06T10:00:00Z", amount["answer"])
def test_query_warns_when_last_sync_is_before_today(self):
sync_kingdee_orders(
self.vault,
[
{
"order_id": "1001",
"order_no": "SO-001",
"order_date": "2026-06-03",
"customer_name": "Customer A",
"total_amount": 1200,
}
],
source_payload={"orders": 1},
synced_at="2026-07-03T09:00:00Z",
)
result = query_business_question(self.vault, "2026-06 order count", now="2026-07-06T00:00:00Z")
self.assertTrue(result["is_stale"])
self.assertIn("may not include data after 2026-07-03T09:00:00Z", result["answer"])
def test_official_kingdee_sales_order_shape_maps_into_business_facts(self):
payload = {
"data": {
"rows": [
{
"id": "3551625973803228160",
"billno": "SO-REAL-001",
"billstatus": "C",
"orderstatus": "C",
"closestatus": "A",
"bizdate": "2026-06-08",
"modifytime": "2026-06-09 10:30:00",
"customer_number": "CUST-001",
"customer_name": "真实客户",
"totalallamount": "1234.56",
"totalamount": "1000.00",
"totaltaxamount": "234.56",
"org_number": "ORG-001",
"org_name": "销售组织",
"dept_number": "DEPT-001",
"dept_name": "销售部",
"operator_operatornumber": "OP-001",
"operator_operatorname": "张三",
"settlecurrency_number": "CNY",
"billentry": [
{
"id": "4568544278879012864",
"seq": 1,
"material_masterid_number": "MAT-001",
"material_masterid_name": "车载连接器",
"material_masterid_modelnum": "MODEL-A",
"qty": "711.97",
"priceandtax": "1.73",
"amountandtax": "1234.56",
"amount": "1000.00",
"unit_number": "PCS",
"unit_name": "个",
"deliverydate": "2026-06-20",
"project_number": "PRJ-001",
"project_name": "重点项目",
"warehouse_number": "WH-001",
"warehouse_name": "成品仓",
"orderdeliverentry": [
{
"id": "7690495588165568512",
"d_plandate": "2026-06-18",
"d_plandeliverydate": "2026-06-16",
"d_planqty": "500.00",
"d_planbaseqty": "500.00",
"d_planunit_number": "PCS",
"d_planunit_name": "个",
"d_receiveaddress": "测试地址",
"d_receiveaddressf7_number": "ADDR-001",
"d_receiveaddressf7_name": "测试收货点",
"d_remark": "首批",
"d_transportleadtime": 2,
}
],
}
],
}
]
}
}
sync_kingdee_orders(
self.vault,
payload["data"]["rows"],
source_payload=payload,
synced_at="2026-07-06T09:00:00Z",
)
count = query_business_question(self.vault, "2026-06 order count")
amount = query_business_question(self.vault, "2026-06 sales amount")
self.assertEqual(count["data"]["order_count"], 1)
self.assertEqual(amount["data"]["sales_amount"], 1234.56)
with sqlite3.connect(self.db_path) as conn:
conn.row_factory = sqlite3.Row
order = conn.execute(
"""
SELECT billstatus, orderstatus, closestatus, org_number, org_name,
dept_number, dept_name, operator_number, operator_name,
total_net_amount, total_tax_amount, total_all_amount, raw_json
FROM orders
WHERE order_id=?
""",
("3551625973803228160",),
).fetchone()
item = conn.execute(
"""
SELECT seq, product_id, product_name, material_model, unit_number, unit_name,
price_and_tax, amount, amount_and_tax, delivery_date,
project_number, project_name, warehouse_number, warehouse_name, raw_json
FROM order_items
WHERE order_id=? AND line_id=?
""",
("3551625973803228160", "4568544278879012864"),
).fetchone()
delivery = conn.execute(
"""
SELECT delivery_id, plan_date, plan_delivery_date, plan_qty, plan_base_qty,
plan_unit_number, plan_unit_name, receive_address,
receive_place_number, receive_place_name, remark, transport_lead_time, raw_json
FROM order_deliveries
WHERE order_id=? AND line_id=?
""",
("3551625973803228160", "4568544278879012864"),
).fetchone()
self.assertEqual(order["billstatus"], "C")
self.assertEqual(order["org_number"], "ORG-001")
self.assertEqual(order["operator_name"], "张三")
self.assertEqual(order["total_net_amount"], 1000.0)
self.assertEqual(order["total_tax_amount"], 234.56)
self.assertEqual(order["total_all_amount"], 1234.56)
self.assertEqual(json.loads(order["raw_json"])["billno"], "SO-REAL-001")
self.assertEqual(item["seq"], 1)
self.assertEqual(item["material_model"], "MODEL-A")
self.assertEqual(item["price_and_tax"], 1.73)
self.assertEqual(item["amount_and_tax"], 1234.56)
self.assertEqual(json.loads(item["raw_json"])["material_masterid_number"], "MAT-001")
self.assertEqual(delivery["delivery_id"], "7690495588165568512")
self.assertEqual(delivery["plan_date"], "2026-06-18")
self.assertEqual(delivery["plan_qty"], 500.0)
self.assertEqual(delivery["transport_lead_time"], 2)
self.assertEqual(json.loads(delivery["raw_json"])["d_remark"], "首批")
def test_sync_extracts_all_kingdee_staff_roles(self):
sync_kingdee_orders(
self.vault,
[
{
"id": "1001",
"billno": "SO-STAFF-001",
"bizdate": "2026-07-01",
"customer_name": "Customer A",
"totalallamount": "100.00",
"operator_id": "S1",
"operator_operatornumber": "XS19",
"operator_operatorname": "徐丽",
"h5a3_basedatafield2_id": "S2",
"h5a3_basedatafield2_operatornumber": "XS20",
"h5a3_basedatafield2_operatorname": "李二",
"h5a3_basedatafield3_id": "S3",
"h5a3_basedatafield3_operatornumber": "XS21",
"h5a3_basedatafield3_operatorname": "王三",
"h5a3_basedatafield5_id": "S4",
"h5a3_basedatafield5_operatornumber": "XS22",
"h5a3_basedatafield5_operatorname": "赵四",
"h5a3_basedatafield4_id": "F1",
"h5a3_basedatafield4_operatornumber": "GD01",
"h5a3_basedatafield4_operatorname": "跟单甲",
}
],
synced_at="2026-07-06T09:00:00Z",
)
with sqlite3.connect(self.db_path) as conn:
conn.row_factory = sqlite3.Row
rows = conn.execute(
"""
SELECT role, staff_id, staff_number, staff_name
FROM order_staff
WHERE order_id=?
ORDER BY role
""",
("1001",),
).fetchall()
self.assertEqual(
[(row["role"], row["staff_id"], row["staff_number"], row["staff_name"]) for row in rows],
[
("followup", "F1", "GD01", "跟单甲"),
("primary_sales", "S1", "XS19", "徐丽"),
("sales_2", "S2", "XS20", "李二"),
("sales_3", "S3", "XS21", "王三"),
("sales_4", "S4", "XS22", "赵四"),
],
)
def test_staff_query_defaults_to_primary_sales_and_can_include_all_roles(self):
sync_kingdee_orders(
self.vault,
[
{
"id": "1001",
"billno": "SO-PRIMARY",
"bizdate": "2026-07-03",
"customer_name": "Customer A",
"totalallamount": "100.00",
"operator_operatorname": "徐丽",
"operator_operatornumber": "XS19",
},
{
"id": "1002",
"billno": "SO-SECONDARY",
"bizdate": "2026-07-03",
"customer_name": "Customer B",
"totalallamount": "200.00",
"operator_operatorname": "其他销售",
"operator_operatornumber": "XS99",
"h5a3_basedatafield2_operatorname": "徐丽",
"h5a3_basedatafield2_operatornumber": "XS19",
},
],
synced_at="2026-07-06T09:00:00Z",
)
primary = query_business_question(
self.vault,
"徐丽最近一周接了多少单",
now="2026-07-06T12:00:00Z",
all_staff=False,
)
all_roles = query_business_question(
self.vault,
"徐丽最近一周接了多少单 全部角色",
now="2026-07-06T12:00:00Z",
)
amount = query_business_question(
self.vault,
"徐丽最近一周销售额",
now="2026-07-06T12:00:00Z",
all_staff=True,
)
self.assertEqual(primary["template"], "staff_order_count")
self.assertEqual(primary["data"]["order_count"], 1)
self.assertEqual(primary["data"]["staff_roles"], ["primary_sales"])
self.assertEqual(all_roles["data"]["order_count"], 2)
self.assertIn("sales_2", all_roles["data"]["staff_roles"])
self.assertEqual(amount["template"], "staff_sales_amount")
self.assertEqual(amount["data"]["sales_amount"], 300.0)
def test_staff_order_list_uses_date_range_and_returns_orders(self):
sync_kingdee_orders(
self.vault,
[
{
"id": "1001",
"billno": "SO-IN-RANGE",
"bizdate": "2026-07-03",
"customer_name": "Customer A",
"totalallamount": "100.00",
"operator_operatorname": "徐丽",
"operator_operatornumber": "XS19",
},
{
"id": "1002",
"billno": "SO-OUT-RANGE",
"bizdate": "2026-06-20",
"customer_name": "Customer B",
"totalallamount": "200.00",
"operator_operatorname": "徐丽",
"operator_operatornumber": "XS19",
},
],
synced_at="2026-07-06T09:00:00Z",
)
result = query_business_question(self.vault, "徐丽最近一周订单", now="2026-07-06T12:00:00Z")
self.assertEqual(result["template"], "staff_order_list")
self.assertEqual(result["data"]["start_date"], "2026-06-30")
self.assertEqual(result["data"]["end_date_exclusive"], "2026-07-07")
self.assertEqual([order["order_no"] for order in result["data"]["orders"]], ["SO-IN-RANGE"])
def test_sql_fallback_allows_readonly_structured_queries_and_blocks_raw_or_writes(self):
sync_kingdee_orders(
self.vault,
[
{
"id": "1001",
"billno": "SO-001",
"bizdate": "2026-07-03",
"customer_name": "Customer A",
"totalallamount": "100.00",
"operator_operatorname": "徐丽",
},
{
"id": "1002",
"billno": "SO-002",
"bizdate": "2026-07-04",
"customer_name": "Customer B",
"totalallamount": "200.00",
"operator_operatorname": "徐丽",
},
],
synced_at="2026-07-06T09:00:00Z",
)
result = execute_business_sql(
self.db_path,
"""
SELECT s.staff_name, COUNT(DISTINCT o.order_no) AS order_count
FROM orders o
JOIN order_staff s ON s.order_id = o.order_id
WHERE s.staff_name = '徐丽'
GROUP BY s.staff_name
""",
)
self.assertEqual(result["template"], "sql_fallback")
self.assertEqual(result["rows"], [{"staff_name": "徐丽", "order_count": 2}])
with self.assertRaisesRegex(RuntimeError, "only SELECT"):
execute_business_sql(self.db_path, "DELETE FROM orders")
with self.assertRaisesRegex(RuntimeError, "raw_json"):
execute_business_sql(self.db_path, "SELECT raw_json FROM orders")
with self.assertRaisesRegex(RuntimeError, "raw_snapshots"):
execute_business_sql(self.db_path, "SELECT payload_json FROM raw_snapshots")
with self.assertRaisesRegex(RuntimeError, "PRAGMA"):
execute_business_sql(self.db_path, "PRAGMA table_info(orders)")
def test_query_can_use_guarded_fallback_sql_when_no_template_matches(self):
sync_kingdee_orders(
self.vault,
[
{
"id": "1001",
"billno": "SO-001",
"bizdate": "2026-07-03",
"customer_name": "Customer A",
"totalallamount": "100.00",
}
],
synced_at="2026-07-06T09:00:00Z",
)
missing = query_business_question(self.vault, "按组织列出订单")
fallback = query_business_question(
self.vault,
"按组织列出订单",
fallback_sql="SELECT order_no, customer_name FROM orders ORDER BY order_no",
)
self.assertEqual(missing["template"], "no_matching_template")
self.assertEqual(fallback["template"], "sql_fallback")
self.assertEqual(fallback["rows"], [{"order_no": "SO-001", "customer_name": "Customer A"}])
def test_business_schema_describes_templates_fields_and_sql_guardrails(self):
schema = business_schema()
template_names = {template["name"] for template in schema["templates"]}
templates = {template["name"]: template for template in schema["templates"]}
self.assertTrue(
{
"order_count",
"sales_amount",
"customer_count",
"product_count",
"quantity_sum",
"avg_order_amount",
"order_trend",
"sales_trend",
"quantity_trend",
"top_customers",
"top_products",
"top_staff",
"top_orgs",
"top_departments",
"top_projects",
"top_warehouses",
"breakdown_by_customer",
"breakdown_by_product",
"breakdown_by_staff",
"breakdown_by_org",
"breakdown_by_department",
"breakdown_by_status",
"breakdown_by_currency",
"order_list",
"customer_order_list",
"product_order_list",
"staff_order_list",
"customer_summary",
"product_summary",
"staff_summary",
"delivery_plan_count",
"delivery_plan_quantity",
"delivery_plan_list",
"order_status_breakdown",
"billstatus_breakdown",
"close_status_breakdown",
}.issubset(template_names)
)
table_names = {table["name"] for table in schema["tables"]}
self.assertIn("orders", table_names)
self.assertIn("order_items", table_names)
self.assertIn("order_staff", table_names)
self.assertIn("order_deliveries", table_names)
self.assertIn("COUNT(DISTINCT order_no)", schema["metric_guidance"]["order_count"])
self.assertIn("internal company transfer", schema["metric_guidance"]["sales_amount"])
self.assertIn("internal_parties", templates["sales_amount"]["optional_parameters"])
self.assertIn("include_internal_orders", templates["sales_amount"]["optional_parameters"])
self.assertEqual(templates["customer_count"]["required_parameters"], [])
self.assertEqual(templates["product_count"]["required_parameters"], [])
self.assertEqual(templates["customer_summary"]["required_parameters"], ["customer"])
self.assertEqual(templates["product_summary"]["required_parameters"], ["product"])
self.assertEqual(templates["staff_summary"]["required_parameters"], ["staff"])
self.assertIn("raw_json", schema["sql_guardrails"]["forbidden_columns"])
self.assertIn("raw_snapshots", schema["sql_guardrails"]["forbidden_tables"])
def test_execute_business_templates_cover_core_totals_rankings_lists_and_delivery_plans(self):
self.seed_template_fixture()
count = execute_business_template(self.db_path, "order_count", start_date="2026-06-01", end_date="2026-07-01")
sales = execute_business_template(self.db_path, "sales_amount", year=2026, month=6)
customers = execute_business_template(self.db_path, "customer_count", year=2026, month=6)
products = execute_business_template(self.db_path, "product_count", year=2026, month=6)
quantity = execute_business_template(self.db_path, "quantity_sum", year=2026, month=6)
average = execute_business_template(self.db_path, "avg_order_amount", year=2026, month=6)
top_products = execute_business_template(self.db_path, "top_products", year=2026, month=6, limit=1)
staff = execute_business_template(
self.db_path,
"staff_summary",
year=2026,
month=6,
staff="徐丽",
staff_role_scope="all",
)
delivery_qty = execute_business_template(self.db_path, "delivery_plan_quantity", year=2026, month=6)
status = execute_business_template(self.db_path, "billstatus_breakdown", year=2026, month=6)
self.assertEqual(count["data"]["order_count"], 2)
self.assertEqual(sales["data"]["sales_amount"], 400.0)
self.assertEqual(customers["data"]["customer_count"], 2)
self.assertEqual(products["data"]["product_count"], 2)
self.assertEqual(quantity["data"]["quantity_sum"], 6.0)
self.assertEqual(average["data"]["avg_order_amount"], 200.0)
self.assertEqual(top_products["rows"][0]["product_id"], "P01")
self.assertEqual(top_products["rows"][0]["quantity"], 5.0)
self.assertEqual(staff["data"]["order_count"], 2)
self.assertEqual(staff["data"]["sales_amount"], 400.0)
self.assertEqual(delivery_qty["data"]["delivery_plan_quantity"], 5.0)
self.assertEqual(
[(row["status"], row["order_count"]) for row in status["rows"]],
[("B", 1), ("C", 1)],
)
def test_business_templates_exclude_configured_internal_company_transfer_orders(self):
sync_kingdee_orders(
self.vault,
[
{
"id": "1001",
"billno": "EXT-BJD-001",
"bizdate": "2026-06-03",
"customer_number": "C01",
"customer_name": "外部客户 A",
"totalallamount": "100.00",
"org_name": "深圳市北极大电子科技有限公司",
"billentry": [
{
"id": "L1",
"material_masterid_number": "P01",
"material_masterid_name": "Part 1",
"qty": "2",
"amountandtax": "100.00",
"orderdeliverentry": [{"id": "D1", "d_plandate": "2026-06-10", "d_planqty": "2"}],
}
],
},
{
"id": "1002",
"billno": "INT-JK-BJD-001",
"bizdate": "2026-06-04",
"customer_number": "NB000001",
"customer_name": "深圳市北极大电子科技有限公司",
"totalallamount": "80.00",
"org_name": "东莞市捷康电子科技有限公司",
"billentry": [
{
"id": "L1",
"material_masterid_number": "P01",
"material_masterid_name": "Part 1",
"qty": "4",
"amountandtax": "80.00",
"orderdeliverentry": [{"id": "D2", "d_plandate": "2026-06-11", "d_planqty": "4"}],
}
],
},
{
"id": "1003",
"billno": "EXT-JK-001",
"bizdate": "2026-06-05",
"customer_number": "C02",
"customer_name": "外部客户 B",
"totalallamount": "40.00",
"org_name": "东莞市捷康电子科技有限公司",
"billentry": [
{
"id": "L1",
"material_masterid_number": "P02",
"material_masterid_name": "Part 2",
"qty": "1",
"amountandtax": "40.00",
"orderdeliverentry": [{"id": "D3", "d_plandate": "2026-06-12", "d_planqty": "1"}],
}
],
},
{
"id": "1004",
"billno": "INT-ZC-JK-001",
"bizdate": "2026-06-06",
"customer_number": "NB000002",
"customer_name": "东莞市捷康电子科技有限公司",
"totalallamount": "20.00",
"org_name": "东莞市众创机电有限公司",
"billentry": [
{
"id": "L1",
"material_masterid_number": "P03",
"material_masterid_name": "Part 3",
"qty": "5",
"amountandtax": "20.00",
"orderdeliverentry": [{"id": "D4", "d_plandate": "2026-06-13", "d_planqty": "5"}],
}
],
},
],
synced_at="2026-07-06T09:00:00Z",
)
dedupe_params = {"year": 2026, "month": 6, "internal_parties": ["北极大", "捷康", "众创"]}
count = execute_business_template(self.db_path, "order_count", **dedupe_params)
sales = execute_business_template(self.db_path, "sales_amount", **dedupe_params)
quantity = execute_business_template(self.db_path, "quantity_sum", **dedupe_params)
delivery_qty = execute_business_template(self.db_path, "delivery_plan_quantity", **dedupe_params)
included = execute_business_template(
self.db_path,
"sales_amount",
**dedupe_params,
include_internal_orders=True,
)
self.assertEqual(count["data"]["order_count"], 2)
self.assertEqual(sales["data"]["sales_amount"], 140.0)
self.assertEqual(quantity["data"]["quantity_sum"], 3.0)
self.assertEqual(delivery_qty["data"]["delivery_plan_quantity"], 3.0)
self.assertEqual(included["data"]["sales_amount"], 240.0)
def test_all_business_templates_execute_with_fixture_data(self):
self.seed_template_fixture()
required_values = {
"customer": "Customer A",
"product": "P01",
"staff": "徐丽",
}
templates = business_schema()["templates"]
template_names = [template["name"] for template in templates]
for template in templates:
params = {
"year": 2026,
"month": 6,
"limit": 5,
"granularity": "day",
"staff_role_scope": "all",
}
for required in template["required_parameters"]:
params[required] = required_values[required]
with self.subTest(template=template["name"]):
result = execute_business_template(self.db_path, template["name"], **params)
self.assertEqual(result["template"], template["name"])
self.assertIsInstance(result["data"], dict)
self.assertIn("answer", result)
self.assertEqual(result["last_sync"], "2026-07-06T09:00:00Z")
self.assertIsInstance(result["is_stale"], bool)
if "rows" in result:
self.assertIsInstance(result["rows"], list)
json.dumps(result, ensure_ascii=False)
self.assertEqual(len(template_names), len(set(template_names)))
def test_execute_business_template_rejects_unknown_or_missing_parameters(self):
with self.assertRaisesRegex(RuntimeError, "unknown business template"):
execute_business_template(self.db_path, "not_a_template", year=2026, month=6)
with self.assertRaisesRegex(RuntimeError, "requires"):
execute_business_template(self.db_path, "staff_summary", year=2026, month=6)
def test_cli_business_schema_and_template_print_json(self):
sync_kingdee_orders(
self.vault,
[
{
"id": "1001",
"billno": "SO-001",
"bizdate": "2026-06-03",
"customer_name": "Customer A",
"totalallamount": "100.00",
}
],
synced_at="2026-07-06T09:00:00Z",
)
from obsidian_wiki.cli import main
schema_output = StringIO()
with redirect_stdout(schema_output):
schema_code = main(["business", "schema", "--json"])
template_output = StringIO()
with redirect_stdout(template_output):
template_code = main(
[
"business",
"template",
"order_count",
"--vault",
str(self.vault),
"--year",
"2026",
"--month",
"6",
"--json",
]
)
self.assertEqual(schema_code, 0)
self.assertEqual(template_code, 0)
self.assertIn("order_count", {template["name"] for template in json.loads(schema_output.getvalue())["templates"]})
self.assertEqual(json.loads(template_output.getvalue())["data"]["order_count"], 1)
def test_cli_business_template_uses_internal_party_environment_configuration(self):
sync_kingdee_orders(
self.vault,
[
{
"id": "1001",
"billno": "EXT-BJD-001",
"bizdate": "2026-06-03",
"customer_name": "外部客户 A",
"totalallamount": "100.00",
"org_name": "深圳市北极大电子科技有限公司",
},
{
"id": "1002",
"billno": "INT-JK-BJD-001",
"bizdate": "2026-06-04",
"customer_name": "深圳市北极大电子科技有限公司",
"totalallamount": "80.00",
"org_name": "东莞市捷康电子科技有限公司",
},
],
synced_at="2026-07-06T09:00:00Z",
)
from obsidian_wiki.cli import main
template_output = StringIO()
with patch.dict(os.environ, {"OBSIDIAN_BUSINESS_INTERNAL_PARTIES": "北极大,捷康"}):
with redirect_stdout(template_output):
template_code = main(
[
"business",
"template",
"sales_amount",
"--vault",
str(self.vault),
"--year",
"2026",
"--month",
"6",
"--json",
]
)
self.assertEqual(template_code, 0)
self.assertEqual(json.loads(template_output.getvalue())["data"]["sales_amount"], 100.0)
def test_cli_business_query_uses_internal_party_environment_configuration(self):
sync_kingdee_orders(
self.vault,
[
{
"id": "1001",
"billno": "EXT-BJD-001",
"bizdate": "2026-06-03",
"customer_name": "外部客户 A",
"totalallamount": "100.00",
"org_name": "深圳市北极大电子科技有限公司",
},
{
"id": "1002",
"billno": "INT-JK-BJD-001",
"bizdate": "2026-06-04",
"customer_name": "深圳市北极大电子科技有限公司",
"totalallamount": "80.00",
"org_name": "东莞市捷康电子科技有限公司",
},
],
synced_at="2026-07-06T09:00:00Z",
)
from obsidian_wiki.cli import main
query_output = StringIO()
with patch.dict(os.environ, {"OBSIDIAN_BUSINESS_INTERNAL_PARTIES": "北极大,捷康"}):
with redirect_stdout(query_output):
query_code = main(
[
"business",
"query",
"2026-06 sales amount",
"--vault",
str(self.vault),
"--json",
]
)
self.assertEqual(query_code, 0)
self.assertEqual(json.loads(query_output.getvalue())["data"]["sales_amount"], 100.0)
def test_incremental_start_modifytime_uses_cursor_with_lookback_or_initial_days(self):
initial = compute_incremental_start_modifytime(
self.db_path,
now="2026-07-06T12:00:00Z",
lookback_hours=48,
initial_days=7,
)
sync_kingdee_orders(
self.vault,
[
{
"id": "1001",
"billno": "SO-001",
"bizdate": "2026-07-03",
"customer_name": "Customer A",
"totalallamount": "100.00",
}
],
synced_at="2026-07-06T09:00:00Z",
)
next_start = compute_incremental_start_modifytime(
self.db_path,
now="2026-07-07T12:00:00Z",
lookback_hours=48,
initial_days=7,
)
self.assertEqual(initial, "2026-06-29 12:00:00")
self.assertEqual(next_start, "2026-07-04 09:00:00")
def test_cli_incremental_sync_passes_computed_modifytime_filter(self):
sync_kingdee_orders(
self.vault,
[
{
"id": "1001",
"billno": "SO-001",
"bizdate": "2026-07-03",
"customer_name": "Customer A",
"totalallamount": "100.00",
}
],
synced_at="2026-07-06T09:00:00Z",
)
from obsidian_wiki import cli
fetched_record = {
"id": "1002",
"billno": "SO-002",
"bizdate": "2026-07-04",
"customer_name": "Customer B",
"totalallamount": "200.00",
}
with (
patch.object(cli, "ensure_kingdee_config", return_value=False),
patch.object(cli, "load_kingdee_config", return_value={}),
patch.object(cli, "fetch_kingdee_payload", return_value=([fetched_record], {"rows": [fetched_record]})) as fetch_payload,
redirect_stdout(StringIO()),
):
status = cli.main(
[
"business",
"sync",
"kingdee",
"--vault",
str(self.vault),
"--config",
str(self.vault / "kingdee.env"),
"--incremental",
"--lookback-hours",
"48",
]
)
self.assertEqual(status, 0)
self.assertEqual(fetch_payload.call_args.kwargs["filters"]["start_modifytime"], "2026-07-04 09:00:00")
def test_cli_sync_payload_can_be_loaded_from_json_file(self):
payload_path = self.vault / "kingdee.json"
payload_path.write_text(
json.dumps(
{
"orders": [
{
"order_id": "1001",
"order_no": "SO-001",
"order_date": "2026-06-03",
"customer_name": "Customer A",
"total_amount": 1200,
}
]
}
),
encoding="utf-8",
)
from obsidian_wiki.cli import main
with redirect_stdout(StringIO()):
sync_code = main(["business", "sync", "kingdee", "--vault", str(self.vault), "--input", str(payload_path)])
query_output = StringIO()
with redirect_stdout(query_output):
query_code = main(["business", "query", "2026-06 order count", "--vault", str(self.vault), "--json"])
self.assertEqual(sync_code, 0)
self.assertEqual(query_code, 0)
self.assertEqual(json.loads(query_output.getvalue())["data"]["order_count"], 1)
if __name__ == "__main__":
unittest.main()