forked from erp-dev/erp
feat: balance api
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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'),
|
||||
|
||||
|
||||
@@ -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`
|
||||
4
api_v1/views/business/balance/__init__.py
Normal file
4
api_v1/views/business/balance/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
"""
|
||||
余额相关 API 视图。
|
||||
"""
|
||||
|
||||
36
api_v1/views/business/balance/views.py
Normal file
36
api_v1/views/business/balance/views.py
Normal 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),
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user