forked from erp-dev/erp
feat: settlement lineup
This commit is contained in:
@@ -653,6 +653,22 @@ class PlateOrderAPITestCase(TestCase):
|
||||
self.assertIn(order2.id, ids)
|
||||
self.assertNotIn(order1.id, ids)
|
||||
self.assertNotIn(order3.id, ids)
|
||||
|
||||
def test_filter_by_plate_date_range_over_10_days_returns_400(self):
|
||||
"""测试按开版日期筛选时,跨度超过 10 天返回 400"""
|
||||
response = self.client.get('/api/v1/plate-orders/?plate_date_from=2025-01-01&plate_date_to=2025-01-11')
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertIn('查询跨度不能超过10天', str(response.data))
|
||||
|
||||
def test_filter_by_plate_at_range_over_10_days_returns_400(self):
|
||||
"""测试按开版时间筛选时,跨度超过 10 天返回 400"""
|
||||
from urllib.parse import quote
|
||||
|
||||
from_value = quote('2025-01-01T00:00:00+08:00')
|
||||
to_value = quote('2025-01-11T00:00:00+08:00')
|
||||
response = self.client.get(f'/api/v1/plate-orders/?plate_at_from={from_value}&plate_at_to={to_value}')
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertIn('查询跨度不能超过10天', str(response.data))
|
||||
|
||||
def test_search_by_design_code(self):
|
||||
"""测试按设计编号搜索"""
|
||||
|
||||
@@ -3,6 +3,7 @@ Printing API ViewSet
|
||||
"""
|
||||
from rest_framework import viewsets, filters, status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import ValidationError as DRFValidationError
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.permissions import BasePermission
|
||||
from rest_framework.permissions import DjangoModelPermissions
|
||||
@@ -646,6 +647,7 @@ class HasActivatePlateOrderPermission(BasePermission):
|
||||
|
||||
class PlateOrderFilterSet(django_filters.FilterSet):
|
||||
"""开版订单过滤器"""
|
||||
MAX_RANGE_DAYS = 10
|
||||
customer_name = django_filters.CharFilter(field_name='customer__name', lookup_expr='icontains')
|
||||
customer_phone = django_filters.CharFilter(field_name='customer__mobile', lookup_expr='icontains')
|
||||
image_name = django_filters.CharFilter(lookup_expr='icontains')
|
||||
@@ -694,6 +696,52 @@ class PlateOrderFilterSet(django_filters.FilterSet):
|
||||
return queryset.filter(created_at__lt=next_day)
|
||||
return queryset
|
||||
|
||||
@staticmethod
|
||||
def _to_local_date_from_datetime(raw_value):
|
||||
dt = parse_datetime(str(raw_value))
|
||||
if dt is None:
|
||||
raise DRFValidationError('日期时间格式无效')
|
||||
if timezone.is_aware(dt):
|
||||
dt = timezone.localtime(dt)
|
||||
return dt.date()
|
||||
|
||||
@staticmethod
|
||||
def _to_date(raw_value):
|
||||
d = parse_date(str(raw_value))
|
||||
if d is None:
|
||||
raise DRFValidationError('日期格式无效')
|
||||
return d
|
||||
|
||||
def _validate_range_days(self, from_key, to_key, mode, label):
|
||||
raw_from = self.data.get(from_key)
|
||||
raw_to = self.data.get(to_key)
|
||||
if not raw_from or not raw_to:
|
||||
return
|
||||
|
||||
if mode == 'datetime':
|
||||
from_date = self._to_local_date_from_datetime(raw_from)
|
||||
to_date = self._to_local_date_from_datetime(raw_to)
|
||||
else:
|
||||
from_date = self._to_date(raw_from)
|
||||
to_date = self._to_date(raw_to)
|
||||
|
||||
days = (to_date - from_date).days + 1
|
||||
if days > self.MAX_RANGE_DAYS:
|
||||
raise DRFValidationError(f'{label}查询跨度不能超过{self.MAX_RANGE_DAYS}天')
|
||||
|
||||
@property
|
||||
def qs(self):
|
||||
self._validate_range_days('plate_date_from', 'plate_date_to', mode='date', label='开版日期')
|
||||
self._validate_range_days('plate_at_from', 'plate_at_to', mode='datetime', label='开版时间')
|
||||
self._validate_range_days(
|
||||
'required_completion_date_from',
|
||||
'required_completion_date_to',
|
||||
mode='datetime',
|
||||
label='要求完成时间',
|
||||
)
|
||||
self._validate_range_days('created_date_from', 'created_date_to', mode='date', label='创建日期')
|
||||
return super().qs
|
||||
|
||||
|
||||
def _plate_order_last_modified(request, *args, **kwargs):
|
||||
"""
|
||||
|
||||
51
api_v1/views/settlement/mixins.py
Normal file
51
api_v1/views/settlement/mixins.py
Normal file
@@ -0,0 +1,51 @@
|
||||
"""Settlement API mixins.
|
||||
|
||||
客户可见性绕过逻辑与 printing 模块的 CustomerVisibilityFilterMixin 对齐:
|
||||
- superuser → 无过滤
|
||||
- 持有 view_all_permission → 无过滤
|
||||
- 其余普通员工 → settlement service 按 created_by / visible_employees 过滤
|
||||
|
||||
settlement.services._get_plate_order_queryset 已通过 ``user=None`` 判断来
|
||||
决定是否施加可见性过滤,故基类只需控制向 service 传入的 user 参数即可。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
class SettlementVisibilityMixin:
|
||||
"""
|
||||
Settlement API 客户可见性绕过基类。
|
||||
|
||||
使用方式:
|
||||
class MySettlementView(SettlementVisibilityMixin, APIView):
|
||||
view_all_permission = 'printing.view_all_plateorders'
|
||||
|
||||
绕过规则(与 CustomerVisibilityFilterMixin 保持一致):
|
||||
- user.is_superuser → 绕过
|
||||
- user.has_perm(view_all_permission) → 绕过
|
||||
- 其余 → 透传原始 user,service 自行应用 visible_employees 过滤
|
||||
"""
|
||||
|
||||
# 子类可覆盖为自定义权限名
|
||||
view_all_permission: str | None = 'printing.view_all_plateorders'
|
||||
|
||||
def can_bypass_settlement_visibility(self, user) -> bool:
|
||||
"""判断该用户是否应跳过 settlement 客户可见性过滤。"""
|
||||
if user is None:
|
||||
return False
|
||||
if user.is_superuser:
|
||||
return True
|
||||
if self.view_all_permission and user.has_perm(self.view_all_permission):
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_service_user(self, user):
|
||||
"""
|
||||
返回传递给 settlement service 函数的 ``user`` 参数。
|
||||
|
||||
- 可绕过时返回 ``None``:service 不施加客户可见性过滤。
|
||||
- 否则返回原始 user:service 按员工可见性过滤。
|
||||
"""
|
||||
if self.can_bypass_settlement_visibility(user):
|
||||
return None
|
||||
return user
|
||||
@@ -1,75 +1,122 @@
|
||||
"""Settlement API views"""
|
||||
|
||||
import logging
|
||||
import re
|
||||
from datetime import datetime, date
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils import timezone
|
||||
from rest_framework import status
|
||||
from rest_framework import status, authentication
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||||
|
||||
from basic_info.models import Merchant
|
||||
from settlement.services import get_plate_order_summary_by_customer
|
||||
from .mixins import SettlementVisibilityMixin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
AGENT_SECRET = "RCYH_BOT_0083"
|
||||
|
||||
class PlateOrderSummaryView(APIView):
|
||||
|
||||
class AgentSecretAuthentication(authentication.BaseAuthentication):
|
||||
"""
|
||||
Agent 密钥认证(仅用于 settlement/plate-orders/summary API)
|
||||
|
||||
当请求头中存在 X-AGENT-SECRET 且值匹配时,跳过 JWT 认证,
|
||||
以 admin 超级管理员身份执行请求。
|
||||
"""
|
||||
|
||||
def authenticate(self, request):
|
||||
secret = request.META.get("HTTP_X_AGENT_SECRET")
|
||||
if secret != AGENT_SECRET:
|
||||
return None
|
||||
User = get_user_model()
|
||||
try:
|
||||
user = User.objects.get(username="admin", is_superuser=True)
|
||||
return (user, None)
|
||||
except User.DoesNotExist:
|
||||
logger.warning("[AgentSecretAuthentication] admin 用户不存在")
|
||||
return None
|
||||
|
||||
|
||||
class PlateOrderSummaryView(SettlementVisibilityMixin, APIView):
|
||||
"""
|
||||
开版订单统计 API
|
||||
|
||||
|
||||
GET /api/v1/settlement/plate-orders/summary/
|
||||
|
||||
|
||||
参数:
|
||||
date: 统计日期(YYYY-MM-DD)
|
||||
|
||||
认证方式:
|
||||
- JWT Token(标准方式)
|
||||
- X-AGENT-SECRET: RCYH_BOT_0083(Agent 专用,跳过鉴权)
|
||||
|
||||
可见性规则(与 plate-order 列表接口对齐):
|
||||
- superuser 或持有 view_all_permission(默认 printing.view_all_plateorders)
|
||||
→ 返回该商户下全部客户的开版汇总
|
||||
- 普通员工 → 仅返回其负责/可见客户的开版汇总
|
||||
"""
|
||||
|
||||
authentication_classes = [
|
||||
AgentSecretAuthentication,
|
||||
authentication.SessionAuthentication,
|
||||
JWTAuthentication,
|
||||
]
|
||||
permission_classes = [IsAuthenticated]
|
||||
view_all_permission = "printing.view_all_plateorders"
|
||||
|
||||
def get(self, request):
|
||||
"""
|
||||
获取开版订单统计
|
||||
"""
|
||||
date_str = request.query_params.get('date')
|
||||
|
||||
date_str = request.query_params.get("date")
|
||||
|
||||
if not date_str:
|
||||
return Response(
|
||||
{'error': '缺少 date 参数'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
{"error": "缺少 date 参数"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
if not re.match(r'^\d{4}-\d{2}-\d{2}$', date_str):
|
||||
|
||||
if not re.match(r"^\d{4}-\d{2}-\d{2}$", date_str):
|
||||
return Response(
|
||||
{'error': '日期格式错误,请使用 YYYY-MM-DD 格式'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
{"error": "日期格式错误,请使用 YYYY-MM-DD 格式"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
settlement_date = date.fromisoformat(date_str)
|
||||
except ValueError:
|
||||
return Response(
|
||||
{'error': '日期不存在'},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
|
||||
emp = getattr(request.user, 'employee', None)
|
||||
return Response({"error": "日期不存在"}, status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
emp = getattr(request.user, "employee", None)
|
||||
if emp is None or emp.merchant is None:
|
||||
return Response(
|
||||
{'error': '用户未关联商户'},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
|
||||
if request.user.is_superuser:
|
||||
merchant = Merchant.objects.first()
|
||||
if merchant is None:
|
||||
return Response(
|
||||
{"error": "系统中没有商户"}, status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
merchant_id = merchant.id
|
||||
else:
|
||||
return Response(
|
||||
{"error": "用户未关联商户"}, status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
else:
|
||||
merchant_id = emp.merchant.id
|
||||
|
||||
try:
|
||||
data = get_plate_order_summary_by_customer(
|
||||
merchant_id=emp.merchant.id,
|
||||
merchant_id=merchant_id,
|
||||
settlement_date=settlement_date,
|
||||
user=request.user
|
||||
user=self.get_service_user(request.user),
|
||||
)
|
||||
return Response({'data': data})
|
||||
return Response({"data": data})
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
f'[settlement.views] 获取开版订单统计失败: {e}'
|
||||
)
|
||||
logger.exception(f"[settlement.views] 获取开版订单统计失败: {e}")
|
||||
return Response(
|
||||
{'error': '获取统计数据失败'},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
{"error": "获取统计数据失败"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user