1
0
forked from erp-dev/erp

feat: balance api

This commit is contained in:
2025-11-30 23:04:21 +08:00
parent 9006a530d1
commit 9acc4e14fc
21 changed files with 714 additions and 64 deletions

View File

@@ -22,7 +22,7 @@ from basic_info.models import (
WareHouse,
WareHouseModeEnum,
)
from business import models as business_models
from business import models as business_models, services
from stock import models as stock_models
from api_v1 import tasks
@@ -494,8 +494,7 @@ class PaymentOrderAPITestCase(TestCase):
{'action': 'cancel'},
format='json',
)
self.assertEqual(response_cancel.status_code, status.HTTP_200_OK)
self.assertEqual(response_cancel.data['status'], business_models.PaymentOrderStatusEnum.CANCELLED)
self.assertEqual(response_cancel.status_code, status.HTTP_400_BAD_REQUEST)
def test_payment_order_requires_amount(self):
payload = {**self.payload}
@@ -569,6 +568,81 @@ class ReceiptOrderAPITestCase(TestCase):
response = self.client.post('/api/v1/receipt-orders/', payload, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn('amount 必须大于 0', response.data['error'])
class CustomerBalanceAPITestCase(TestCase):
def setUp(self):
self.merchant = Merchant.objects.create(name='余额商户', type=MerchantTypeEnum.FACTORY)
self.customer = Customer.objects.create(
merchant=self.merchant,
name='余额客户',
mobile='13800000000',
created_by=None,
)
self.user = User.objects.create_user(username='balance_user', password='pass123')
self.employee = Employee.objects.create(
merchant=self.merchant,
sys_user=self.user,
name='财务查询',
status=EmployeeStatusEnum.ACTIVE,
)
self.client = APIClient()
self.client.force_authenticate(user=self.user)
def test_customer_balance_defaults_to_zero(self):
response = self.client.get(f'/api/v1/customers/{self.customer.id}/balance/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['balance'], '0')
def test_customer_balance_reflects_sales_and_receipts(self):
# 创建销售单
product_category = ProductCategory.objects.create(
merchant=self.merchant,
name='余额品类',
product_prefix='BAL',
)
product = Product.objects.create(
merchant=self.merchant,
category=product_category,
name='余额产品',
human_id='BAL-001',
unit=ProductUnitEnum.METER,
)
warehouse = WareHouse.objects.create(
merchant=self.merchant,
name='余额仓',
mode=WareHouseModeEnum.RESTRICT_IN,
)
order = services.create_sales_order(
merchant=self.merchant,
customer=self.customer,
order_date='2025-11-26',
warehouse=warehouse,
operator=self.employee,
items=[{'product_id': product.id, 'numbers': [5], 'price': '10', 'unit': ''}],
)
services.review_sales_order(
sales_order=order,
target_status=business_models.SalesOrderStatusEnum.APPROVED,
reviewed_by=self.user,
)
response = self.client.get(f'/api/v1/customers/{self.customer.id}/balance/')
self.assertEqual(response.data['balance'], '50.00')
receipt = services.create_receipt_order(
merchant=self.merchant,
customer=self.customer,
receipt_date='2025-11-27',
amount='20',
operator=self.employee,
)
services.review_receipt_order(
receipt_order=receipt,
target_status=business_models.ReceiptOrderStatusEnum.APPROVED,
reviewed_by=self.user,
)
response = self.client.get(f'/api/v1/customers/{self.customer.id}/balance/')
self.assertEqual(response.data['balance'], '30.00')
@override_settings(
CELERY_TASK_ALWAYS_EAGER=True,
CELERY_TASK_EAGER_PROPAGATES=True,

View File

@@ -10,10 +10,11 @@ from .views import (
users,
print_count,
)
from .business.purchase import views as purchase_views
from .business.sales import views as sales_views
from .business.payment import views as payment_views
from .business.receipt import views as receipt_views
from .views.business.purchase import views as purchase_views
from .views.business.sales import views as sales_views
from .views.business.payment import views as payment_views
from .views.business.receipt import views as receipt_views
from .views.business.balance import views as balance_views
from .views.stock_change_views.snapshot import StockSnapshotListView
from .views.printing.views import PrintingOrderViewSet, PrintingJobViewSet, PlateOrderViewSet
from .views.upload import UploadFileViewSet
@@ -69,6 +70,7 @@ urlpatterns = [
path('payment-orders/<int:pk>/review/', payment_views.PaymentOrderReviewView.as_view(), name='payment_order_review'),
path('receipt-orders/', receipt_views.ReceiptOrderView.as_view(), name='receipt_orders'),
path('receipt-orders/<int:pk>/review/', receipt_views.ReceiptOrderReviewView.as_view(), name='receipt_order_review'),
path('customers/<int:customer_id>/balance/', balance_views.CustomerBalanceView.as_view(), name='customer_balance'),
path('health/', healthy.HealthCheckView.as_view(), name='health_check'),
path('print-count/delta/', print_count.adjust_print_count, name='print_count_delta'),

View File

@@ -150,6 +150,13 @@
---
## 7. 客户欠款查询
- **GET** `/api/v1/customers/<id>/balance/`
- **描述**:返回指定客户当前的应收余额(审批通过的销售单金额累加减去收款单金额),数据来源于余额表,因此查询为 O(1)。
- **响应**`{"customer": 12, "customer_name": "张三", "balance": "1234.50"}`
- `balance` 为字符串格式的十进制数,正数表示客户欠款,应收;负数表示已收超额。
如需对 `items` 结构、仓库模式或审批流程做深入了解,请参阅:
- `docs/purchase_order_approval_and_red_flush.md`
- `docs/sales_order_approval_and_red_flush.md`

View File

@@ -0,0 +1,4 @@
"""
余额相关 API 视图。
"""

View File

@@ -0,0 +1,36 @@
from decimal import Decimal
from rest_framework import views
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from basic_info import models as basic_models
from business import services as business_services
from api_v1.views.stock_change_views.mixins import StockChangeViewMixin
class CustomerBalanceView(StockChangeViewMixin, views.APIView):
permission_classes = [IsAuthenticated]
def get(self, request, customer_id: int):
if not self.check_employee_permission(request):
return self.permission_error_response('无权限访问')
merchant = request.user.employee.merchant
try:
customer = basic_models.Customer.objects.get(id=customer_id, merchant=merchant)
except basic_models.Customer.DoesNotExist:
return self.not_found_response('客户不存在')
balance: Decimal = business_services.BalanceService.get_customer_balance(
merchant=merchant,
customer=customer,
)
return Response(
{
'customer': customer.id,
'customer_name': customer.name,
'balance': str(balance),
}
)