1
0
forked from erp-dev/erp

feat: settlement lineup

This commit is contained in:
2026-03-09 22:40:02 +08:00
parent 86f5dab32b
commit 663276288e
24 changed files with 1730 additions and 33 deletions

View 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) → 绕过
- 其余 → 透传原始 userservice 自行应用 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 不施加客户可见性过滤。
- 否则返回原始 userservice 按员工可见性过滤。
"""
if self.can_bypass_settlement_visibility(user):
return None
return user

View File

@@ -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_0083Agent 专用,跳过鉴权)
可见性规则(与 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,
)