Files
dealerhub/backend/tests/test_p0_aging_statement.py

183 lines
6.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""P0 #6 账龄分析 + 客户对账单 + 公开分享链接集成测试。"""
import pytest
from datetime import date, timedelta
from decimal import Decimal
from model_bakery import baker
from rest_framework.test import APIClient
from rest_framework_simplejwt.tokens import RefreshToken
from apps.catalog.models import Product
from apps.inventory.models import Warehouse
from apps.partner.models import Customer
from apps.finance import services as fin_services
from apps.finance.models import Receivable, Receipt, StatementShare
from apps.sales import services as sales_services
@pytest.fixture
def auth_client(db, user, tenant):
c = APIClient()
refresh = RefreshToken.for_user(user)
c.credentials(
HTTP_AUTHORIZATION=f"Bearer {refresh.access_token}",
HTTP_X_TENANT_ID=tenant.code,
)
return c
@pytest.fixture
def anon_client(db):
return APIClient()
@pytest.fixture
def warehouse(db, tenant):
return baker.make(Warehouse, tenant=tenant, code="WH01", name="主仓")
@pytest.fixture
def product(db, tenant):
return baker.make(Product, tenant=tenant, code="P001", name="商品A",
sale_price=Decimal("100"))
@pytest.fixture
def customer(db, tenant):
return baker.make(Customer, tenant=tenant, code="C001", name="账龄客户")
def _recv(tenant, customer, *, days_ago, total, paid=0, ref="RC-X"):
return baker.make(
Receivable, tenant=tenant, customer=customer, bill_no=ref,
source_type="sale", source_ref=ref, bill_date=date.today() - timedelta(days=days_ago),
total_amount=Decimal(str(total)), paid_amount=Decimal(str(paid)),
status="partial" if paid else "open",
)
# ---------- 账龄 ----------
def test_aging_buckets(tenant, customer):
_recv(tenant, customer, days_ago=10, total=100, ref="RC-A") # 0-30
_recv(tenant, customer, days_ago=45, total=200, paid=50, ref="RC-B") # 31-60 → 150
_recv(tenant, customer, days_ago=100, total=300, ref="RC-C") # 91-180
_recv(tenant, customer, days_ago=400, total=400, ref="RC-D") # 365+
data = fin_services.receivable_aging(tenant)
amounts = {b["bucket"]: b["amount"] for b in data["buckets"]}
assert amounts["0-30"] == "100.0000"
assert amounts["31-60"] == "150.0000"
assert amounts["61-90"] == "0.0000"
assert amounts["91-180"] == "300.0000"
assert amounts["181-365"] == "0.0000"
assert amounts["365+"] == "400.0000"
assert data["total"] == "950.0000"
def test_aging_paid_receivable_excluded(tenant, customer):
_recv(tenant, customer, days_ago=5, total=999, paid=999, ref="RC-P")
data = fin_services.receivable_aging(tenant)
assert data["total"] == "0.0000"
def test_aging_per_customer(tenant, customer):
other = baker.make(Customer, tenant=tenant, code="C002", name="别家")
_recv(tenant, customer, days_ago=10, total=100, ref="RC-A")
_recv(tenant, other, days_ago=10, total=500, ref="RC-B")
data = fin_services.receivable_aging(tenant, customer=customer)
assert data["total"] == "100.0000"
# ---------- 对账单 ----------
def test_statement_opening_lines_closing(tenant, customer, warehouse, product):
"""期初(区间前应收未结)→ 区间内新应收(借)→ 区间内收款(贷)→ 期末平衡。"""
# 期初:30 天前应收 1000,已收 300
_recv(tenant, customer, days_ago=40, total=1000, paid=300, ref="RC-OPEN")
# 区间内:今天销售开单 500
from apps.inventory import services as inv_services
inv_services.inbound(
tenant=tenant, warehouse=warehouse, product=product,
quantity=Decimal("10"), unit_cost=Decimal("50"),
)
bill = sales_services.create_sales_bill(
tenant=tenant, customer=customer, warehouse=warehouse,
lines=[{"product": product, "quantity": 5, "unit_price": 100}],
)
sales_services.confirm_sales_bill(bill) # 应收 RC… 500
# 区间内收款 200,核销到期初应收
receipt = baker.make(
Receipt, tenant=tenant, customer=customer, bill_no="SK001",
bill_date=date.today(), amount=Decimal("200"), status="draft",
method="银行转账",
)
recv_open = Receivable.objects.get(bill_no="RC-OPEN")
fin_services.allocate_receipt(receipt, [(recv_open, Decimal("200"))])
data = fin_services.customer_statement(
tenant, customer, date_from=date.today(), date_to=date.today()
)
assert data["opening_balance"] == "700.0000" # 1000-300
assert data["closing_balance"] == "1000.0000" # 700 + 500 - 200
types = [(l["type"], l["ref"]) for l in data["lines"]]
# 应收行 ref 是应收单号(RC…,source_ref 才是销售单号)
rc_refs = [l["ref"] for l in data["lines"] if l["type"] == "receivable"]
assert len(rc_refs) == 1 and rc_refs[0].startswith("RC")
assert ("receipt", "SK001") in types
# 余额滚动校验
last = data["lines"][-1]
assert last["balance"] == "1000.0000"
# ---------- 分享链接 ----------
def test_share_link_flow(db, auth_client, anon_client, tenant, customer):
# 生成分享
resp = auth_client.post(
"/api/v1/finance/statements/share/",
{
"customer_id": customer.id,
"date_from": date.today().isoformat(),
"date_to": date.today().isoformat(),
"expires_days": 1,
},
format="json",
)
assert resp.status_code == 201, resp.content
token = resp.json()["token"]
url = resp.json()["url"]
# 匿名可访问(无需租户头/JWT)
resp2 = anon_client.get(url)
assert resp2.status_code == 200, resp2.content
body = resp2.json()
assert body["customer"]["code"] == "C001"
assert body["share"]["token"] == token
# 吊销后 404
share = StatementShare.objects.get(token=token)
share.revoked = True
share.save()
resp3 = anon_client.get(url)
assert resp3.status_code == 404
def test_share_link_expired_404(db, auth_client, anon_client, tenant, customer):
from django.utils import timezone
share = baker.make(
StatementShare, tenant=tenant, customer=customer,
date_from=date.today(), date_to=date.today(),
expires_at=timezone.now() - timedelta(hours=1),
)
resp = anon_client.get(f"/api/v1/open/statements/{share.token}/")
assert resp.status_code == 404
def test_share_list_requires_auth(db, anon_client):
resp = anon_client.get("/api/v1/finance/statements/share/")
assert resp.status_code in (401, 403)