forked from erp-dev/erp
305 lines
12 KiB
Python
305 lines
12 KiB
Python
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 | None = None,
|
|
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 is not None and 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 is not None and 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()
|