forked from erp-dev/erp
318 lines
11 KiB
Python
318 lines
11 KiB
Python
"""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, 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 (
|
||
DEFAULT_DESIGNER_SUMMARY_STATE_NAMES,
|
||
DEFAULT_DESIGNER_TASK_STATE_NAME_SUFFIX,
|
||
DEFAULT_DESIGNER_PARAM_KEY,
|
||
DEFAULT_DESIGNER_TASK_QUANTITY_PARAM_KEY,
|
||
DEFAULT_DESIGNER_TASK_TIME_PARAM_KEY,
|
||
get_plate_order_summary_by_customer,
|
||
get_plate_order_summary_by_designer,
|
||
get_designer_workflow_task_summary,
|
||
)
|
||
from .mixins import SettlementVisibilityMixin
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
AGENT_SECRET = "RCYH_BOT_0083"
|
||
|
||
|
||
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")
|
||
|
||
if not date_str:
|
||
return Response(
|
||
{"error": "缺少 date 参数"}, status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
|
||
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,
|
||
)
|
||
|
||
try:
|
||
settlement_date = date.fromisoformat(date_str)
|
||
except ValueError:
|
||
return Response({"error": "日期不存在"}, status=status.HTTP_403_FORBIDDEN)
|
||
|
||
emp = getattr(request.user, "employee", None)
|
||
if emp is None or emp.merchant is None:
|
||
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=merchant_id,
|
||
settlement_date=settlement_date,
|
||
user=self.get_service_user(request.user),
|
||
)
|
||
return Response({"data": data})
|
||
except Exception as e:
|
||
logger.exception(f"[settlement.views] 获取开版订单统计失败: {e}")
|
||
return Response(
|
||
{"error": "获取统计数据失败"},
|
||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||
)
|
||
|
||
|
||
class PlateOrderDesignerSummaryView(SettlementVisibilityMixin, APIView):
|
||
"""
|
||
开版订单设计师统计 API
|
||
|
||
GET /api/v1/settlement/plate-orders/designer-summary/
|
||
|
||
参数:
|
||
date: 统计日期(YYYY-MM-DD)
|
||
state_names: 可选,逗号分隔或重复传参的 state 名称,默认“画图完成”
|
||
"""
|
||
|
||
authentication_classes = PlateOrderSummaryView.authentication_classes
|
||
permission_classes = [IsAuthenticated]
|
||
view_all_permission = "printing.view_all_plateorders"
|
||
|
||
def get(self, request):
|
||
date_str = request.query_params.get("date")
|
||
|
||
if not date_str:
|
||
return Response(
|
||
{"error": "缺少 date 参数"}, status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
|
||
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,
|
||
)
|
||
|
||
try:
|
||
settlement_date = date.fromisoformat(date_str)
|
||
except ValueError:
|
||
return Response({"error": "日期不存在"}, status=status.HTTP_403_FORBIDDEN)
|
||
|
||
emp = getattr(request.user, "employee", None)
|
||
if emp is None or emp.merchant is None:
|
||
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
|
||
|
||
state_names = _parse_state_names(request)
|
||
|
||
try:
|
||
data = get_plate_order_summary_by_designer(
|
||
merchant_id=merchant_id,
|
||
settlement_date=settlement_date,
|
||
user=self.get_service_user(request.user),
|
||
state_names=state_names,
|
||
)
|
||
return Response({
|
||
"data": data,
|
||
"meta": {
|
||
"state_names": list(state_names or DEFAULT_DESIGNER_SUMMARY_STATE_NAMES),
|
||
"designer_param_key": DEFAULT_DESIGNER_PARAM_KEY,
|
||
},
|
||
})
|
||
except ValueError as e:
|
||
return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||
except Exception as e:
|
||
logger.exception(f"[settlement.views] 获取开版订单设计师统计失败: {e}")
|
||
return Response(
|
||
{"error": "获取统计数据失败"},
|
||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||
)
|
||
|
||
|
||
def _parse_state_names(request):
|
||
"""解析 state_names,支持逗号分隔和重复传参。"""
|
||
return _parse_csv_query_param(request, "state_names")
|
||
|
||
|
||
def _parse_designer_names(request):
|
||
"""解析 designer_names,支持逗号分隔和重复传参。"""
|
||
return _parse_csv_query_param(request, "designer_names")
|
||
|
||
|
||
def _parse_csv_query_param(request, name: str):
|
||
raw_values = request.query_params.getlist("state_names")
|
||
if name != "state_names":
|
||
raw_values = request.query_params.getlist(name)
|
||
if not raw_values:
|
||
return None
|
||
|
||
values = []
|
||
for raw_value in raw_values:
|
||
values.extend(
|
||
item.strip() for item in str(raw_value).split(',') if item.strip()
|
||
)
|
||
return values
|
||
|
||
|
||
class DesignerWorkflowTaskSummaryView(SettlementVisibilityMixin, APIView):
|
||
"""
|
||
设计师工序任务量统计 API
|
||
|
||
GET /api/v1/settlement/workflows/designer-task-summary/
|
||
"""
|
||
|
||
authentication_classes = PlateOrderSummaryView.authentication_classes
|
||
permission_classes = [IsAuthenticated]
|
||
view_all_permission = "printing.view_all_plateorders"
|
||
|
||
def get(self, request):
|
||
date_str = request.query_params.get("date")
|
||
|
||
if not date_str:
|
||
return Response(
|
||
{"error": "缺少 date 参数"}, status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
|
||
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,
|
||
)
|
||
|
||
try:
|
||
settlement_date = date.fromisoformat(date_str)
|
||
except ValueError:
|
||
return Response({"error": "日期不存在"}, status=status.HTTP_403_FORBIDDEN)
|
||
|
||
emp = getattr(request.user, "employee", None)
|
||
if emp is None or emp.merchant is None:
|
||
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
|
||
|
||
state_names = _parse_state_names(request)
|
||
designer_names = _parse_designer_names(request)
|
||
|
||
try:
|
||
data = get_designer_workflow_task_summary(
|
||
merchant_id=merchant_id,
|
||
settlement_date=settlement_date,
|
||
user=self.get_service_user(request.user),
|
||
state_names=state_names,
|
||
designer_names=designer_names,
|
||
)
|
||
return Response({
|
||
"data": data,
|
||
"meta": {
|
||
"date": settlement_date.isoformat(),
|
||
"state_names": list(state_names or []),
|
||
"state_filter": (
|
||
{"mode": "exact", "state_names": list(state_names)}
|
||
if state_names is not None
|
||
else {"mode": "suffix", "suffix": DEFAULT_DESIGNER_TASK_STATE_NAME_SUFFIX}
|
||
),
|
||
"designer_names": list(designer_names or []),
|
||
"designer_param_key": DEFAULT_DESIGNER_PARAM_KEY,
|
||
"quantity_param_key": DEFAULT_DESIGNER_TASK_QUANTITY_PARAM_KEY,
|
||
"time_param_key": DEFAULT_DESIGNER_TASK_TIME_PARAM_KEY,
|
||
"empty_quantity_default": 1,
|
||
"time_source": "parameters.完成时间; fallback=StateFlowRecord.completed_at",
|
||
},
|
||
})
|
||
except ValueError as e:
|
||
return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||
except Exception as e:
|
||
logger.exception(f"[settlement.views] 获取设计师工序任务量统计失败: {e}")
|
||
return Response(
|
||
{"error": "获取统计数据失败"},
|
||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||
)
|