diff --git a/apps/production/filters.py b/apps/production/filters.py index a505264..c472f1a 100644 --- a/apps/production/filters.py +++ b/apps/production/filters.py @@ -1,7 +1,26 @@ from django_filters.rest_framework import FilterSet from django_filters.filters import * +from apps.production.models import * + + +class ProductionOrderFilter(FilterSet): + start_date = DateFilter(field_name='create_time', lookup_expr='gte', label='开始日期') + end_date = DateFilter(field_name='create_time', lookup_expr='lt', label='结束日期') + + class Meta: + model = ProductionOrder + fields = ['sales_order', 'goods', 'status', 'creator', 'start_date', 'end_date'] + + +class ProductionRecordFilter(FilterSet): + start_date = DateFilter(field_name='create_time', lookup_expr='gte', label='开始日期') + end_date = DateFilter(field_name='create_time', lookup_expr='lt', label='结束日期') + + class Meta: + model = ProductionRecord + fields = ['production_order', 'goods', 'creator', 'start_date', 'end_date'] __all__ = [ - + 'ProductionOrderFilter', 'ProductionRecordFilter', ] diff --git a/apps/production/models.py b/apps/production/models.py index e8a4762..2cb2d17 100644 --- a/apps/production/models.py +++ b/apps/production/models.py @@ -1,22 +1,44 @@ +from extensions.common.base import * from extensions.models import * class ProductionOrder(Model): """生产单据""" + class Status(TextChoices): + IN_PLAN = ('in_plan', '计划中') + IN_PROGRESS = ('in_progress', '进行中') + COMPLETED = ('completed', '已完成') + CLOSED = ('closed', '强制关闭') + number = CharField(max_length=32, verbose_name='编号') is_related = BooleanField(default=False, verbose_name='关联状态') sales_order = ForeignKey('sales.SalesOrder', on_delete=PROTECT, null=True, related_name='production_orders', verbose_name='销售单') goods = ForeignKey('goods.Goods', on_delete=PROTECT, related_name='production_orders', verbose_name='产品') total_quantity = FloatField(verbose_name='生产总数') + quantity_produced = FloatField(verbose_name='已生产数量') remain_quantity = FloatField(verbose_name='剩余数量') start_time = DateTimeField(null=True, verbose_name='开始时间') end_time = DateTimeField(null=True, verbose_name='结束时间') + status = CharField(max_length=32, choices=Status.choices, default=Status.IN_PLAN, verbose_name='状态') creator = ForeignKey('system.User', on_delete=PROTECT, related_name='created_production_orders', verbose_name='创建人') create_time = DateTimeField(auto_now_add=True, verbose_name='创建时间') team = ForeignKey('system.Team', on_delete=CASCADE, related_name='production_orders') + @classmethod + def get_number(cls, team): + start_date, end_date = pendulum.today().to_datetime_string(), pendulum.tomorrow().to_datetime_string() + instance = cls.objects.filter(team=team, create_time__gte=start_date, create_time__lt=end_date).last() + + try: + result = re.match('^(.*?)([1-9]+)$', instance.number) + number = result.group(1) + str(int(result.group(2)) + 1) + except AttributeError: + number = 'SC' + pendulum.today().format('YYYYMMDD') + '0001' + + return number + class ProductionRecord(Model): """生产记录""" diff --git a/apps/production/permissions.py b/apps/production/permissions.py index f6dff13..ef3d4c9 100644 --- a/apps/production/permissions.py +++ b/apps/production/permissions.py @@ -1,6 +1,14 @@ from extensions.permissions import ModelPermission -__all__ = [ +class ProductionOrderPermission(ModelPermission): + code = 'production_order' + +class ProductionRecordPermission(ModelPermission): + code = 'production_record' + + +__all__ = [ + 'ProductionOrderPermission', 'ProductionRecordPermission', ] diff --git a/apps/production/schemas.py b/apps/production/schemas.py index 8d7cdfd..1dffa8c 100644 --- a/apps/production/schemas.py +++ b/apps/production/schemas.py @@ -1,6 +1,10 @@ from extensions.serializers import * -__all__ = [ +class NumberResponse(Serializer): + number = CharField(label='编号') + +__all__ = [ + 'NumberResponse', ] diff --git a/apps/production/serializers.py b/apps/production/serializers.py index 448df9e..85374fe 100644 --- a/apps/production/serializers.py +++ b/apps/production/serializers.py @@ -9,13 +9,16 @@ from apps.goods.models import * class ProductionOrderSerializer(BaseSerializer): """生产单据""" + sales_order_number = CharField(source='sales_order.number', read_only=True, label='销售单号') goods_number = CharField(source='goods.number', read_only=True, label='产品编号') goods_name = CharField(source='goods.name', read_only=True, label='产品名称') + status_display = CharField(source='get_status_display', read_only=True, label='状态') creator_name = CharField(source='creator.name', read_only=True, label='创建人名称') class Meta: model = ProductionOrder - read_only_fields = ['id', 'remain_quantity', 'goods_number', 'goods_name', 'creator', + read_only_fields = ['id', 'sales_order_number', 'remain_quantity', 'goods_number', 'goods_name', + 'quantity_produced', 'remain_quantity', 'status', 'status_display', 'creator', 'creator_name', 'create_time'] fields = ['number', 'is_related', 'sales_order', 'goods', 'total_quantity', 'start_time', 'end_time', *read_only_fields] diff --git a/apps/production/urls.py b/apps/production/urls.py index 60018cb..fc365f6 100644 --- a/apps/production/urls.py +++ b/apps/production/urls.py @@ -1,5 +1,8 @@ from extensions.routers import * +from apps.production.views import * router = BaseRouter() +router.register('production_orders', ProductionOrderViewSet, 'production_order') +router.register('production_records', ProductionRecordViewSet, 'production_record') urlpatterns = router.urls diff --git a/apps/production/views.py b/apps/production/views.py index f60eb9f..9f0bce5 100644 --- a/apps/production/views.py +++ b/apps/production/views.py @@ -3,8 +3,103 @@ from extensions.common.base import * from extensions.permissions import * from extensions.exceptions import * from extensions.viewsets import * +from apps.production.serializers import * +from apps.production.permissions import * +from apps.production.filters import * +from apps.production.schemas import * +from apps.production.models import * + + +class ProductionOrderViewSet(ModelViewSet): + """生产单据""" + + serializer_class = ProductionOrderSerializer + permission_classes = [IsAuthenticated, ProductionOrderPermission] + filterset_class = ProductionOrderFilter + search_fields = ['number', 'sales_order__number', 'goods__number', 'goods__name'] + ordering_fields = ['number', 'total_quantity', 'quantity_produced', 'remain_quantity', + 'start_time', 'end_time', 'create_time'] + select_related_fields = ['sales_order', 'goods', 'creator'] + queryset = ProductionOrder.objects.all() + + def perform_update(self, serializer): + instance = serializer.instance + if instance.status != ProductionOrder.Status.IN_PLAN: + raise ValidationError(f'工单{instance.number}{instance.get_status_display()}, 无法编辑') + return super().perform_update(serializer) + + def perform_destroy(self, instance): + if instance.status != ProductionOrder.Status.IN_PLAN: + raise ValidationError(f'工单{instance.number}{instance.get_status_display()}, 无法删除') + return super().perform_destroy(instance) + + @extend_schema(responses={200: NumberResponse}) + @action(detail=False, methods=['get']) + def number(self, request, *args, **kwargs): + """获取编号""" + + number = ProductionOrder.get_number(self.team) + return Response(data={'number': number}, status=status.HTTP_200_OK) + + @extend_schema(responses={200: ProductionOrderSerializer}) + @action(detail=True, methods=['get']) + def issue(self, request, *args, **kwargs): + """发布工单""" + + instance = self.get_object() + if instance.status != ProductionOrder.Status.IN_PLAN: + raise ValidationError(f'工单{instance.number}{instance.get_status_display()}, 无法发布工单') + + instance.status = ProductionOrder.Status.IN_PROGRESS + instance.save(update_fields=['status']) + + serializer = ProductionOrderSerializer(instance=instance) + return Response(data=serializer.data, status=status.HTTP_200_OK) + + @extend_schema(responses={200: ProductionOrderSerializer}) + @action(detail=True, methods=['get']) + def close(self, request, *args, **kwargs): + """关闭工单""" + + instance = self.get_object() + if instance.status != ProductionOrder.Status.IN_PROGRESS: + raise ValidationError(f'工单{instance.number}{instance.get_status_display()}, 无法关闭工单') + + instance.status = ProductionOrder.Status.CLOSED + instance.save(update_fields=['status']) + + serializer = ProductionOrderSerializer(instance=instance) + return Response(data=serializer.data, status=status.HTTP_200_OK) + + +class ProductionRecordViewSet(BaseViewSet, ListModelMixin, RetrieveModelMixin, CreateModelMixin): + """生产记录""" + + serializer_class = ProductionRecordSerializer + permission_classes = [IsAuthenticated, ProductionRecordPermission] + filterset_class = ProductionRecordFilter + search_fields = ['production_order__number', 'goods__number', 'goods__name'] + ordering_fields = ['production_quantity', 'create_time'] + select_related_fields = ['production_order', 'goods', 'creator'] + queryset = ProductionRecord.objects.all() + + @transaction.atomic + def perform_create(self, serializer): + validated_data = serializer.validated_data + production_order = validated_data['production_order'] + production_quantity = validated_data['production_quantity'] + + if production_order.remain_quantity < production_quantity: + raise ValidationError('生产数量错误') + + production_order.remain_quantity = NP.minus(production_order.remain_quantity, production_quantity) + if production_order.remain_quantity == 0: + production_order.status = ProductionOrder.Status.COMPLETED + production_order.save(update_fields=['remain_quantity', 'status']) + + serializer.save() __all__ = [ - + 'ProductionOrderViewSet', 'ProductionRecordViewSet', ] diff --git a/scripts/init_permission.py b/scripts/init_permission.py index 0820971..a1ccf9f 100644 --- a/scripts/init_permission.py +++ b/scripts/init_permission.py @@ -54,6 +54,13 @@ PERMISSIONS = [ {'name': '销售退货', 'code': 'sales_return_order'}, ], }, + { + 'name': '生产管理', + 'permissions': [ + {'name': '生产计划', 'code': 'production_order'}, + {'name': '生产记录', 'code': 'production_record'}, + ], + }, { 'name': '库内管理', 'permissions': [