forked from erp-dev/erp
fix: appversions
This commit is contained in:
@@ -1,14 +1,27 @@
|
||||
"""日结模块统计服务函数"""
|
||||
import logging
|
||||
from datetime import datetime, date
|
||||
from datetime import datetime, date, time, timedelta
|
||||
from decimal import Decimal, InvalidOperation
|
||||
|
||||
from django.db.models import Q, Sum, Case, When, Value, CharField, IntegerField
|
||||
from django.db.models import Prefetch
|
||||
from django.db.models import Q, Sum, Case, When, Value, CharField, IntegerField, Count
|
||||
from django.db.models.fields.json import KeyTextTransform
|
||||
from django.db.models.functions import Concat
|
||||
from django.utils.dateparse import parse_datetime
|
||||
from django.utils import timezone
|
||||
|
||||
from printing.models import PlateOrder
|
||||
from stateflow.models import StateFlowRecord, StateLogParameterRecord
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
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 = "完成时间"
|
||||
DEFAULT_EMPTY_TASK_QUANTITY = Decimal("1")
|
||||
|
||||
|
||||
def get_plate_order_summary_by_customer(
|
||||
merchant_id: int,
|
||||
@@ -43,6 +56,427 @@ def get_plate_order_summary_by_customer(
|
||||
return _format_plate_order_summary(aggregated_data)
|
||||
|
||||
|
||||
def get_plate_order_summary_by_designer(
|
||||
merchant_id: int,
|
||||
settlement_date: date,
|
||||
user=None,
|
||||
state_names: list[str] | tuple[str, ...] | None = None,
|
||||
designer_param_key: str = DEFAULT_DESIGNER_PARAM_KEY,
|
||||
) -> list[dict]:
|
||||
"""
|
||||
获取按设计师分组的开版订单统计。
|
||||
|
||||
设计师来源于 stateflow 参数记录,默认只统计“画图完成”节点下的
|
||||
``parameters["设计师名称"]``。同一设计师在同一开版单同一类型下出现多次只计 1 单。
|
||||
"""
|
||||
logger.info(
|
||||
f'[settlement.services] 获取开版订单设计师统计: '
|
||||
f'merchant_id={merchant_id}, date={settlement_date}, states={state_names}'
|
||||
)
|
||||
|
||||
settlement_date = _validate_and_normalize_summary_args(
|
||||
merchant_id=merchant_id,
|
||||
settlement_date=settlement_date,
|
||||
)
|
||||
normalized_state_names = _normalize_designer_summary_state_names(state_names)
|
||||
if not designer_param_key or not str(designer_param_key).strip():
|
||||
raise ValueError('designer_param_key 不能为空')
|
||||
|
||||
month_start_dt, next_day_dt, today_start_dt = _get_summary_datetime_ranges(
|
||||
settlement_date
|
||||
)
|
||||
queryset = _get_designer_plate_order_queryset(
|
||||
merchant_id=merchant_id,
|
||||
user=user,
|
||||
state_names=normalized_state_names,
|
||||
designer_param_key=designer_param_key,
|
||||
month_start_dt=month_start_dt,
|
||||
next_day_dt=next_day_dt,
|
||||
)
|
||||
aggregated_data = _aggregate_plate_orders_by_designer_and_type(
|
||||
queryset=queryset,
|
||||
today_start_dt=today_start_dt,
|
||||
next_day_dt=next_day_dt,
|
||||
)
|
||||
return _format_plate_order_designer_summary(aggregated_data)
|
||||
|
||||
|
||||
def get_designer_workflow_task_summary(
|
||||
merchant_id: int,
|
||||
settlement_date: date,
|
||||
user=None,
|
||||
state_names: list[str] | tuple[str, ...] | None = None,
|
||||
designer_names: list[str] | tuple[str, ...] | None = None,
|
||||
designer_param_key: str = DEFAULT_DESIGNER_PARAM_KEY,
|
||||
quantity_param_key: str = DEFAULT_DESIGNER_TASK_QUANTITY_PARAM_KEY,
|
||||
time_param_key: str = DEFAULT_DESIGNER_TASK_TIME_PARAM_KEY,
|
||||
) -> list[dict]:
|
||||
"""
|
||||
获取设计师工序任务量统计。
|
||||
|
||||
统计单位是未撤销的 StateFlowRecord。每条状态流转记录最多计入一次,
|
||||
即使其下存在多条 StateLogParameterRecord。
|
||||
"""
|
||||
logger.info(
|
||||
f'[settlement.services] 获取设计师工序任务量统计: '
|
||||
f'merchant_id={merchant_id}, date={settlement_date}, states={state_names}'
|
||||
)
|
||||
|
||||
settlement_date = _validate_and_normalize_summary_args(
|
||||
merchant_id=merchant_id,
|
||||
settlement_date=settlement_date,
|
||||
)
|
||||
normalized_state_names = _normalize_optional_state_names(state_names)
|
||||
normalized_designer_names = _normalize_optional_names(designer_names)
|
||||
for key, label in [
|
||||
(designer_param_key, 'designer_param_key'),
|
||||
(quantity_param_key, 'quantity_param_key'),
|
||||
(time_param_key, 'time_param_key'),
|
||||
]:
|
||||
if not key or not str(key).strip():
|
||||
raise ValueError(f'{label} 不能为空')
|
||||
|
||||
month_start_dt, next_day_dt, today_start_dt = _get_summary_datetime_ranges(
|
||||
settlement_date
|
||||
)
|
||||
queryset = _get_designer_workflow_task_queryset(
|
||||
merchant_id=merchant_id,
|
||||
user=user,
|
||||
state_names=normalized_state_names,
|
||||
designer_param_key=designer_param_key,
|
||||
)
|
||||
summary = _aggregate_designer_workflow_tasks(
|
||||
queryset=queryset,
|
||||
month_start_dt=month_start_dt,
|
||||
today_start_dt=today_start_dt,
|
||||
next_day_dt=next_day_dt,
|
||||
designer_names=normalized_designer_names,
|
||||
designer_param_key=designer_param_key,
|
||||
quantity_param_key=quantity_param_key,
|
||||
time_param_key=time_param_key,
|
||||
)
|
||||
return _format_designer_workflow_task_summary(summary)
|
||||
|
||||
|
||||
def _normalize_optional_names(
|
||||
names: list[str] | tuple[str, ...] | None,
|
||||
) -> tuple[str, ...] | None:
|
||||
if names is None:
|
||||
return None
|
||||
normalized = tuple(name.strip() for name in names if isinstance(name, str) and name.strip())
|
||||
if not normalized:
|
||||
raise ValueError('designer_names 不能为空')
|
||||
return normalized
|
||||
|
||||
|
||||
def _normalize_optional_state_names(
|
||||
state_names: list[str] | tuple[str, ...] | None,
|
||||
) -> tuple[str, ...] | None:
|
||||
if state_names is None:
|
||||
return None
|
||||
normalized = tuple(
|
||||
name.strip() for name in state_names if isinstance(name, str) and name.strip()
|
||||
)
|
||||
if not normalized:
|
||||
raise ValueError('state_names 不能为空')
|
||||
return normalized
|
||||
|
||||
|
||||
def _normalize_designer_summary_state_names(
|
||||
state_names: list[str] | tuple[str, ...] | None,
|
||||
) -> tuple[str, ...]:
|
||||
"""标准化设计师统计节点名。"""
|
||||
if state_names is None:
|
||||
return DEFAULT_DESIGNER_SUMMARY_STATE_NAMES
|
||||
|
||||
normalized = tuple(
|
||||
name.strip() for name in state_names if isinstance(name, str) and name.strip()
|
||||
)
|
||||
if not normalized:
|
||||
raise ValueError('state_names 不能为空')
|
||||
return normalized
|
||||
|
||||
|
||||
def _get_summary_datetime_ranges(settlement_date: date):
|
||||
"""返回月初、次日、当天起始的当前时区 aware datetime。"""
|
||||
current_tz = timezone.get_current_timezone()
|
||||
month_start = settlement_date.replace(day=1)
|
||||
month_start_dt = timezone.make_aware(
|
||||
datetime.combine(month_start, time.min), current_tz
|
||||
)
|
||||
today_start_dt = timezone.make_aware(
|
||||
datetime.combine(settlement_date, time.min), current_tz
|
||||
)
|
||||
next_day_dt = timezone.make_aware(
|
||||
datetime.combine(settlement_date + timedelta(days=1), time.min), current_tz
|
||||
)
|
||||
return month_start_dt, next_day_dt, today_start_dt
|
||||
|
||||
|
||||
def _get_designer_workflow_task_queryset(
|
||||
*,
|
||||
merchant_id: int,
|
||||
user=None,
|
||||
state_names: tuple[str, ...] | None,
|
||||
designer_param_key: str,
|
||||
):
|
||||
queryset = StateFlowRecord.objects.filter(
|
||||
is_cancelled=False,
|
||||
business_object__plate_order__merchant_id=merchant_id,
|
||||
business_object__plate_order__plate_type__isnull=False,
|
||||
business_object__plate_order__production_method__isnull=False,
|
||||
parameter_records__parameters__has_key=designer_param_key,
|
||||
).select_related(
|
||||
'state',
|
||||
'business_object__plate_order',
|
||||
'business_object__plate_order__customer',
|
||||
).prefetch_related(
|
||||
Prefetch(
|
||||
'parameter_records',
|
||||
queryset=StateLogParameterRecord.objects.order_by('created_at', 'id'),
|
||||
to_attr='_prefetched_parameter_records',
|
||||
)
|
||||
).distinct()
|
||||
|
||||
if state_names is None:
|
||||
queryset = queryset.filter(state__name__endswith=DEFAULT_DESIGNER_TASK_STATE_NAME_SUFFIX)
|
||||
else:
|
||||
queryset = queryset.filter(state__name__in=state_names)
|
||||
|
||||
if user and not user.is_superuser:
|
||||
emp = getattr(user, 'employee', None)
|
||||
if emp:
|
||||
queryset = queryset.filter(
|
||||
Q(business_object__plate_order__customer__created_by=emp) |
|
||||
Q(business_object__plate_order__customer__visible_employees=emp)
|
||||
).distinct()
|
||||
|
||||
return queryset.order_by('id')
|
||||
|
||||
|
||||
def _aggregate_designer_workflow_tasks(
|
||||
*,
|
||||
queryset,
|
||||
month_start_dt: datetime,
|
||||
today_start_dt: datetime,
|
||||
next_day_dt: datetime,
|
||||
designer_names: tuple[str, ...] | None,
|
||||
designer_param_key: str,
|
||||
quantity_param_key: str,
|
||||
time_param_key: str,
|
||||
) -> dict[tuple[str, str, str], dict[str, Decimal]]:
|
||||
summary: dict[tuple[str, str, str], dict[str, Decimal]] = {}
|
||||
designer_name_filter = set(designer_names) if designer_names is not None else None
|
||||
|
||||
for state_log in queryset:
|
||||
parameters = _merge_state_log_parameters(state_log)
|
||||
designer_name = str(parameters.get(designer_param_key) or '').strip()
|
||||
if not designer_name:
|
||||
continue
|
||||
if designer_name_filter is not None and designer_name not in designer_name_filter:
|
||||
continue
|
||||
|
||||
effective_time = _resolve_task_effective_time(
|
||||
parameters.get(time_param_key),
|
||||
fallback=state_log.completed_at,
|
||||
)
|
||||
if effective_time is None or effective_time < month_start_dt or effective_time >= next_day_dt:
|
||||
continue
|
||||
|
||||
plate_order = state_log.business_object.plate_order
|
||||
task_quantity = _parse_task_quantity(parameters.get(quantity_param_key))
|
||||
key = (
|
||||
designer_name,
|
||||
state_log.state.name,
|
||||
f'{plate_order.plate_type}-{plate_order.production_method}',
|
||||
)
|
||||
if key not in summary:
|
||||
summary[key] = {'today': Decimal('0'), 'current_month': Decimal('0')}
|
||||
|
||||
summary[key]['current_month'] += task_quantity
|
||||
if today_start_dt <= effective_time < next_day_dt:
|
||||
summary[key]['today'] += task_quantity
|
||||
|
||||
return summary
|
||||
|
||||
|
||||
def _merge_state_log_parameters(state_log: StateFlowRecord) -> dict:
|
||||
records = getattr(state_log, '_prefetched_parameter_records', None)
|
||||
if records is None:
|
||||
records = state_log.parameter_records.all().order_by('created_at', 'id')
|
||||
|
||||
merged = {}
|
||||
for record in records:
|
||||
if isinstance(record.parameters, dict):
|
||||
merged.update(record.parameters)
|
||||
return merged
|
||||
|
||||
|
||||
def _parse_task_quantity(value) -> Decimal:
|
||||
try:
|
||||
quantity = Decimal(str(value).strip())
|
||||
except (InvalidOperation, ValueError, TypeError, AttributeError):
|
||||
return DEFAULT_EMPTY_TASK_QUANTITY
|
||||
if quantity > 0:
|
||||
return quantity
|
||||
return DEFAULT_EMPTY_TASK_QUANTITY
|
||||
|
||||
|
||||
def _resolve_task_effective_time(value, *, fallback):
|
||||
parsed = None
|
||||
if isinstance(value, datetime):
|
||||
parsed = value
|
||||
elif isinstance(value, str) and value.strip():
|
||||
raw_value = value.strip()
|
||||
parsed = parse_datetime(raw_value)
|
||||
if parsed is None:
|
||||
for fmt in ('%Y-%m-%d %H:%M:%S', '%Y-%m-%d %H:%M', '%Y-%m-%d'):
|
||||
try:
|
||||
parsed = datetime.strptime(raw_value, fmt)
|
||||
break
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
effective = parsed or fallback
|
||||
if effective is None:
|
||||
return None
|
||||
if timezone.is_naive(effective):
|
||||
effective = timezone.make_aware(effective, timezone.get_current_timezone())
|
||||
return effective.astimezone(timezone.get_current_timezone())
|
||||
|
||||
|
||||
def _format_decimal_quantity(value: Decimal):
|
||||
if value == value.to_integral_value():
|
||||
return int(value)
|
||||
return str(value.normalize())
|
||||
|
||||
|
||||
def _format_designer_workflow_task_summary(
|
||||
summary: dict[tuple[str, str, str], dict[str, Decimal]],
|
||||
) -> list[dict]:
|
||||
result: dict[str, dict] = {}
|
||||
for (designer_name, state_name, type_name), counts in sorted(summary.items()):
|
||||
if designer_name not in result:
|
||||
result[designer_name] = {
|
||||
'designer_name': designer_name,
|
||||
'task_count': [],
|
||||
'today': Decimal('0'),
|
||||
'current_month': Decimal('0'),
|
||||
}
|
||||
result[designer_name]['task_count'].append({
|
||||
'state_name': state_name,
|
||||
'type': type_name,
|
||||
'today': _format_decimal_quantity(counts['today']),
|
||||
'current_month': _format_decimal_quantity(counts['current_month']),
|
||||
})
|
||||
result[designer_name]['today'] += counts['today']
|
||||
result[designer_name]['current_month'] += counts['current_month']
|
||||
|
||||
formatted = []
|
||||
for item in result.values():
|
||||
item['today'] = _format_decimal_quantity(item['today'])
|
||||
item['current_month'] = _format_decimal_quantity(item['current_month'])
|
||||
formatted.append(item)
|
||||
return formatted
|
||||
|
||||
|
||||
def _get_designer_plate_order_queryset(
|
||||
*,
|
||||
merchant_id: int,
|
||||
user=None,
|
||||
state_names: tuple[str, ...],
|
||||
designer_param_key: str,
|
||||
month_start_dt: datetime,
|
||||
next_day_dt: datetime,
|
||||
):
|
||||
"""获取设计师统计的 stateflow 参数记录基础查询集。"""
|
||||
queryset = StateLogParameterRecord.objects.filter(
|
||||
parameters__has_key=designer_param_key,
|
||||
state_log__is_cancelled=False,
|
||||
state_log__state__name__in=state_names,
|
||||
state_log__business_object__plate_order__merchant_id=merchant_id,
|
||||
state_log__business_object__plate_order__plate_type__isnull=False,
|
||||
state_log__business_object__plate_order__production_method__isnull=False,
|
||||
state_log__business_object__plate_order__plate_date__gte=month_start_dt,
|
||||
state_log__business_object__plate_order__plate_date__lt=next_day_dt,
|
||||
).annotate(
|
||||
designer_name=KeyTextTransform(designer_param_key, 'parameters'),
|
||||
type=Concat(
|
||||
'state_log__business_object__plate_order__plate_type',
|
||||
Value('-'),
|
||||
'state_log__business_object__plate_order__production_method',
|
||||
output_field=CharField(),
|
||||
),
|
||||
).exclude(
|
||||
designer_name__isnull=True,
|
||||
).exclude(
|
||||
designer_name='',
|
||||
)
|
||||
|
||||
if user and not user.is_superuser:
|
||||
emp = getattr(user, 'employee', None)
|
||||
if emp:
|
||||
queryset = queryset.filter(
|
||||
Q(state_log__business_object__plate_order__customer__created_by=emp) |
|
||||
Q(state_log__business_object__plate_order__customer__visible_employees=emp)
|
||||
).distinct()
|
||||
|
||||
return queryset
|
||||
|
||||
|
||||
def _aggregate_plate_orders_by_designer_and_type(
|
||||
*,
|
||||
queryset,
|
||||
today_start_dt: datetime,
|
||||
next_day_dt: datetime,
|
||||
):
|
||||
"""按设计师和类型聚合开版订单数量。"""
|
||||
plate_order_path = 'state_log__business_object__plate_order'
|
||||
return queryset.values(
|
||||
'designer_name',
|
||||
'type',
|
||||
).annotate(
|
||||
today=Count(
|
||||
plate_order_path,
|
||||
filter=Q(
|
||||
state_log__business_object__plate_order__plate_date__gte=today_start_dt,
|
||||
state_log__business_object__plate_order__plate_date__lt=next_day_dt,
|
||||
),
|
||||
distinct=True,
|
||||
),
|
||||
current_month=Count(
|
||||
plate_order_path,
|
||||
distinct=True,
|
||||
),
|
||||
).order_by('designer_name', 'type')
|
||||
|
||||
|
||||
def _format_plate_order_designer_summary(aggregated_data):
|
||||
"""格式化设计师聚合结果为 API 返回格式。"""
|
||||
result = {}
|
||||
|
||||
for item in aggregated_data:
|
||||
designer_name = item['designer_name']
|
||||
if designer_name not in result:
|
||||
result[designer_name] = {
|
||||
'designer_name': designer_name,
|
||||
'plate_order_count': [],
|
||||
}
|
||||
|
||||
result[designer_name]['plate_order_count'].append({
|
||||
'type': item['type'],
|
||||
'today': item['today'],
|
||||
'current_month': item['current_month'],
|
||||
})
|
||||
|
||||
for designer_name in result:
|
||||
result[designer_name]['plate_order_count'] = _filter_zero_data(
|
||||
result[designer_name]['plate_order_count']
|
||||
)
|
||||
|
||||
return [v for v in result.values() if v['plate_order_count']]
|
||||
|
||||
|
||||
def _validate_and_normalize_summary_args(
|
||||
merchant_id: int,
|
||||
settlement_date: date,
|
||||
|
||||
@@ -8,6 +8,7 @@ from django.utils import timezone
|
||||
from basic_info import models as basic_models
|
||||
from printing import models as printing_models
|
||||
from settlement import services
|
||||
from stateflow import models as stateflow_models
|
||||
|
||||
|
||||
User = get_user_model()
|
||||
@@ -508,3 +509,308 @@ class GetPlateOrderSummaryByCustomerTestCase(SettlementServiceTestCase):
|
||||
settlement_date='2026-02-08',
|
||||
user=self.user
|
||||
)
|
||||
|
||||
|
||||
class GetPlateOrderSummaryByDesignerTestCase(SettlementServiceTestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
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='调色完成')
|
||||
|
||||
def _create_plate_order(self, *, customer=None, plate_date=None, plate_type='首版', production_method='定位'):
|
||||
plate_order = printing_models.PlateOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=customer or self.customer1,
|
||||
plate_type=plate_type,
|
||||
production_method=production_method,
|
||||
plate_date=timezone.make_aware(datetime.combine(plate_date or self.settlement_date, 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, cancelled=False):
|
||||
state_log = stateflow_models.StateFlowRecord.objects.create(
|
||||
business_object=plate_order.business_object,
|
||||
state=state or self.drawing_done,
|
||||
completed_by=self.user,
|
||||
is_cancelled=cancelled,
|
||||
)
|
||||
return stateflow_models.StateLogParameterRecord.objects.create(
|
||||
state_log=state_log,
|
||||
parameters={'设计师名称': designer_name},
|
||||
)
|
||||
|
||||
def test_returns_summary_for_drawing_done_designer(self):
|
||||
plate_order = self._create_plate_order()
|
||||
self._add_designer_param(plate_order, designer_name='设计师A')
|
||||
|
||||
result = services.get_plate_order_summary_by_designer(
|
||||
self.merchant.id,
|
||||
self.settlement_date,
|
||||
self.user,
|
||||
)
|
||||
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assertEqual(result[0]['designer_name'], '设计师A')
|
||||
self.assertEqual(result[0]['plate_order_count'][0]['type'], '首版-定位')
|
||||
self.assertEqual(result[0]['plate_order_count'][0]['today'], 1)
|
||||
self.assertEqual(result[0]['plate_order_count'][0]['current_month'], 1)
|
||||
|
||||
def test_defaults_to_drawing_done_only(self):
|
||||
plate_order = self._create_plate_order()
|
||||
self._add_designer_param(plate_order, designer_name='设计师A', state=self.color_done)
|
||||
|
||||
result = services.get_plate_order_summary_by_designer(
|
||||
self.merchant.id,
|
||||
self.settlement_date,
|
||||
self.user,
|
||||
)
|
||||
|
||||
self.assertEqual(result, [])
|
||||
|
||||
|
||||
class GetDesignerWorkflowTaskSummaryTestCase(SettlementServiceTestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
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='画图中')
|
||||
|
||||
def _create_plate_order(self, *, customer=None, plate_type='首版', production_method='定位'):
|
||||
plate_order = printing_models.PlateOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=customer or self.customer1,
|
||||
plate_type=plate_type,
|
||||
production_method=production_method,
|
||||
plate_date=timezone.make_aware(datetime.combine(self.settlement_date, 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 _create_state_log(self, plate_order, *, state=None, cancelled=False):
|
||||
return stateflow_models.StateFlowRecord.objects.create(
|
||||
business_object=plate_order.business_object,
|
||||
state=state or self.drawing_done,
|
||||
completed_by=self.user,
|
||||
is_cancelled=cancelled,
|
||||
)
|
||||
|
||||
def _add_params(self, state_log, params):
|
||||
return stateflow_models.StateLogParameterRecord.objects.create(
|
||||
state_log=state_log,
|
||||
parameters=params,
|
||||
)
|
||||
|
||||
def _add_designer_param(self, plate_order, *, designer_name, state=None, cancelled=False):
|
||||
state_log = self._create_state_log(
|
||||
plate_order,
|
||||
state=state or self.drawing_done,
|
||||
cancelled=cancelled,
|
||||
)
|
||||
return self._add_params(state_log, {
|
||||
'设计师名称': designer_name,
|
||||
'完成时间': '2026-02-08 10:00:00',
|
||||
})
|
||||
|
||||
def test_sums_task_quantity_by_state_and_type(self):
|
||||
first = self._create_plate_order()
|
||||
second = self._create_plate_order()
|
||||
first_log = self._create_state_log(first, state=self.drawing_done)
|
||||
second_log = self._create_state_log(second, state=self.color_done)
|
||||
self._add_params(first_log, {
|
||||
'设计师名称': '左威',
|
||||
'完成数量': '3',
|
||||
'完成时间': '2026-02-08 10:00:00',
|
||||
})
|
||||
self._add_params(second_log, {
|
||||
'设计师名称': '左威',
|
||||
'完成数量': '2',
|
||||
'完成时间': '2026-02-08 11:00:00',
|
||||
})
|
||||
|
||||
result = services.get_designer_workflow_task_summary(
|
||||
self.merchant.id,
|
||||
self.settlement_date,
|
||||
self.user,
|
||||
state_names=['画图完成', '调色完成'],
|
||||
)
|
||||
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assertEqual(result[0]['designer_name'], '左威')
|
||||
self.assertEqual(result[0]['today'], 5)
|
||||
self.assertEqual(result[0]['current_month'], 5)
|
||||
by_state = {item['state_name']: item for item in result[0]['task_count']}
|
||||
self.assertEqual(by_state['画图完成']['today'], 3)
|
||||
self.assertEqual(by_state['调色完成']['today'], 2)
|
||||
|
||||
def test_default_state_filter_includes_only_done_suffix(self):
|
||||
for state, quantity in [
|
||||
(self.drawing_done, '3'),
|
||||
(self.color_done, '2'),
|
||||
(self.drawing_in_progress, '10'),
|
||||
]:
|
||||
plate_order = self._create_plate_order()
|
||||
state_log = self._create_state_log(plate_order, state=state)
|
||||
self._add_params(state_log, {
|
||||
'设计师名称': '左威',
|
||||
'完成数量': quantity,
|
||||
'完成时间': '2026-02-08 10:00:00',
|
||||
})
|
||||
|
||||
result = services.get_designer_workflow_task_summary(
|
||||
self.merchant.id,
|
||||
self.settlement_date,
|
||||
self.user,
|
||||
)
|
||||
|
||||
self.assertEqual(result[0]['today'], 5)
|
||||
states = {item['state_name'] for item in result[0]['task_count']}
|
||||
self.assertEqual(states, {'画图完成', '调色完成'})
|
||||
|
||||
def test_invalid_quantity_defaults_to_one_and_does_not_dedupe_plate_order(self):
|
||||
plate_order = self._create_plate_order()
|
||||
values = [None, '', '0', '-2', 'bad']
|
||||
for index, value in enumerate(values):
|
||||
state_log = self._create_state_log(plate_order)
|
||||
self._add_params(state_log, {
|
||||
'设计师名称': '左威',
|
||||
'完成数量': value,
|
||||
'完成时间': f'2026-02-08 10:0{index}:00',
|
||||
})
|
||||
|
||||
result = services.get_designer_workflow_task_summary(
|
||||
self.merchant.id,
|
||||
self.settlement_date,
|
||||
self.user,
|
||||
)
|
||||
|
||||
self.assertEqual(result[0]['today'], 5)
|
||||
self.assertEqual(result[0]['current_month'], 5)
|
||||
|
||||
def test_multiple_parameter_records_for_one_state_log_count_once(self):
|
||||
plate_order = self._create_plate_order()
|
||||
state_log = self._create_state_log(plate_order)
|
||||
self._add_params(state_log, {'设计师名称': '左威'})
|
||||
self._add_params(state_log, {
|
||||
'完成数量': '4',
|
||||
'完成时间': '2026-02-08 10:00:00',
|
||||
})
|
||||
|
||||
result = services.get_designer_workflow_task_summary(
|
||||
self.merchant.id,
|
||||
self.settlement_date,
|
||||
self.user,
|
||||
)
|
||||
|
||||
self.assertEqual(result[0]['today'], 4)
|
||||
self.assertEqual(result[0]['current_month'], 4)
|
||||
|
||||
def test_uses_completed_at_when_time_param_missing(self):
|
||||
plate_order = self._create_plate_order()
|
||||
state_log = self._create_state_log(plate_order)
|
||||
completed_at = timezone.make_aware(datetime(2026, 2, 8, 12, 0, 0))
|
||||
stateflow_models.StateFlowRecord.objects.filter(id=state_log.id).update(completed_at=completed_at)
|
||||
state_log.refresh_from_db()
|
||||
self._add_params(state_log, {
|
||||
'设计师名称': '左威',
|
||||
'完成数量': '2',
|
||||
})
|
||||
|
||||
result = services.get_designer_workflow_task_summary(
|
||||
self.merchant.id,
|
||||
self.settlement_date,
|
||||
self.user,
|
||||
)
|
||||
|
||||
self.assertEqual(result[0]['today'], 2)
|
||||
|
||||
def test_filters_cancelled_and_designer_names(self):
|
||||
first = self._create_plate_order()
|
||||
second = self._create_plate_order()
|
||||
cancelled_log = self._create_state_log(first, cancelled=True)
|
||||
kept_log = self._create_state_log(second)
|
||||
self._add_params(cancelled_log, {
|
||||
'设计师名称': '左威',
|
||||
'完成数量': '10',
|
||||
'完成时间': '2026-02-08 10:00:00',
|
||||
})
|
||||
self._add_params(kept_log, {
|
||||
'设计师名称': '王五',
|
||||
'完成数量': '3',
|
||||
'完成时间': '2026-02-08 10:00:00',
|
||||
})
|
||||
|
||||
result = services.get_designer_workflow_task_summary(
|
||||
self.merchant.id,
|
||||
self.settlement_date,
|
||||
self.user,
|
||||
designer_names=['王五'],
|
||||
)
|
||||
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assertEqual(result[0]['designer_name'], '王五')
|
||||
self.assertEqual(result[0]['today'], 3)
|
||||
|
||||
def test_accepts_custom_state_names(self):
|
||||
plate_order = self._create_plate_order()
|
||||
self._add_designer_param(plate_order, designer_name='设计师A', state=self.color_done)
|
||||
|
||||
result = services.get_plate_order_summary_by_designer(
|
||||
self.merchant.id,
|
||||
self.settlement_date,
|
||||
self.user,
|
||||
state_names=['画图完成', '调色完成'],
|
||||
)
|
||||
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assertEqual(result[0]['designer_name'], '设计师A')
|
||||
|
||||
def test_distincts_same_order_same_designer(self):
|
||||
plate_order = self._create_plate_order()
|
||||
self._add_designer_param(plate_order, designer_name='设计师A')
|
||||
self._add_designer_param(plate_order, designer_name='设计师A')
|
||||
|
||||
result = services.get_plate_order_summary_by_designer(
|
||||
self.merchant.id,
|
||||
self.settlement_date,
|
||||
self.user,
|
||||
)
|
||||
|
||||
self.assertEqual(result[0]['plate_order_count'][0]['today'], 1)
|
||||
self.assertEqual(result[0]['plate_order_count'][0]['current_month'], 1)
|
||||
|
||||
def test_applies_customer_visibility(self):
|
||||
other_employee = basic_models.Employee.objects.create(
|
||||
sys_user=User.objects.create_user('designer-other', 'pass123'),
|
||||
merchant=self.merchant,
|
||||
name='其他员工'
|
||||
)
|
||||
invisible_customer = basic_models.Customer.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='不可见客户',
|
||||
created_by=other_employee,
|
||||
)
|
||||
plate_order = self._create_plate_order(customer=invisible_customer)
|
||||
self._add_designer_param(plate_order, designer_name='设计师A')
|
||||
|
||||
result = services.get_plate_order_summary_by_designer(
|
||||
self.merchant.id,
|
||||
self.settlement_date,
|
||||
self.user,
|
||||
)
|
||||
|
||||
self.assertEqual(result, [])
|
||||
|
||||
Reference in New Issue
Block a user