from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError from django.db import IntegrityError, transaction from mes.models import ( CapacityUnitEnum, Device, DeviceCategory, ProductionAssignment, ProductionAssignmentStatusEnum, ) UNSET = object() ALLOWED_PRODUCTION_ASSIGNMENT_STATUS_TRANSITIONS = { ProductionAssignmentStatusEnum.DRAFT: { ProductionAssignmentStatusEnum.DRAFT, ProductionAssignmentStatusEnum.PUBLISHED, ProductionAssignmentStatusEnum.CANCELLED, }, ProductionAssignmentStatusEnum.PUBLISHED: { ProductionAssignmentStatusEnum.PUBLISHED, ProductionAssignmentStatusEnum.ACCEPTED, ProductionAssignmentStatusEnum.CANCELLED, }, ProductionAssignmentStatusEnum.ACCEPTED: { ProductionAssignmentStatusEnum.ACCEPTED, ProductionAssignmentStatusEnum.COMPLETED, }, ProductionAssignmentStatusEnum.CANCELLED: { ProductionAssignmentStatusEnum.CANCELLED, }, ProductionAssignmentStatusEnum.COMPLETED: { ProductionAssignmentStatusEnum.COMPLETED, }, } def _normalize_name(name: str, field_name: str = 'name') -> str: normalized = (name or '').strip() if not normalized: raise ValueError(f'{field_name}不能为空') return normalized def _assert_operator_belongs_to_merchant(*, operator, merchant) -> None: if operator is None: raise ValueError('操作人不能为空') if operator.merchant_id != merchant.id: raise ValueError('操作人不属于当前商户') def _assert_category_belongs_to_merchant(*, category: DeviceCategory, merchant) -> None: if category.merchant_id != merchant.id: raise ValueError('设备分类不属于当前商户') def _assert_employee_belongs_to_merchant(*, employee, merchant, role: str) -> None: if employee is None: raise ValueError(f'{role}不能为空') if employee.merchant_id != merchant.id: raise ValueError(f'{role}不属于当前商户') def _validate_peak_capacity(peak_capacity: int) -> int: if peak_capacity is None: raise ValueError('峰值产能不能为空') if int(peak_capacity) <= 0: raise ValueError('峰值产能必须大于0') return int(peak_capacity) def _validate_capacity_unit(capacity_unit: int) -> int: valid_values = {choice[0] for choice in CapacityUnitEnum.choices} if capacity_unit not in valid_values: raise ValueError('产能单位不合法') return capacity_unit def _validate_production_quantity(production_quantity: int) -> int: if production_quantity is None: raise ValueError('生产数量不能为空') if int(production_quantity) <= 0: raise ValueError('生产数量必须大于0') return int(production_quantity) def _validate_production_assignment_status(status: int) -> int: valid_values = {choice[0] for choice in ProductionAssignmentStatusEnum.choices} if status not in valid_values: raise ValueError('生产指派状态不合法') return status def _validate_production_assignment_status_transition(*, current_status: int, next_status: int) -> int: next_status = _validate_production_assignment_status(next_status) allowed_statuses = ALLOWED_PRODUCTION_ASSIGNMENT_STATUS_TRANSITIONS.get(current_status, {current_status}) if next_status not in allowed_statuses: raise ValueError('当前状态不允许变更为目标状态') return next_status def _validate_content_object_merchant(*, content_type, object_id: int, merchant) -> None: try: content_object = content_type.get_object_for_this_type(pk=object_id) except Exception as exc: raise ValueError('关联对象不存在') from exc content_object_merchant_id = getattr(content_object, 'merchant_id', None) if content_object_merchant_id is not None and content_object_merchant_id != merchant.id: raise ValueError('关联对象不属于当前商户') def _save_with_validation(instance): try: instance.full_clean() instance.save() except ValidationError as exc: if hasattr(exc, 'message_dict'): first_error = next(iter(exc.message_dict.values())) if isinstance(first_error, list) and first_error: raise ValueError(first_error[0]) from exc raise ValueError(str(exc)) from exc except IntegrityError as exc: raise ValueError('名称已存在') from exc return instance def list_device_categories_for_merchant(*, merchant): return DeviceCategory.objects.filter(merchant=merchant).select_related('merchant', 'created_by', 'operator') def get_device_category_for_merchant(*, merchant, category_id: int) -> DeviceCategory: return list_device_categories_for_merchant(merchant=merchant).get(id=category_id) @transaction.atomic def create_device_category(*, merchant, name: str, created_by, operator) -> DeviceCategory: _assert_operator_belongs_to_merchant(operator=operator, merchant=merchant) category = DeviceCategory( merchant=merchant, name=_normalize_name(name, '设备分类名称'), created_by=created_by, operator=operator, ) return _save_with_validation(category) @transaction.atomic def update_device_category(*, category: DeviceCategory, operator, name=UNSET) -> DeviceCategory: _assert_operator_belongs_to_merchant(operator=operator, merchant=category.merchant) category = DeviceCategory.objects.select_for_update().get(pk=category.pk) category.operator = operator if name is not UNSET: category.name = _normalize_name(name, '设备分类名称') return _save_with_validation(category) @transaction.atomic def delete_device_category(*, category: DeviceCategory, operator) -> None: _assert_operator_belongs_to_merchant(operator=operator, merchant=category.merchant) category = DeviceCategory.objects.select_for_update().get(pk=category.pk) category.delete() def list_devices_for_merchant(*, merchant, category_id: int | None = None, extra_json_path: list[str] | None = None, extra_json_value=UNSET): queryset = Device.objects.filter(merchant=merchant).select_related('merchant', 'category', 'created_by', 'operator') if category_id is not None: queryset = queryset.filter(category_id=category_id) if extra_json_path: lookup = '__'.join(['extra', *extra_json_path]) queryset = queryset.filter(**{lookup: extra_json_value}) return queryset def get_device_for_merchant(*, merchant, device_id: int) -> Device: return list_devices_for_merchant(merchant=merchant).get(id=device_id) @transaction.atomic def create_device(*, merchant, category: DeviceCategory, name: str, created_by, operator, peak_capacity: int, capacity_unit: int = CapacityUnitEnum.METER, extra=None) -> Device: _assert_operator_belongs_to_merchant(operator=operator, merchant=merchant) _assert_category_belongs_to_merchant(category=category, merchant=merchant) device = Device( merchant=merchant, category=category, name=_normalize_name(name, '设备名称'), peak_capacity=_validate_peak_capacity(peak_capacity), capacity_unit=_validate_capacity_unit(capacity_unit), extra=extra, created_by=created_by, operator=operator, ) return _save_with_validation(device) @transaction.atomic def update_device(*, device: Device, operator, name=UNSET, category=UNSET, peak_capacity=UNSET, capacity_unit=UNSET, extra=UNSET): device = Device.objects.select_for_update().get(pk=device.pk) _assert_operator_belongs_to_merchant(operator=operator, merchant=device.merchant) device.operator = operator if name is not UNSET: device.name = _normalize_name(name, '设备名称') if category is not UNSET: _assert_category_belongs_to_merchant(category=category, merchant=device.merchant) device.category = category if peak_capacity is not UNSET: device.peak_capacity = _validate_peak_capacity(peak_capacity) if capacity_unit is not UNSET: device.capacity_unit = _validate_capacity_unit(capacity_unit) if extra is not UNSET: device.extra = extra return _save_with_validation(device) @transaction.atomic def delete_device(*, device: Device, operator) -> None: _assert_operator_belongs_to_merchant(operator=operator, merchant=device.merchant) device = Device.objects.select_for_update().get(pk=device.pk) device.delete() def list_production_assignments_for_merchant(*, merchant, device_id: int | None = None, content_type_id: int | None = None, object_id: int | None = None, status: int | None = None): queryset = ProductionAssignment.objects.filter(merchant=merchant).select_related( 'merchant', 'device', 'content_type', 'assigner', 'assignee', 'created_by', 'operator' ) if device_id is not None: queryset = queryset.filter(device_id=device_id) if content_type_id is not None: queryset = queryset.filter(content_type_id=content_type_id) if object_id is not None: queryset = queryset.filter(object_id=object_id) if status is not None: queryset = queryset.filter(status=status) return queryset def get_production_assignment_for_merchant(*, merchant, assignment_id: int) -> ProductionAssignment: return list_production_assignments_for_merchant(merchant=merchant).get(id=assignment_id) @transaction.atomic def create_production_assignment(*, merchant, device: Device, content_type: ContentType, object_id: int, assigner, production_quantity: int, assignee=None, created_by=None, operator=None, status: int = ProductionAssignmentStatusEnum.DRAFT, extra=None) -> ProductionAssignment: _assert_employee_belongs_to_merchant(employee=assigner, merchant=merchant, role='指派者') if assignee is not None: _assert_employee_belongs_to_merchant(employee=assignee, merchant=merchant, role='被指派人') _assert_employee_belongs_to_merchant(employee=operator, merchant=merchant, role='操作人') if device.merchant_id != merchant.id: raise ValueError('设备不属于当前商户') _validate_content_object_merchant(content_type=content_type, object_id=object_id, merchant=merchant) assignment = ProductionAssignment( merchant=merchant, device=device, content_type=content_type, object_id=object_id, assigner=assigner, assignee=assignee, production_quantity=_validate_production_quantity(production_quantity), status=_validate_production_assignment_status(status), created_by=created_by, operator=operator, extra=extra, ) return _save_with_validation(assignment) @transaction.atomic def update_production_assignment(*, assignment: ProductionAssignment, operator, device=UNSET, assignee=UNSET, production_quantity=UNSET, status=UNSET, extra=UNSET): assignment = ProductionAssignment.objects.select_for_update().get(pk=assignment.pk) _assert_employee_belongs_to_merchant(employee=operator, merchant=assignment.merchant, role='操作人') assignment.operator = operator if device is not UNSET: if device.merchant_id != assignment.merchant_id: raise ValueError('设备不属于当前商户') assignment.device = device if assignee is not UNSET: if assignee is not None: _assert_employee_belongs_to_merchant(employee=assignee, merchant=assignment.merchant, role='被指派人') assignment.assignee = assignee if production_quantity is not UNSET: assignment.production_quantity = _validate_production_quantity(production_quantity) if status is not UNSET: assignment.status = _validate_production_assignment_status_transition( current_status=assignment.status, next_status=status, ) if extra is not UNSET: assignment.extra = extra return _save_with_validation(assignment) @transaction.atomic def delete_production_assignment(*, assignment: ProductionAssignment, operator) -> None: _assert_employee_belongs_to_merchant(employee=operator, merchant=assignment.merchant, role='操作人') assignment = ProductionAssignment.objects.select_for_update().get(pk=assignment.pk) assignment.delete()