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.
199 lines
7.5 KiB
Python
199 lines
7.5 KiB
Python
import json
|
|
import tempfile
|
|
import unittest
|
|
from contextlib import redirect_stdout
|
|
from io import StringIO
|
|
from pathlib import Path
|
|
|
|
from obsidian_wiki.business_facts import (
|
|
business_db_path_for_vault,
|
|
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 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",
|
|
"settlecurrency_number": "CNY",
|
|
"billentry": [
|
|
{
|
|
"id": "4568544278879012864",
|
|
"material_masterid_number": "MAT-001",
|
|
"material_masterid_name": "车载连接器",
|
|
"qty": "711.97",
|
|
"amountandtax": "1234.56",
|
|
"amount": "1000.00",
|
|
}
|
|
],
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
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)
|
|
|
|
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()
|