1
0
forked from erp-dev/erp

feat: added printing module

This commit is contained in:
2025-11-12 17:08:20 +08:00
parent 9898d71a7e
commit 808e5699f9
28 changed files with 1110 additions and 31 deletions

0
printing/__init__.py Normal file
View File

26
printing/admin.py Normal file
View File

@@ -0,0 +1,26 @@
from django.contrib import admin
from . import models
class PrintingJobInline(admin.StackedInline):
model = models.PrintingJob
extra = 1
@admin.register(models.PrintingOrder)
class PrintingOrderAdmin(admin.ModelAdmin):
list_display = (
'oid',
'customer_name',
'created_at',
'outgoing_date',
)
inlines = [PrintingJobInline]
@admin.display(description='客户名称')
def customer_name(self, obj: models.PrintingOrder):
return obj.customer.name
@admin.display(description='订单编号(虚拟)')
def oid(self, obj: models.PrintingOrder):
return obj.human_id

7
printing/apps.py Normal file
View File

@@ -0,0 +1,7 @@
from django.apps import AppConfig
class PrintingConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'printing'
verbose_name = '印染管理'

View File

@@ -0,0 +1,65 @@
# Generated by Django 5.2.7 on 2025-11-12 07:03
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('basic_info', '0009_product_minimum_quantity'),
]
operations = [
migrations.CreateModel(
name='PrintingOrder',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('fabric', models.CharField(max_length=100, verbose_name='面料')),
('width', models.CharField(max_length=100, verbose_name='幅宽')),
('is_urgent', models.BooleanField(default=False, verbose_name='紧急')),
('area', models.CharField(blank=True, max_length=100, null=True, verbose_name='地区')),
('address', models.CharField(blank=True, max_length=200, null=True, verbose_name='地址')),
('fabric_source', models.CharField(max_length=100, null=True, verbose_name='布料来源')),
('is_fabric_received', models.BooleanField(default=False, verbose_name='布料是否已收')),
('craft', models.CharField(max_length=100, null=True, verbose_name='工艺')),
('description', models.TextField(blank=True, null=True, verbose_name='订单描述')),
('outgoing_date', models.DateField(blank=True, null=True, verbose_name='出货日期')),
('curve', models.CharField(blank=True, max_length=100, null=True, verbose_name='曲线')),
('new_curve', models.CharField(blank=True, max_length=100, null=True, verbose_name='新加曲线')),
('position', models.TextField(blank=True, null=True, verbose_name='位置')),
('printing_warn', models.TextField(blank=True, null=True, verbose_name='打印注意事项')),
('rolling_warn', models.TextField(blank=True, null=True, verbose_name='滚筒注意事项')),
('production_warn', models.TextField(blank=True, null=True, verbose_name='生产注意事项')),
('customer', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='printing_orders', to='basic_info.customer', verbose_name='客户')),
],
options={
'verbose_name': '印染订单',
'verbose_name_plural': '印染订单',
},
),
migrations.CreateModel(
name='PrintingJob',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('quantity', models.PositiveIntegerField(verbose_name='数量')),
('unit', models.CharField(max_length=50, verbose_name='单位')),
('size', models.CharField(max_length=100, verbose_name='一段尺寸')),
('pieces', models.PositiveIntegerField(verbose_name='件数')),
('description', models.TextField(blank=True, verbose_name='备注')),
('status', models.CharField(max_length=50, verbose_name='状态')),
('product', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='printing_jobs', to='basic_info.product', verbose_name='产品')),
('printing_order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='printing_jobs', to='printing.printingorder', verbose_name='主订单')),
],
options={
'verbose_name': '印染款式明细',
'verbose_name_plural': '印染款式明细',
},
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.2.7 on 2025-11-12 07:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('printing', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='printingorder',
name='craft',
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='工艺'),
),
migrations.AlterField(
model_name='printingorder',
name='fabric_source',
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='布料来源'),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 5.2.7 on 2025-11-12 08:00
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('printing', '0002_alter_printingorder_craft_and_more'),
]
operations = [
migrations.RemoveField(
model_name='printingjob',
name='status',
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2025-11-12 08:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('printing', '0003_remove_printingjob_status'),
]
operations = [
migrations.AddField(
model_name='printingorder',
name='is_invalid',
field=models.BooleanField(default=False, verbose_name='是否作废'),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 5.2.7 on 2025-11-12 08:14
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('printing', '0004_printingorder_is_invalid'),
]
operations = [
migrations.AlterModelOptions(
name='printingorder',
options={'permissions': [('can_invalidate_printingorder', '可以作废印染订单'), ('can_activate_printingorder', '可以恢复印染订单')], 'verbose_name': '印染订单', 'verbose_name_plural': '印染订单'},
),
]

View File

73
printing/models.py Normal file
View File

@@ -0,0 +1,73 @@
from django.db import models
from flower.common import ModelBase
from basic_info import models as basic_models
class PrintingOrder(ModelBase):
"""PrintingOrder model representing a printing order."""
customer = models.ForeignKey(
basic_models.Customer,
on_delete=models.PROTECT,
related_name='printing_orders',
verbose_name='客户',
)
fabric = models.CharField(max_length=100, verbose_name='面料')
width = models.CharField(max_length=100, verbose_name='幅宽')
is_urgent = models.BooleanField(default=False, verbose_name='紧急')
area = models.CharField(max_length=100, null=True, blank=True, verbose_name='地区')
address = models.CharField(max_length=200, null=True, blank=True, verbose_name='地址')
fabric_source = models.CharField(max_length=100, null=True, blank=True, verbose_name='布料来源')
is_fabric_received = models.BooleanField(default=False, verbose_name='布料是否已收')
craft = models.CharField(max_length=100, null=True, blank=True, verbose_name='工艺')
description = models.TextField(blank=True, null=True, verbose_name='订单描述')
outgoing_date = models.DateField(null=True, blank=True, verbose_name='出货日期')
curve = models.CharField(max_length=100, blank=True, null=True, verbose_name='曲线')
new_curve = models.CharField(max_length=100, blank=True, null=True, verbose_name='新加曲线')
position = models.TextField(blank=True, null=True, verbose_name='位置')
printing_warn = models.TextField(blank=True, null=True, verbose_name='打印注意事项')
rolling_warn = models.TextField(blank=True, null=True, verbose_name='滚筒注意事项')
production_warn = models.TextField(blank=True, null=True, verbose_name='生产注意事项')
is_invalid = models.BooleanField(default=False, verbose_name='是否作废')
def __str__(self):
return self.human_id
class Meta:
verbose_name = '印染订单'
verbose_name_plural = '印染订单'
permissions = [
('can_invalidate_printingorder', '可以作废印染订单'),
('can_activate_printingorder', '可以恢复印染订单'),
]
@property
def human_id(self) -> str:
return f"{self.created_at.year}{self.created_at.month:02d}{self.created_at.day:02d}{self.id:06d}"
class PrintingJob(ModelBase):
"""PrintingJob model representing a printing job associated with an order."""
printing_order = models.ForeignKey(
PrintingOrder,
on_delete=models.CASCADE,
related_name='printing_jobs',
verbose_name='主订单',
)
product = models.ForeignKey(
basic_models.Product,
on_delete=models.PROTECT,
related_name='printing_jobs',
verbose_name='产品',
)
quantity = models.PositiveIntegerField(verbose_name='数量')
unit = models.CharField(max_length=50, verbose_name='单位')
size = models.CharField(max_length=100, verbose_name='一段尺寸')
pieces = models.PositiveIntegerField(verbose_name='件数')
description = models.TextField(blank=True, verbose_name='备注')
def __str__(self):
return self.printing_order.human_id
class Meta:
verbose_name = '印染款式明细'
verbose_name_plural = '印染款式明细'

59
printing/serializers.py Normal file
View File

@@ -0,0 +1,59 @@
"""
Printing 序列化器
"""
from rest_framework import serializers
from printing import models
class PrintingOrderListSerializer(serializers.ModelSerializer):
"""印染订单列表序列化器"""
customer_name = serializers.CharField(source='customer.name', read_only=True)
customer_phone = serializers.CharField(source='customer.phone', read_only=True)
class Meta:
model = models.PrintingOrder
fields = [
'id', 'human_id', 'customer', 'customer_name', 'customer_phone',
'fabric', 'width', 'is_urgent', 'area', 'address',
'is_fabric_received', 'outgoing_date', 'is_invalid',
'created_at', 'updated_at'
]
read_only_fields = ['id', 'human_id', 'created_at', 'updated_at']
class PrintingOrderDetailSerializer(serializers.ModelSerializer):
"""印染订单详情序列化器"""
customer_name = serializers.CharField(source='customer.name', read_only=True)
customer_phone = serializers.CharField(source='customer.phone', read_only=True)
customer_address = serializers.CharField(source='customer.address', read_only=True)
class Meta:
model = models.PrintingOrder
fields = [
'id', 'human_id', 'customer', 'customer_name', 'customer_phone', 'customer_address',
'fabric', 'width', 'is_urgent', 'area', 'address', 'fabric_source',
'is_fabric_received', 'craft', 'description', 'outgoing_date',
'curve', 'new_curve', 'position',
'printing_warn', 'rolling_warn', 'production_warn',
'is_invalid', 'created_at', 'updated_at'
]
read_only_fields = ['id', 'human_id', 'created_at', 'updated_at']
class PrintingOrderCreateUpdateSerializer(serializers.ModelSerializer):
"""印染订单创建/更新序列化器"""
class Meta:
model = models.PrintingOrder
fields = [
'customer', 'fabric', 'width', 'is_urgent', 'area', 'address',
'fabric_source', 'is_fabric_received', 'craft', 'description',
'outgoing_date', 'curve', 'new_curve', 'position',
'printing_warn', 'rolling_warn', 'production_warn', 'is_invalid'
]
def validate_customer(self, value):
"""验证客户是否存在"""
if not value:
raise serializers.ValidationError("客户不能为空")
return value

0
printing/services.py Normal file
View File

3
printing/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

227
printing/views.py Normal file
View File

@@ -0,0 +1,227 @@
"""
Printing API ViewSet
"""
from rest_framework import viewsets, filters, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import BasePermission
from rest_framework.pagination import LimitOffsetPagination
from rest_framework.permissions import DjangoModelPermissions
from django_filters.rest_framework import DjangoFilterBackend
from django_filters import rest_framework as django_filters
from printing import models
from basic_info.models import MerchantTypeEnum
from printing.serializers import (
PrintingOrderListSerializer,
PrintingOrderDetailSerializer,
PrintingOrderCreateUpdateSerializer,
)
class IsPrintingFactory(BasePermission):
"""自定义权限类,允许印染工厂用户访问"""
message = '您没有访问印染订单的权限'
def has_permission(self, request, view):
if not request.user.is_authenticated:
return False
if hasattr(request.user, 'employee'):
return request.user.employee.merchant.type == MerchantTypeEnum.FACTORY
return False
class HasInvalidatePrintingOrderPermission(BasePermission):
"""自定义权限类,检查用户是否有作废印染订单的权限"""
message = '您没有权限作废订单'
def has_permission(self, request, view):
if not request.user.is_authenticated:
return False
return request.user.has_perm('printing.can_invalidate_printingorder')
class HasActivatePrintingOrderPermission(BasePermission):
"""自定义权限类,检查用户是否有恢复印染订单的权限"""
message = '您没有权限恢复订单'
def has_permission(self, request, view):
if not request.user.is_authenticated:
return False
return request.user.has_perm('printing.can_activate_printingorder')
class PrintingOrderFilterSet(django_filters.FilterSet):
"""印染订单过滤器"""
customer_name = django_filters.CharFilter(field_name='customer__name', lookup_expr='icontains')
customer_phone = django_filters.CharFilter(field_name='customer__phone', lookup_expr='icontains')
fabric = django_filters.CharFilter(lookup_expr='icontains')
is_urgent = django_filters.BooleanFilter()
is_fabric_received = django_filters.BooleanFilter()
is_invalid = django_filters.BooleanFilter()
area = django_filters.CharFilter(lookup_expr='icontains')
outgoing_date_from = django_filters.DateFilter(field_name='outgoing_date', lookup_expr='gte')
outgoing_date_to = django_filters.DateFilter(field_name='outgoing_date', lookup_expr='lte')
created_date_from = django_filters.DateFilter(field_name='created_at', lookup_expr='gte')
created_date_to = django_filters.DateFilter(field_name='created_at', lookup_expr='lte')
class Meta:
model = models.PrintingOrder
fields = ['customer', 'is_urgent', 'is_fabric_received', 'is_invalid']
class PrintingOrderViewSet(viewsets.ModelViewSet):
"""
印染订单 ViewSet
提供印染订单的增改查功能(不支持删除)
list: 获取印染订单列表
retrieve: 获取印染订单详情
create: 创建印染订单
update: 更新印染订单
partial_update: 部分更新印染订单
查询参数:
- customer: 客户ID
- customer_name: 客户名称(模糊查询)
- customer_phone: 客户电话(模糊查询)
- fabric: 面料(模糊查询)
- is_urgent: 是否紧急true/false
- is_fabric_received: 布料是否已收true/false
- is_invalid: 是否作废true/false
- area: 地区(模糊查询)
- outgoing_date_from: 出货日期起始
- outgoing_date_to: 出货日期结束
- created_date_from: 创建日期起始
- created_date_to: 创建日期结束
- search: 全文搜索(客户名称、面料、地区、工艺)
- ordering: 排序字段
"""
queryset = models.PrintingOrder.objects.all()
permission_classes = [DjangoModelPermissions]
pagination_class = LimitOffsetPagination
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_class = PrintingOrderFilterSet
search_fields = ['customer__name', 'fabric', 'area', 'craft', 'description']
ordering_fields = [
'id', 'created_at', 'updated_at', 'outgoing_date',
'is_urgent', 'is_fabric_received', 'is_invalid'
]
ordering = ['-created_at']
def get_permissions(self):
permissions = super().get_permissions() + [IsPrintingFactory()]
if self.action == 'invalidate':
permissions.append(HasInvalidatePrintingOrderPermission())
elif self.action == 'activate':
permissions.append(HasActivatePrintingOrderPermission())
return permissions
def get_serializer_class(self):
"""根据动作选择序列化器"""
if self.action == 'list':
return PrintingOrderListSerializer
elif self.action in ['create', 'update', 'partial_update']:
return PrintingOrderCreateUpdateSerializer
else: # retrieve
return PrintingOrderDetailSerializer
def get_queryset(self):
"""优化查询"""
queryset = super().get_queryset()
if self.action in ['list', 'retrieve']:
queryset = queryset.select_related('customer')
return queryset
def destroy(self, request, *args, **kwargs):
"""禁用删除操作"""
return Response(
{'detail': '印染订单不支持删除操作,请使用作废功能'},
status=status.HTTP_405_METHOD_NOT_ALLOWED
)
@action(detail=True, methods=['post'])
def invalidate(self, request, pk=None):
"""
作废订单
需要权限: printing.can_invalidate_printingorder
"""
# 检查权限
if not request.user.has_perm('printing.can_invalidate_printingorder'):
return Response(
{'detail': '您没有权限作废订单'},
status=status.HTTP_403_FORBIDDEN
)
printing_order = self.get_object()
if printing_order.is_invalid:
return Response(
{'detail': '该订单已经作废'},
status=status.HTTP_400_BAD_REQUEST
)
printing_order.is_invalid = True
printing_order.save()
serializer = PrintingOrderDetailSerializer(printing_order)
return Response({
'detail': '订单已作废',
'data': serializer.data
})
@action(detail=True, methods=['post'])
def activate(self, request, pk=None):
"""
恢复订单
需要权限: printing.can_activate_printingorder
"""
# 检查权限
if not request.user.has_perm('printing.can_activate_printingorder'):
return Response(
{'detail': '您没有权限恢复订单'},
status=status.HTTP_403_FORBIDDEN
)
printing_order = self.get_object()
if not printing_order.is_invalid:
return Response(
{'detail': '该订单未作废,无需恢复'},
status=status.HTTP_400_BAD_REQUEST
)
printing_order.is_invalid = False
printing_order.save()
serializer = PrintingOrderDetailSerializer(printing_order)
return Response({
'detail': '订单已恢复',
'data': serializer.data
})
@action(detail=True, methods=['post'])
def mark_fabric_received(self, request, pk=None):
"""标记布料已收"""
printing_order = self.get_object()
if printing_order.is_fabric_received:
return Response(
{'detail': '布料已经标记为已收'},
status=status.HTTP_400_BAD_REQUEST
)
printing_order.is_fabric_received = True
printing_order.save()
serializer = PrintingOrderDetailSerializer(printing_order)
return Response({
'detail': '已标记布料已收',
'data': serializer.data
})