Wire DocumentNumberSequence allocator, ledger-based subsidiary/detail reports, and posted-voucher reopen flow without mock ops forms. Co-authored-by: Cursor <cursoragent@cursor.com>
299 lines
8.9 KiB
Python
299 lines
8.9 KiB
Python
import uuid
|
|
from datetime import date
|
|
|
|
import pytest
|
|
|
|
from app.tests.conftest import TENANT_A, TENANT_B, tenant_headers
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_health(client):
|
|
resp = await client.get("/health")
|
|
assert resp.status_code == 200
|
|
assert resp.json()["service"] == "accounting-service"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_chart_of_accounts(client):
|
|
resp = await client.post(
|
|
"/api/v1/accounts/charts",
|
|
json={"name": "Default COA", "code": "COA-001", "is_default": True},
|
|
headers=tenant_headers(TENANT_A),
|
|
)
|
|
assert resp.status_code == 201
|
|
data = resp.json()
|
|
assert data["code"] == "COA-001"
|
|
assert data["tenant_id"] == str(TENANT_A)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tenant_isolation_accounts(client):
|
|
chart_resp = await client.post(
|
|
"/api/v1/accounts/charts",
|
|
json={"name": "Tenant A COA", "code": "COA-A"},
|
|
headers=tenant_headers(TENANT_A),
|
|
)
|
|
assert chart_resp.status_code == 201
|
|
chart_id = chart_resp.json()["id"]
|
|
|
|
acc_resp = await client.post(
|
|
"/api/v1/accounts",
|
|
json={
|
|
"chart_id": chart_id,
|
|
"code": "1000",
|
|
"name": "Test Account",
|
|
"account_type": "asset",
|
|
"account_category": "current_asset",
|
|
},
|
|
headers=tenant_headers(TENANT_A),
|
|
)
|
|
assert acc_resp.status_code == 201
|
|
account_id = acc_resp.json()["id"]
|
|
|
|
resp_b_list = await client.get(
|
|
"/api/v1/accounts",
|
|
headers=tenant_headers(TENANT_B),
|
|
)
|
|
assert resp_b_list.status_code == 200
|
|
assert len(resp_b_list.json()) == 0
|
|
|
|
resp_b_get = await client.get(
|
|
f"/api/v1/accounts/{account_id}",
|
|
headers=tenant_headers(TENANT_B),
|
|
)
|
|
assert resp_b_get.status_code == 404
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_posting_engine_balanced_voucher(client):
|
|
year_resp = await client.post(
|
|
"/api/v1/fiscal/years",
|
|
json={"name": "1403", "start_date": "2024-03-20", "end_date": "2025-03-19", "is_current": True},
|
|
headers=tenant_headers(TENANT_A),
|
|
)
|
|
assert year_resp.status_code == 201
|
|
year_id = year_resp.json()["id"]
|
|
|
|
period_resp = await client.post(
|
|
"/api/v1/fiscal/periods",
|
|
json={
|
|
"fiscal_year_id": year_id,
|
|
"name": "Month 1",
|
|
"period_number": 1,
|
|
"start_date": "2024-03-20",
|
|
"end_date": "2024-04-19",
|
|
"is_current": True,
|
|
},
|
|
headers=tenant_headers(TENANT_A),
|
|
)
|
|
assert period_resp.status_code == 201
|
|
period_id = period_resp.json()["id"]
|
|
|
|
chart_resp = await client.post(
|
|
"/api/v1/accounts/charts",
|
|
json={"name": "COA", "code": "COA-POST"},
|
|
headers=tenant_headers(TENANT_A),
|
|
)
|
|
chart_id = chart_resp.json()["id"]
|
|
|
|
acc1_resp = await client.post(
|
|
"/api/v1/accounts",
|
|
json={
|
|
"chart_id": chart_id,
|
|
"code": "1101",
|
|
"name": "Cash",
|
|
"account_type": "asset",
|
|
"account_category": "current_asset",
|
|
},
|
|
headers=tenant_headers(TENANT_A),
|
|
)
|
|
acc2_resp = await client.post(
|
|
"/api/v1/accounts",
|
|
json={
|
|
"chart_id": chart_id,
|
|
"code": "4101",
|
|
"name": "Revenue",
|
|
"account_type": "revenue",
|
|
"account_category": "operating_revenue",
|
|
},
|
|
headers=tenant_headers(TENANT_A),
|
|
)
|
|
acc1_id = acc1_resp.json()["id"]
|
|
acc2_id = acc2_resp.json()["id"]
|
|
|
|
voucher_resp = await client.post(
|
|
"/api/v1/posting/vouchers",
|
|
json={
|
|
"fiscal_period_id": period_id,
|
|
"voucher_number": "V-001",
|
|
"voucher_date": str(date.today()),
|
|
"description": "Test voucher",
|
|
"lines": [
|
|
{"account_id": acc1_id, "debit": "1000.0000", "credit": "0"},
|
|
{"account_id": acc2_id, "debit": "0", "credit": "1000.0000"},
|
|
],
|
|
},
|
|
headers=tenant_headers(TENANT_A),
|
|
)
|
|
assert voucher_resp.status_code == 201
|
|
voucher_id = voucher_resp.json()["id"]
|
|
|
|
post_resp = await client.post(
|
|
f"/api/v1/posting/vouchers/{voucher_id}/post",
|
|
json={},
|
|
headers=tenant_headers(TENANT_A),
|
|
)
|
|
assert post_resp.status_code == 200
|
|
assert post_resp.json()["status"] == "posted"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_unbalanced_voucher_rejected(client):
|
|
year_resp = await client.post(
|
|
"/api/v1/fiscal/years",
|
|
json={"name": "1404", "start_date": "2025-03-20", "end_date": "2026-03-19"},
|
|
headers=tenant_headers(TENANT_A),
|
|
)
|
|
year_id = year_resp.json()["id"]
|
|
period_resp = await client.post(
|
|
"/api/v1/fiscal/periods",
|
|
json={
|
|
"fiscal_year_id": year_id,
|
|
"name": "P1",
|
|
"period_number": 1,
|
|
"start_date": "2025-03-20",
|
|
"end_date": "2025-04-19",
|
|
"is_current": True,
|
|
},
|
|
headers=tenant_headers(TENANT_A),
|
|
)
|
|
period_id = period_resp.json()["id"]
|
|
|
|
chart_resp = await client.post(
|
|
"/api/v1/accounts/charts",
|
|
json={"name": "COA2", "code": "COA-UNBAL"},
|
|
headers=tenant_headers(TENANT_A),
|
|
)
|
|
chart_id = chart_resp.json()["id"]
|
|
|
|
acc_resp = await client.post(
|
|
"/api/v1/accounts",
|
|
json={
|
|
"chart_id": chart_id,
|
|
"code": "1102",
|
|
"name": "Bank",
|
|
"account_type": "asset",
|
|
"account_category": "current_asset",
|
|
},
|
|
headers=tenant_headers(TENANT_A),
|
|
)
|
|
acc_id = acc_resp.json()["id"]
|
|
|
|
voucher_resp = await client.post(
|
|
"/api/v1/posting/vouchers",
|
|
json={
|
|
"fiscal_period_id": period_id,
|
|
"voucher_number": "V-UNBAL",
|
|
"voucher_date": str(date.today()),
|
|
"lines": [
|
|
{"account_id": acc_id, "debit": "500", "credit": "0"},
|
|
{"account_id": acc_id, "debit": "0", "credit": "300"},
|
|
],
|
|
},
|
|
headers=tenant_headers(TENANT_A),
|
|
)
|
|
voucher_id = voucher_resp.json()["id"]
|
|
|
|
validate_resp = await client.post(
|
|
f"/api/v1/posting/vouchers/{voucher_id}/validate",
|
|
headers=tenant_headers(TENANT_A),
|
|
)
|
|
assert validate_resp.status_code == 422
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_trial_balance(client):
|
|
tb_resp = await client.post(
|
|
"/api/v1/ledger/trial-balance",
|
|
params={"fiscal_period_id": str(uuid.uuid4())},
|
|
headers=tenant_headers(TENANT_A),
|
|
)
|
|
assert tb_resp.status_code == 200
|
|
assert "is_balanced" in tb_resp.json()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_subsidiary_ledger_report(client):
|
|
year_resp = await client.post(
|
|
"/api/v1/fiscal/years",
|
|
json={
|
|
"name": "1404-sub",
|
|
"start_date": "2025-03-21",
|
|
"end_date": "2026-03-20",
|
|
"is_current": True,
|
|
},
|
|
headers=tenant_headers(TENANT_A),
|
|
)
|
|
assert year_resp.status_code == 201
|
|
year_id = year_resp.json()["id"]
|
|
|
|
period_resp = await client.post(
|
|
"/api/v1/fiscal/periods",
|
|
json={
|
|
"fiscal_year_id": year_id,
|
|
"name": "Farvardin",
|
|
"period_number": 1,
|
|
"start_date": "2025-03-21",
|
|
"end_date": "2025-04-20",
|
|
"is_current": True,
|
|
},
|
|
headers=tenant_headers(TENANT_A),
|
|
)
|
|
assert period_resp.status_code == 201
|
|
period_id = period_resp.json()["id"]
|
|
|
|
chart_resp = await client.post(
|
|
"/api/v1/accounts/charts",
|
|
json={"name": "COA Sub", "code": "COA-SUB"},
|
|
headers=tenant_headers(TENANT_A),
|
|
)
|
|
chart_id = chart_resp.json()["id"]
|
|
await client.post(
|
|
"/api/v1/accounts",
|
|
json={
|
|
"chart_id": chart_id,
|
|
"code": "1101",
|
|
"name": "صندوق",
|
|
"account_type": "asset",
|
|
"account_category": "current_asset",
|
|
"level": 3,
|
|
"is_postable": True,
|
|
"is_group": False,
|
|
},
|
|
headers=tenant_headers(TENANT_A),
|
|
)
|
|
|
|
resp = await client.post(
|
|
"/api/v1/reporting/subsidiary-ledger",
|
|
params={"fiscal_period_id": period_id},
|
|
headers=tenant_headers(TENANT_A),
|
|
)
|
|
assert resp.status_code == 200
|
|
body = resp.json()
|
|
assert "id" in body
|
|
data = body["data"]
|
|
if isinstance(data, str):
|
|
import json
|
|
|
|
data = json.loads(data)
|
|
assert data["mode"] == "summary"
|
|
assert data["level_mode"] == "subsidiary"
|
|
assert isinstance(data["rows"], list)
|
|
assert any(r.get("account_code") == "1101" for r in data["rows"])
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_requires_tenant_header(client):
|
|
resp = await client.get("/api/v1/accounts")
|
|
assert resp.status_code == 400
|