forked from erp-dev/erp
fix: appversions
This commit is contained in:
@@ -9,6 +9,7 @@ from django.contrib.auth import get_user_model
|
||||
|
||||
from basic_info import models as basic_models
|
||||
from printing import models as printing_models
|
||||
from stateflow import models as stateflow_models
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
@@ -318,3 +319,224 @@ class PlateOrderSummaryAPITestCase(TestCase):
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
|
||||
class PlateOrderDesignerSummaryAPITestCase(TestCase):
|
||||
"""测试开版订单设计师统计 API"""
|
||||
|
||||
def setUp(self):
|
||||
self.client = APIClient()
|
||||
self.merchant = basic_models.Merchant.objects.create(
|
||||
name='测试商户',
|
||||
type=basic_models.MerchantTypeEnum.FACTORY
|
||||
)
|
||||
self.user = User.objects.create_user(
|
||||
username='designer-summary-user',
|
||||
password='testpass123'
|
||||
)
|
||||
self.employee = basic_models.Employee.objects.create(
|
||||
sys_user=self.user,
|
||||
merchant=self.merchant,
|
||||
name='测试员工'
|
||||
)
|
||||
self.customer = basic_models.Customer.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='测试客户',
|
||||
created_by=self.employee
|
||||
)
|
||||
self.process = stateflow_models.Process.objects.create(name='开版流程')
|
||||
self.drawing_done = stateflow_models.State.objects.create(name='画图完成')
|
||||
self.color_done = stateflow_models.State.objects.create(name='调色完成')
|
||||
self.client.force_authenticate(user=self.user)
|
||||
|
||||
def _create_plate_order(self, *, plate_date=None):
|
||||
plate_order = printing_models.PlateOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer,
|
||||
plate_type='首版',
|
||||
production_method='定位',
|
||||
plate_date=timezone.make_aware(datetime.combine(plate_date or date(2026, 2, 8), datetime.min.time()))
|
||||
)
|
||||
business_object = stateflow_models.BusinessObject.objects.create(
|
||||
name=f'PlateOrder-{plate_order.id}',
|
||||
process=self.process,
|
||||
description='',
|
||||
)
|
||||
plate_order.business_object = business_object
|
||||
plate_order.save(update_fields=['business_object'])
|
||||
return plate_order
|
||||
|
||||
def _add_designer_param(self, plate_order, *, designer_name, state=None):
|
||||
state_log = stateflow_models.StateFlowRecord.objects.create(
|
||||
business_object=plate_order.business_object,
|
||||
state=state or self.drawing_done,
|
||||
completed_by=self.user,
|
||||
)
|
||||
stateflow_models.StateLogParameterRecord.objects.create(
|
||||
state_log=state_log,
|
||||
parameters={'设计师名称': designer_name},
|
||||
)
|
||||
|
||||
def test_returns_designer_summary(self):
|
||||
plate_order = self._create_plate_order()
|
||||
self._add_designer_param(plate_order, designer_name='设计师A')
|
||||
|
||||
response = self.client.get(
|
||||
'/api/v1/settlement/plate-orders/designer-summary/?date=2026-02-08'
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data['meta']['state_names'], ['画图完成'])
|
||||
self.assertEqual(len(response.data['data']), 1)
|
||||
self.assertEqual(response.data['data'][0]['designer_name'], '设计师A')
|
||||
self.assertEqual(response.data['data'][0]['plate_order_count'][0]['today'], 1)
|
||||
|
||||
def test_accepts_state_names_query_param(self):
|
||||
plate_order = self._create_plate_order()
|
||||
self._add_designer_param(plate_order, designer_name='设计师A', state=self.color_done)
|
||||
|
||||
response = self.client.get(
|
||||
'/api/v1/settlement/plate-orders/designer-summary/?date=2026-02-08&state_names=调色完成'
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data['meta']['state_names'], ['调色完成'])
|
||||
self.assertEqual(response.data['data'][0]['designer_name'], '设计师A')
|
||||
|
||||
def test_requires_date_parameter(self):
|
||||
response = self.client.get('/api/v1/settlement/plate-orders/designer-summary/')
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertIn('缺少 date 参数', response.data['error'])
|
||||
|
||||
def test_invalid_state_names(self):
|
||||
response = self.client.get(
|
||||
'/api/v1/settlement/plate-orders/designer-summary/?date=2026-02-08&state_names=,,'
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertIn('state_names 不能为空', response.data['error'])
|
||||
|
||||
|
||||
class DesignerWorkflowTaskSummaryAPITestCase(TestCase):
|
||||
"""测试设计师工序任务量统计 API"""
|
||||
|
||||
def setUp(self):
|
||||
self.client = APIClient()
|
||||
self.merchant = basic_models.Merchant.objects.create(
|
||||
name='测试商户',
|
||||
type=basic_models.MerchantTypeEnum.FACTORY
|
||||
)
|
||||
self.user = User.objects.create_user(
|
||||
username='designer-task-summary-user',
|
||||
password='testpass123'
|
||||
)
|
||||
self.employee = basic_models.Employee.objects.create(
|
||||
sys_user=self.user,
|
||||
merchant=self.merchant,
|
||||
name='测试员工'
|
||||
)
|
||||
self.customer = basic_models.Customer.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='测试客户',
|
||||
created_by=self.employee
|
||||
)
|
||||
self.process = stateflow_models.Process.objects.create(name='开版流程')
|
||||
self.drawing_done = stateflow_models.State.objects.create(name='画图完成')
|
||||
self.color_done = stateflow_models.State.objects.create(name='调色完成')
|
||||
self.drawing_in_progress = stateflow_models.State.objects.create(name='画图中')
|
||||
self.client.force_authenticate(user=self.user)
|
||||
|
||||
def _create_plate_order(self):
|
||||
plate_order = printing_models.PlateOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer,
|
||||
plate_type='首版',
|
||||
production_method='定位',
|
||||
plate_date=timezone.make_aware(datetime.combine(date(2026, 2, 8), datetime.min.time()))
|
||||
)
|
||||
business_object = stateflow_models.BusinessObject.objects.create(
|
||||
name=f'PlateOrder-{plate_order.id}',
|
||||
process=self.process,
|
||||
description='',
|
||||
)
|
||||
plate_order.business_object = business_object
|
||||
plate_order.save(update_fields=['business_object'])
|
||||
return plate_order
|
||||
|
||||
def _add_task(self, *, designer_name='左威', quantity='3', state=None):
|
||||
plate_order = self._create_plate_order()
|
||||
state_log = stateflow_models.StateFlowRecord.objects.create(
|
||||
business_object=plate_order.business_object,
|
||||
state=state or self.drawing_done,
|
||||
completed_by=self.user,
|
||||
)
|
||||
stateflow_models.StateLogParameterRecord.objects.create(
|
||||
state_log=state_log,
|
||||
parameters={
|
||||
'设计师名称': designer_name,
|
||||
'完成数量': quantity,
|
||||
'完成时间': '2026-02-08 10:00:00',
|
||||
},
|
||||
)
|
||||
|
||||
def test_returns_workflow_task_summary(self):
|
||||
self._add_task(designer_name='左威', quantity='3')
|
||||
|
||||
response = self.client.get(
|
||||
'/api/v1/settlement/workflows/designer-task-summary/?date=2026-02-08'
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data['meta']['state_names'], [])
|
||||
self.assertEqual(response.data['meta']['state_filter'], {'mode': 'suffix', 'suffix': '完成'})
|
||||
self.assertEqual(response.data['meta']['quantity_param_key'], '完成数量')
|
||||
self.assertEqual(len(response.data['data']), 1)
|
||||
self.assertEqual(response.data['data'][0]['designer_name'], '左威')
|
||||
self.assertEqual(response.data['data'][0]['today'], 3)
|
||||
self.assertEqual(response.data['data'][0]['task_count'][0]['state_name'], '画图完成')
|
||||
|
||||
def test_default_state_filter_includes_only_done_suffix(self):
|
||||
self._add_task(designer_name='左威', quantity='3', state=self.drawing_done)
|
||||
self._add_task(designer_name='左威', quantity='2', state=self.color_done)
|
||||
self._add_task(designer_name='左威', quantity='10', state=self.drawing_in_progress)
|
||||
|
||||
response = self.client.get(
|
||||
'/api/v1/settlement/workflows/designer-task-summary/?date=2026-02-08'
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data['data'][0]['today'], 5)
|
||||
states = {item['state_name'] for item in response.data['data'][0]['task_count']}
|
||||
self.assertEqual(states, {'画图完成', '调色完成'})
|
||||
|
||||
def test_accepts_state_and_designer_filters(self):
|
||||
self._add_task(designer_name='左威', quantity='3', state=self.drawing_done)
|
||||
self._add_task(designer_name='王五', quantity='2', state=self.color_done)
|
||||
|
||||
response = self.client.get(
|
||||
'/api/v1/settlement/workflows/designer-task-summary/'
|
||||
'?date=2026-02-08&state_names=调色完成&designer_names=王五'
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data['meta']['state_names'], ['调色完成'])
|
||||
self.assertEqual(response.data['meta']['state_filter'], {'mode': 'exact', 'state_names': ['调色完成']})
|
||||
self.assertEqual(response.data['meta']['designer_names'], ['王五'])
|
||||
self.assertEqual(len(response.data['data']), 1)
|
||||
self.assertEqual(response.data['data'][0]['designer_name'], '王五')
|
||||
self.assertEqual(response.data['data'][0]['today'], 2)
|
||||
|
||||
def test_requires_date_parameter(self):
|
||||
response = self.client.get('/api/v1/settlement/workflows/designer-task-summary/')
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertIn('缺少 date 参数', response.data['error'])
|
||||
|
||||
def test_invalid_designer_names(self):
|
||||
response = self.client.get(
|
||||
'/api/v1/settlement/workflows/designer-task-summary/?date=2026-02-08&designer_names=,,'
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertIn('designer_names 不能为空', response.data['error'])
|
||||
|
||||
@@ -13,7 +13,16 @@ 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 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__)
|
||||
@@ -120,3 +129,189 @@ class PlateOrderSummaryView(SettlementVisibilityMixin, APIView):
|
||||
{"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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user