diff --git a/.env b/.env index 23e43b5..a857286 100644 --- a/.env +++ b/.env @@ -3,6 +3,6 @@ DEBUG=True ALLOWED_HOSTS=*, DB_HOST=localhost DB_PORT=5432 -DB_NAME=myapp +DB_NAME=flower DB_USER=postgres DB_PASSWORD=postgres \ No newline at end of file diff --git a/api_man/serializers.py b/api_man/serializers.py index fb7ff6c..ca86518 100644 --- a/api_man/serializers.py +++ b/api_man/serializers.py @@ -2,62 +2,105 @@ from rest_framework import serializers from basic_info import models as basic_models -class ProductCategorySerializer(serializers.ModelSerializer): +class BaseSerializer(serializers.ModelSerializer): + merchant = serializers.PrimaryKeyRelatedField( + queryset=basic_models.Merchant.objects.all(), + required=False # 创建时由视图层设置 + ) + + def to_representation(self, instance): + try: + merchant = self.context['request'].user.employee.merchant + if instance.merchant != merchant: + # 数据保护手段 + raise PermissionError("无权限访问该对象") + except (KeyError, AttributeError): + # TODO: log something? + raise PermissionError("无权限访问该对象") + + result = super().to_representation(instance) + if self.context.get('expand_merchant', False): + result['merchant'] = { + 'id': instance.merchant.id, + 'name': instance.merchant.name + } + return result + + +class QuickInputSerializer(serializers.ModelSerializer): + class Meta: + model = basic_models.QuickInput + fields = ['name', 'value', 'group'] + + +class ProductCategorySerializer(BaseSerializer): class Meta: model = basic_models.ProductCategory fields = '__all__' -class ProductSerializer(serializers.ModelSerializer): +class ProductSerializer(BaseSerializer): class Meta: model = basic_models.Product fields = '__all__' - depth = 1 # 展开外键关系 + + def to_representation(self, instance): + result = super().to_representation(instance) + result['category'] = { + 'id': instance.category.id, + 'name': instance.category.name + } + return result -class WareHouseSerializer(serializers.ModelSerializer): +class WareHouseSerializer(BaseSerializer): class Meta: model = basic_models.WareHouse fields = '__all__' -class SupplierSerializer(serializers.ModelSerializer): +class SupplierSerializer(BaseSerializer): class Meta: model = basic_models.Supplier fields = '__all__' -class EmployeeSerializer(serializers.ModelSerializer): +class EmployeeSerializer(BaseSerializer): class Meta: model = basic_models.Employee fields = '__all__' -class CustomerSerializer(serializers.ModelSerializer): +class CustomerSerializer(BaseSerializer): + created_by = serializers.PrimaryKeyRelatedField( + queryset=basic_models.Employee.objects.all(), + required=False, + ) + class Meta: model = basic_models.Customer fields = '__all__' -class VehicleTypeSerializer(serializers.ModelSerializer): +class VehicleTypeSerializer(BaseSerializer): class Meta: model = basic_models.VehicleType fields = '__all__' -class BankAccountSerializer(serializers.ModelSerializer): +class BankAccountSerializer(BaseSerializer): class Meta: model = basic_models.BankAccount fields = '__all__' -class DeviceInfoSerializer(serializers.ModelSerializer): +class DeviceInfoSerializer(BaseSerializer): class Meta: model = basic_models.DeviceInfo fields = '__all__' -class VehicleTransportRecordSerializer(serializers.ModelSerializer): +class VehicleTransportRecordSerializer(BaseSerializer): vehicle_type_name = serializers.CharField(source='vehicle_type.name', read_only=True) class Meta: diff --git a/api_man/urls.py b/api_man/urls.py index bc0b508..c3ffd9e 100644 --- a/api_man/urls.py +++ b/api_man/urls.py @@ -2,6 +2,8 @@ from django.urls import path, include from rest_framework import routers from . import views +quick_input_router = routers.DefaultRouter() +quick_input_router.register(prefix='', viewset=views.QuickInputViewSet) product_router = routers.DefaultRouter() product_router.register(prefix='', viewset=views.ProductViewSet) @@ -35,6 +37,7 @@ vehicle_transport_record_router.register(prefix='', viewset=views.VehicleTranspo urlpatterns = [ + path('quick-inputs/', include(quick_input_router.urls)), path('products/', include(product_router.urls)), path('warehouses/', include(warehouse_router.urls)), path('product-categories/', include(product_category_router.urls)), diff --git a/api_man/views.py b/api_man/views.py index 93e6a3d..5d487ad 100644 --- a/api_man/views.py +++ b/api_man/views.py @@ -1,9 +1,38 @@ from rest_framework import viewsets +from rest_framework.exceptions import PermissionDenied +from rest_framework.pagination import LimitOffsetPagination from . import serializers class BaseViewSet(viewsets.ModelViewSet): - permission_classes = [] # 允许所有访问 + pagination_class = LimitOffsetPagination + + def get_queryset(self): + qs = super().get_queryset() + # 如果是超级用户,返回所有数据 + # TODO: 测试结束后要取注释 + # if self.request.user.is_superuser: + # return qs + return qs.filter(merchant=self.request.user.employee.merchant) + + def perform_create(self, serializer): + try: + merchant = self.request.user.employee.merchant + serializer.save(merchant_id=merchant.id) + except AttributeError: + raise PermissionDenied("无权限创建该对象") + + +class QuickInputViewSet(viewsets.ModelViewSet): + queryset = serializers.basic_models.QuickInput.objects + serializer_class = serializers.QuickInputSerializer + + def filter_queryset(self, queryset): + qs = super().filter_queryset(queryset) + group = self.request.query_params.get('group') + if group: + qs = qs.filter(group=group) + return qs class ProductViewSet(BaseViewSet): @@ -35,6 +64,17 @@ class CustomerViewSet(BaseViewSet): queryset = serializers.basic_models.Customer.objects serializer_class = serializers.CustomerSerializer + def perform_create(self, serializer): + merchant = self.request.user.employee.merchant + employee = self.request.user.employee + serializer.save(merchant=merchant, created_by=employee) + + def get_queryset(self): + qs = super().get_queryset() + # 应用可见性过滤 + from basic_info.services import CustomerVisibilityService + return CustomerVisibilityService.filter_customers_for_employee(qs, self.request.user) + class VehicleTypeViewSet(BaseViewSet): queryset = serializers.basic_models.VehicleType.objects diff --git a/api_v1/serializers.py b/api_v1/serializers.py index 977836e..9d1f0c1 100644 --- a/api_v1/serializers.py +++ b/api_v1/serializers.py @@ -83,4 +83,11 @@ class CreateStockChangeResponseSerializer(serializers.Serializer): stock_change_record = StockChangeRecordResponseSerializer() details = StockChangeDetailResponseSerializer(many=True) message = serializers.CharField() - created_details_count = serializers.IntegerField() \ No newline at end of file + created_details_count = serializers.IntegerField() + + +class SetMerchantAutoCompleteStockChangeSerializer(serializers.Serializer): + """设置商户自动确认库存变动序列化器""" + auto_complete = serializers.BooleanField( + help_text="是否自动确认库存变动" + ) \ No newline at end of file diff --git a/api_v1/urls.py b/api_v1/urls.py index 25fde7a..709b0f9 100644 --- a/api_v1/urls.py +++ b/api_v1/urls.py @@ -4,4 +4,9 @@ from . import views urlpatterns = [ # 库存变动相关API path('stock-change/', views.create_full_stock_change, name='create_full_stock_change'), + path( + 'set-merchant-auto-complete-stock-change/', + views.set_merchant_auto_complete_stock_change, + name='set_merchant_auto_complete_stock_change', + ), ] \ No newline at end of file diff --git a/api_v1/views.py b/api_v1/views.py index bf1f3dc..ff42cec 100644 --- a/api_v1/views.py +++ b/api_v1/views.py @@ -1,9 +1,8 @@ from rest_framework import status from rest_framework.decorators import api_view, permission_classes -from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response +from basic_info.services import DataVisibilityService from django.db import transaction -from typing import List, Dict, Any import logging from stock import models as stock_models @@ -57,6 +56,20 @@ def create_full_stock_change(request): return Response({ 'error': '产品列表不能为空' }, status=status.HTTP_400_BAD_REQUEST) + + # 验证仓库是否本员工可见 + if not DataVisibilityService.is_warehouse_visible_to_employee(record_data['warehouse'], request.user): + return Response({ + 'error': f'仓库ID {record_data["warehouse"]} 对当前用户不可见' + }, status=status.HTTP_403_FORBIDDEN) + + # 验证产品是否本员工可见 + for p in products_data: + product_id = p.get('product') + if not DataVisibilityService.is_product_visible_to_employee(product_id, request.user): + return Response({ + 'error': f'产品ID {product_id} 对当前用户不可见' + }, status=status.HTTP_403_FORBIDDEN) # 验证产品数据 product_serializer = serializers.CreateStockChangeSerializer(data={ @@ -83,7 +96,8 @@ def create_full_stock_change(request): warehouse=warehouse, source_type=record_data['source_type'], source_id=record_data['source_id'], - # created_by=request.user + merchant=request.user.employee.merchant, + created_by=request.user ) logger.info(f"创建新库存变动记录 ID: {stock_change_record.id}") @@ -108,10 +122,18 @@ def create_full_stock_change(request): stock_change_record=stock_change_record, product=product, quantity=quantity, + merchant=request.user.employee.merchant, unit=product.unit ) created_details.append(detail) created_count += 1 + + + if warehouse.merchant.auto_complete_stock_change: + # 自动确认库存变动 + from stock import services as stock_services + stock_services.make_stock_change_completed(stock_change_record) + logger.info(f"自动确认库存变动记录 ID: {stock_change_record.id}") # 3. 构建响应 response_serializer = serializers.CreateStockChangeResponseSerializer({ @@ -134,3 +156,30 @@ def create_full_stock_change(request): 'error': '创建库存变动记录失败', 'message': str(e) }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +@extend_schema(tags=['修改商户自动确认出入库设定']) +@api_view(['POST']) +def set_merchant_auto_complete_stock_change(request): + """ + 设置商户自动确认库存变动设定 + POST /api/v1/merchant/set-auto-complete-stock-change/ + 请求参数: + { + "auto_complete_stock_change": true // 是否自动确认库存变动 + } + """ + try: + emp = request.user.employee + merchant = emp.merchant + serializer = serializers.SetMerchantAutoCompleteStockChangeSerializer(data=request.data) + if serializer.is_valid(): + auto_complete = serializer.validated_data['auto_complete'] + merchant.auto_complete_stock_change = auto_complete + merchant.save() + logger.info(f"用户 {request.user.username} 设置商户 {merchant.name} 自动确认库存变动为 {auto_complete}") + return Response({'status': 'success'}) + return Response({'error': serializer.errors}, status=status.HTTP_400_BAD_REQUEST) + except AttributeError: + logger.error(f"用户 {request.user.username} 无权限设置商户自动确认库存变动", exc_info=True) + return Response({'error': '无权限操作'}, status=status.HTTP_403_FORBIDDEN) diff --git a/basic_info/admin.py b/basic_info/admin.py index 5e6ff5d..193f124 100644 --- a/basic_info/admin.py +++ b/basic_info/admin.py @@ -1,21 +1,63 @@ from django.contrib import admin +from basic_info.services import CustomerVisibilityService from . import models +class AdminBase(admin.ModelAdmin): + """管理员基础类,定义通用配置""" + + def get_readonly_fields(self, request, obj = ...): + readonly_fields = list(super().get_readonly_fields(request, obj)) + if request.user.is_superuser: + readonly_fields.append('merchant') + return readonly_fields + + def get_list_display(self, request): + result = super().get_list_display(request) + result = list(result) + + if 'id' not in result: + result = ['id'] + result + + if request.user.is_superuser and 'merchant' not in result: + result = result + ['merchant'] + + return result + + def save_model(self, request, obj, form, change): + if not change: + if not obj.merchant_id: + obj.merchant_id = request.user.employee.merchant.id + return super().save_model(request, obj, form, change) + + +@admin.register(models.QuickInput) +class QuickInputAdmin(admin.ModelAdmin): + list_display = ('name', 'value', 'group') + list_filter = ('group',) + + +@admin.register(models.Merchant) +class MerchantAdmin(admin.ModelAdmin): + list_display = ('name', 'type', 'email', 'mobile', 'auto_complete_stock_change') + search_fields = ('name', 'mobile') + list_filter = ('type', 'auto_complete_stock_change') + + @admin.register(models.Supplier) -class SupplierAdmin(admin.ModelAdmin): +class SupplierAdmin(AdminBase): list_display = ('name', 'area', 'email', 'mobile') search_fields = ('name', 'area', 'email', 'mobile') @admin.register(models.ProductCategory) -class ProductCategoryAdmin(admin.ModelAdmin): +class ProductCategoryAdmin(AdminBase): list_display = ('name', 'product_prefix', 'description') search_fields = ('name',) @admin.register(models.Product) -class ProductAdmin(admin.ModelAdmin): +class ProductAdmin(AdminBase): list_display = ( 'name', 'category', @@ -31,29 +73,35 @@ class ProductAdmin(admin.ModelAdmin): @admin.register(models.BankAccount) -class BankAccountAdmin(admin.ModelAdmin): +class BankAccountAdmin(AdminBase): list_display = ('name', 'auto_number') search_fields = ('name', 'auto_number') @admin.register(models.WareHouse) -class WareHouseAdmin(admin.ModelAdmin): - list_display = ('name', 'location', 'area', 'mobile') - search_fields = ('name', 'area') +class WareHouseAdmin(AdminBase): + list_display = ('name', 'location', 'area', 'mobile', 'mode') + list_filter = ('area', 'mode') + search_fields = ('name', 'location', 'mobile') @admin.register(models.Customer) -class CustomerAdmin(admin.ModelAdmin): - list_display = ('name', 'area', 'email', 'mobile', 'total_debt') +class CustomerAdmin(AdminBase): + list_display = ('name', 'area', 'email', 'mobile', 'created_by', 'total_debt') search_fields = ('name', 'area', 'email', 'mobile') + def get_queryset(self, request): + qs = super().get_queryset(request) + qs = CustomerVisibilityService.filter_customers_for_employee(qs, request.user) + return qs + @admin.display(boolean=False, description='累欠金额') def total_debt(self, obj): return '0 (动态逻辑字段待实现)' @admin.register(models.Employee) -class EmployeeAdmin(admin.ModelAdmin): +class EmployeeAdmin(AdminBase): list_display = ('name', 'mobile', 'status', 'job_type', 'sys_user') search_fields = ('name', 'mobile') list_filter = ( @@ -63,20 +111,20 @@ class EmployeeAdmin(admin.ModelAdmin): @admin.register(models.DeviceInfo) -class DeviceInfoAdmin(admin.ModelAdmin): +class DeviceInfoAdmin(AdminBase): list_display = ('name', 'type', 'is_occupied', 'status') search_fields = ('name', 'start_working_at', 'stop_working_at') list_filter = ('type', 'status') @admin.register(models.VehicleType) -class VehicleTypeAdmin(admin.ModelAdmin): +class VehicleTypeAdmin(AdminBase): list_display = ('name', 'description') search_fields = ('name',) @admin.register(models.VehicleTransportRecord) -class VehicleTransportRecordAdmin(admin.ModelAdmin): +class VehicleTransportRecordAdmin(AdminBase): list_display = ('driver_name', 'contact_number', 'delivery_date') search_fields = ('driver_name', 'contact_number') list_filter = ('vehicle_type',) diff --git a/basic_info/migrations/0001_initial.py b/basic_info/migrations/0001_initial.py index c1ffb78..d98729f 100644 --- a/basic_info/migrations/0001_initial.py +++ b/basic_info/migrations/0001_initial.py @@ -1,6 +1,7 @@ -# Generated by Django 5.2.7 on 2025-11-03 06:06 +# Generated by Django 5.2.7 on 2025-11-06 05:33 import django.db.models.deletion +from django.conf import settings from django.db import migrations, models @@ -9,9 +10,88 @@ class Migration(migrations.Migration): initial = True dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ + migrations.CreateModel( + name='Merchant', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('id', models.BigAutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=100, verbose_name='商户名称')), + ('type', models.IntegerField(choices=[(1, '布行'), (2, '印花厂')], verbose_name='商户类型')), + ('area', models.CharField(blank=True, max_length=100, null=True, verbose_name='地区')), + ('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='电子邮箱')), + ('mobile', models.CharField(blank=True, max_length=20, null=True, verbose_name='手机')), + ('contact', models.TextField(blank=True, null=True, verbose_name='联系人')), + ('description', models.TextField(blank=True, null=True, verbose_name='备注描述')), + ], + options={ + 'verbose_name': '商户资料', + 'verbose_name_plural': '商户资料', + }, + ), + migrations.CreateModel( + name='Employee', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('id', models.BigAutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=100, verbose_name='员工姓名')), + ('job_type', models.CharField(choices=[('打纸', '打纸'), ('滚筒', '滚筒'), ('仓库', '仓库'), ('送货', '送货')], default='打纸', max_length=20, verbose_name='职位')), + ('mobile', models.CharField(blank=True, max_length=20, null=True, verbose_name='手机')), + ('area', models.CharField(blank=True, max_length=100, null=True, verbose_name='地区')), + ('description', models.TextField(blank=True, null=True, verbose_name='备注描述')), + ('status', models.CharField(choices=[('在职', '在职'), ('离职', '离职')], default='在职', max_length=20, verbose_name='员工状态')), + ('sys_user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='employee', to=settings.AUTH_USER_MODEL, verbose_name='系统用户')), + ('merchant', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='employees', to='basic_info.merchant', verbose_name='所属商户')), + ], + options={ + 'verbose_name': '员工资料', + 'verbose_name_plural': '员工资料', + }, + ), + migrations.CreateModel( + name='DeviceInfo', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('id', models.BigAutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=100, verbose_name='设备名称')), + ('type', models.CharField(choices=[('滚筒', '滚筒'), ('打印', '打印')], default='滚筒', max_length=20, verbose_name='设备类型')), + ('status', models.CharField(choices=[('使用中', '使用中'), ('停用', '停用'), ('维修中', '维修中')], default='使用中', max_length=20, verbose_name='设备状态')), + ('start_working_at', models.DateField(blank=True, null=True, verbose_name='开始服役日期')), + ('stop_working_at', models.DateField(blank=True, null=True, verbose_name='停止服役日期')), + ('is_occupied', models.BooleanField(default=False, verbose_name='是否被占用')), + ('description', models.TextField(blank=True, null=True, verbose_name='备注描述')), + ('merchant', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='basic_info.merchant', verbose_name='所属商户')), + ], + options={ + 'verbose_name': '设备资料', + 'verbose_name_plural': '设备资料', + }, + ), + migrations.CreateModel( + name='Customer', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('id', models.BigAutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=100, verbose_name='客户名称')), + ('mobile', models.CharField(max_length=20, verbose_name='手机')), + ('area', models.CharField(blank=True, max_length=100, null=True, verbose_name='地区')), + ('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='电子邮箱')), + ('contact', models.CharField(blank=True, null=True, verbose_name='联系人')), + ('description', models.TextField(blank=True, null=True, verbose_name='备注描述')), + ('merchant', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='customers', to='basic_info.merchant', verbose_name='所属商户')), + ], + options={ + 'verbose_name': '客户资料', + 'verbose_name_plural': '客户资料', + }, + ), migrations.CreateModel( name='BankAccount', fields=[ @@ -22,6 +102,7 @@ class Migration(migrations.Migration): ('auto_number', models.CharField(max_length=50, verbose_name='自动编号')), ('description', models.TextField(blank=True, null=True, verbose_name='备注描述')), ('attachment', models.FileField(blank=True, null=True, upload_to='bank_accounts/', verbose_name='附件')), + ('merchant', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='bank_accounts', to='basic_info.merchant', verbose_name='所属商户')), ], options={ 'verbose_name': '银行账户', @@ -37,6 +118,7 @@ class Migration(migrations.Migration): ('name', models.CharField(max_length=100, verbose_name='类别名称')), ('description', models.TextField(blank=True, null=True, verbose_name='类别描述')), ('product_prefix', models.CharField(max_length=5, verbose_name='产品编号前缀')), + ('merchant', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='product_categories', to='basic_info.merchant', verbose_name='所属商户')), ], options={ 'verbose_name': '产品类别', @@ -44,17 +126,28 @@ class Migration(migrations.Migration): }, ), migrations.CreateModel( - name='ProductUnit', + name='Product', fields=[ ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), ('id', models.BigAutoField(primary_key=True, serialize=False)), - ('name', models.CharField(max_length=20, verbose_name='单位名称')), + ('name', models.CharField(max_length=100, verbose_name='产品名')), + ('human_id', models.CharField(blank=True, max_length=100, verbose_name='产品编号')), + ('color', models.CharField(blank=True, max_length=50, null=True, verbose_name='颜色')), + ('width_size', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='宽幅')), + ('single_price_in', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='参考进价')), + ('single_price_out', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='参考售价')), + ('unit', models.IntegerField(choices=[(1, '米'), (2, '码'), (3, '公斤')], default=1, verbose_name='单位')), + ('spec', models.CharField(blank=True, max_length=100, null=True, verbose_name='规格')), ('description', models.TextField(blank=True, null=True, verbose_name='备注描述')), + ('image', models.ImageField(blank=True, null=True, upload_to='product_images/', verbose_name='产品图片')), + ('transform_rate', models.DecimalField(blank=True, decimal_places=4, max_digits=10, null=True, verbose_name='公斤转换率')), + ('merchant', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='products', to='basic_info.merchant', verbose_name='所属商户')), + ('category', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='products', to='basic_info.productcategory', verbose_name='产品类别')), ], options={ - 'verbose_name': '产品单位', - 'verbose_name_plural': '产品单位', + 'verbose_name': '产品资料', + 'verbose_name_plural': '产品资料', }, ), migrations.CreateModel( @@ -69,12 +162,46 @@ class Migration(migrations.Migration): ('mobile', models.CharField(blank=True, max_length=20, null=True, verbose_name='手机')), ('contact', models.TextField(blank=True, null=True, verbose_name='联系人')), ('description', models.TextField(blank=True, null=True, verbose_name='备注描述')), + ('merchant', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='suppliers', to='basic_info.merchant', verbose_name='所属商户')), ], options={ 'verbose_name': '供应商', 'verbose_name_plural': '供应商', }, ), + migrations.CreateModel( + name='VehicleType', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('id', models.BigAutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=50, verbose_name='车辆类型名称')), + ('description', models.TextField(blank=True, null=True, verbose_name='备注描述')), + ('merchant', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='vehicle_types', to='basic_info.merchant', verbose_name='所属商户')), + ], + options={ + 'verbose_name': '车辆类型', + 'verbose_name_plural': '车辆类型', + }, + ), + migrations.CreateModel( + name='VehicleTransportRecord', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('id', models.BigAutoField(primary_key=True, serialize=False)), + ('driver_name', models.CharField(max_length=100, verbose_name='司机姓名')), + ('contact_number', models.CharField(blank=True, max_length=20, null=True, verbose_name='联系电话')), + ('license_plate', models.CharField(blank=True, max_length=20, null=True, verbose_name='车牌号码')), + ('delivery_date', models.DateField(verbose_name='送货日期')), + ('merchant', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='vehicle_transport_records', to='basic_info.merchant', verbose_name='所属商户')), + ('vehicle_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='vehicles', to='basic_info.vehicletype', verbose_name='车次')), + ], + options={ + 'verbose_name': '司机车次', + 'verbose_name_plural': '司机车次', + }, + ), migrations.CreateModel( name='WareHouse', fields=[ @@ -87,47 +214,11 @@ class Migration(migrations.Migration): ('mobile', models.CharField(blank=True, max_length=20, null=True, verbose_name='手机')), ('area', models.CharField(blank=True, max_length=100, null=True, verbose_name='地区')), ('description', models.TextField(blank=True, null=True, verbose_name='备注描述')), + ('merchant', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='warehouses', to='basic_info.merchant', verbose_name='所属商户')), ], options={ 'verbose_name': '仓库资料', 'verbose_name_plural': '仓库资料', }, ), - migrations.CreateModel( - name='Product', - fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), - ('id', models.BigAutoField(primary_key=True, serialize=False)), - ('name', models.CharField(max_length=100, verbose_name='产品名')), - ('human_id', models.CharField(blank=True, max_length=100, verbose_name='产品编号')), - ('color', models.CharField(blank=True, max_length=50, null=True, verbose_name='颜色')), - ('width_size', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='宽幅')), - ('single_price', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='单价')), - ('spec', models.CharField(blank=True, max_length=100, null=True, verbose_name='规格')), - ('description', models.TextField(blank=True, null=True, verbose_name='备注描述')), - ('category', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='products', to='basic_info.productcategory', verbose_name='产品类别')), - ('unit', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='products', to='basic_info.productunit', verbose_name='单位')), - ], - options={ - 'verbose_name': '产品资料', - 'verbose_name_plural': '产品资料', - }, - ), - migrations.CreateModel( - name='PurchaseOrder', - fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), - ('id', models.BigAutoField(primary_key=True, serialize=False)), - ('order_date', models.DateField(verbose_name='订单日期')), - ('total_amount', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='总金额')), - ('remarks', models.TextField(blank=True, null=True, verbose_name='备注')), - ('supplier', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='purchase_orders', to='basic_info.supplier', verbose_name='供应商')), - ], - options={ - 'verbose_name': '采购单', - 'verbose_name_plural': '采购单', - }, - ), ] diff --git a/basic_info/migrations/0002_delete_purchaseorder.py b/basic_info/migrations/0002_delete_purchaseorder.py deleted file mode 100644 index dc90556..0000000 --- a/basic_info/migrations/0002_delete_purchaseorder.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-03 06:11 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('basic_info', '0001_initial'), - ] - - operations = [ - migrations.DeleteModel( - name='PurchaseOrder', - ), - ] diff --git a/stock/migrations/0010_prodictunit.py b/basic_info/migrations/0002_quickinput.py similarity index 54% rename from stock/migrations/0010_prodictunit.py rename to basic_info/migrations/0002_quickinput.py index b34c4ae..640c690 100644 --- a/stock/migrations/0010_prodictunit.py +++ b/basic_info/migrations/0002_quickinput.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.7 on 2025-11-03 05:28 +# Generated by Django 5.2.7 on 2025-11-06 07:48 from django.db import migrations, models @@ -6,22 +6,23 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('stock', '0009_alter_product_single_price'), + ('basic_info', '0001_initial'), ] operations = [ migrations.CreateModel( - name='ProdictUnit', + name='QuickInput', fields=[ ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), ('id', models.BigAutoField(primary_key=True, serialize=False)), - ('name', models.CharField(max_length=20, verbose_name='单位名称')), - ('description', models.TextField(blank=True, null=True, verbose_name='备注描述')), + ('name', models.CharField(max_length=100, verbose_name='名')), + ('value', models.CharField(max_length=200, verbose_name='值')), + ('group', models.CharField(verbose_name='组')), ], options={ - 'verbose_name': '产品单位', - 'verbose_name_plural': '产品单位', + 'verbose_name': '快捷输入', + 'verbose_name_plural': '快捷输入', }, ), ] diff --git a/basic_info/migrations/0003_alter_quickinput_unique_together.py b/basic_info/migrations/0003_alter_quickinput_unique_together.py new file mode 100644 index 0000000..9cc4f59 --- /dev/null +++ b/basic_info/migrations/0003_alter_quickinput_unique_together.py @@ -0,0 +1,17 @@ +# Generated by Django 5.2.7 on 2025-11-06 07:55 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('basic_info', '0002_quickinput'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='quickinput', + unique_together={('name', 'group')}, + ), + ] diff --git a/basic_info/migrations/0003_customer.py b/basic_info/migrations/0003_customer.py deleted file mode 100644 index db33b1c..0000000 --- a/basic_info/migrations/0003_customer.py +++ /dev/null @@ -1,31 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-03 06:14 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('basic_info', '0002_delete_purchaseorder'), - ] - - operations = [ - migrations.CreateModel( - name='Customer', - fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), - ('id', models.BigAutoField(primary_key=True, serialize=False)), - ('name', models.CharField(max_length=100, verbose_name='客户名称')), - ('area', models.CharField(blank=True, max_length=100, null=True, verbose_name='地区')), - ('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='电子邮箱')), - ('mobile', models.CharField(blank=True, max_length=20, null=True, verbose_name='手机')), - ('contact', models.TextField(blank=True, null=True, verbose_name='联系人')), - ('description', models.TextField(blank=True, null=True, verbose_name='备注描述')), - ], - options={ - 'verbose_name': '客户资料', - 'verbose_name_plural': '客户资料', - }, - ), - ] diff --git a/basic_info/migrations/0004_alter_customer_contact.py b/basic_info/migrations/0004_alter_customer_contact.py deleted file mode 100644 index 6114236..0000000 --- a/basic_info/migrations/0004_alter_customer_contact.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-03 06:15 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('basic_info', '0003_customer'), - ] - - operations = [ - migrations.AlterField( - model_name='customer', - name='contact', - field=models.CharField(blank=True, null=True, verbose_name='联系人'), - ), - ] diff --git a/basic_info/migrations/0004_customer_visible_employees.py b/basic_info/migrations/0004_customer_visible_employees.py new file mode 100644 index 0000000..fcbcf42 --- /dev/null +++ b/basic_info/migrations/0004_customer_visible_employees.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2025-11-06 09:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('basic_info', '0003_alter_quickinput_unique_together'), + ] + + operations = [ + migrations.AddField( + model_name='customer', + name='visible_employees', + field=models.ManyToManyField(blank=True, related_name='visible_customers', to='basic_info.employee', verbose_name='可见该客户的员工'), + ), + ] diff --git a/basic_info/migrations/0005_alter_customer_mobile.py b/basic_info/migrations/0005_alter_customer_mobile.py deleted file mode 100644 index f2ac2a3..0000000 --- a/basic_info/migrations/0005_alter_customer_mobile.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-03 06:16 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('basic_info', '0004_alter_customer_contact'), - ] - - operations = [ - migrations.AlterField( - model_name='customer', - name='mobile', - field=models.CharField(max_length=20, verbose_name='手机'), - ), - ] diff --git a/basic_info/migrations/0005_customer_created_by.py b/basic_info/migrations/0005_customer_created_by.py new file mode 100644 index 0000000..6cd3287 --- /dev/null +++ b/basic_info/migrations/0005_customer_created_by.py @@ -0,0 +1,19 @@ +# Generated by Django 5.2.7 on 2025-11-06 09:36 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('basic_info', '0004_customer_visible_employees'), + ] + + operations = [ + migrations.AddField( + model_name='customer', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='created_customers', to='basic_info.employee', verbose_name='创建者'), + ), + ] diff --git a/basic_info/migrations/0006_alter_product_merchant.py b/basic_info/migrations/0006_alter_product_merchant.py new file mode 100644 index 0000000..f920e18 --- /dev/null +++ b/basic_info/migrations/0006_alter_product_merchant.py @@ -0,0 +1,19 @@ +# Generated by Django 5.2.7 on 2025-11-07 06:03 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('basic_info', '0005_customer_created_by'), + ] + + operations = [ + migrations.AlterField( + model_name='product', + name='merchant', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='basic_info.merchant', verbose_name='所属商户'), + ), + ] diff --git a/basic_info/migrations/0006_employee.py b/basic_info/migrations/0006_employee.py deleted file mode 100644 index b6b6f1d..0000000 --- a/basic_info/migrations/0006_employee.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-03 06:24 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('basic_info', '0005_alter_customer_mobile'), - ] - - operations = [ - migrations.CreateModel( - name='Employee', - fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), - ('id', models.BigAutoField(primary_key=True, serialize=False)), - ('name', models.CharField(max_length=100, verbose_name='员工姓名')), - ('job_type', models.CharField(choices=[('打纸', '打纸'), ('滚筒', '滚筒'), ('仓库', '仓库'), ('送货', '送货')], default='打纸', max_length=20, verbose_name='职位')), - ('mobile', models.CharField(blank=True, max_length=20, null=True, verbose_name='手机')), - ('area', models.CharField(blank=True, max_length=100, null=True, verbose_name='地区')), - ('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='电子邮箱')), - ('contact', models.CharField(blank=True, null=True, verbose_name='联系人')), - ('description', models.TextField(blank=True, null=True, verbose_name='备注描述')), - ('status', models.CharField(choices=[('在职', '在职'), ('离职', '离职')], default='在职', max_length=20, verbose_name='员工状态')), - ], - options={ - 'verbose_name': '员工资料', - 'verbose_name_plural': '员工资料', - }, - ), - ] diff --git a/basic_info/migrations/0007_remove_employee_contact.py b/basic_info/migrations/0007_remove_employee_contact.py deleted file mode 100644 index ee38afb..0000000 --- a/basic_info/migrations/0007_remove_employee_contact.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-03 06:27 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('basic_info', '0006_employee'), - ] - - operations = [ - migrations.RemoveField( - model_name='employee', - name='contact', - ), - ] diff --git a/basic_info/migrations/0007_warehouse_mode.py b/basic_info/migrations/0007_warehouse_mode.py new file mode 100644 index 0000000..0788d90 --- /dev/null +++ b/basic_info/migrations/0007_warehouse_mode.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2025-11-07 08:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('basic_info', '0006_alter_product_merchant'), + ] + + operations = [ + migrations.AddField( + model_name='warehouse', + name='mode', + field=models.IntegerField(choices=[(1, '严进宽出'), (2, '严进严出'), (3, '宽进宽出')], default=1, verbose_name='出入库模式'), + ), + ] diff --git a/basic_info/migrations/0008_merchant_auto_complete_stock_change.py b/basic_info/migrations/0008_merchant_auto_complete_stock_change.py new file mode 100644 index 0000000..47748cc --- /dev/null +++ b/basic_info/migrations/0008_merchant_auto_complete_stock_change.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2025-11-07 08:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('basic_info', '0007_warehouse_mode'), + ] + + operations = [ + migrations.AddField( + model_name='merchant', + name='auto_complete_stock_change', + field=models.BooleanField(default=False, verbose_name='自动确认库存变动'), + ), + ] diff --git a/basic_info/migrations/0008_remove_employee_email.py b/basic_info/migrations/0008_remove_employee_email.py deleted file mode 100644 index 5a6262d..0000000 --- a/basic_info/migrations/0008_remove_employee_email.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-03 06:29 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('basic_info', '0007_remove_employee_contact'), - ] - - operations = [ - migrations.RemoveField( - model_name='employee', - name='email', - ), - ] diff --git a/basic_info/migrations/0009_employee_sys_user.py b/basic_info/migrations/0009_employee_sys_user.py deleted file mode 100644 index 3e36d16..0000000 --- a/basic_info/migrations/0009_employee_sys_user.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-03 07:05 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('basic_info', '0008_remove_employee_email'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.AddField( - model_name='employee', - name='sys_user', - field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='employee', to=settings.AUTH_USER_MODEL, verbose_name='系统用户'), - ), - ] diff --git a/basic_info/migrations/0010_deviceinfo.py b/basic_info/migrations/0010_deviceinfo.py deleted file mode 100644 index aee3e42..0000000 --- a/basic_info/migrations/0010_deviceinfo.py +++ /dev/null @@ -1,32 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-03 07:14 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('basic_info', '0009_employee_sys_user'), - ] - - operations = [ - migrations.CreateModel( - name='DeviceInfo', - fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), - ('id', models.BigAutoField(primary_key=True, serialize=False)), - ('name', models.CharField(max_length=100, verbose_name='设备名称')), - ('type', models.CharField(choices=[('滚筒', '滚筒'), ('打印', '打印')], default='滚筒', max_length=20, verbose_name='设备类型')), - ('status', models.CharField(choices=[('使用中', '使用中'), ('停用', '停用'), ('维修中', '维修中')], default='使用中', max_length=20, verbose_name='设备状态')), - ('start_working_at', models.DateField(blank=True, null=True, verbose_name='开始服役日期')), - ('stop_working_at', models.DateField(blank=True, null=True, verbose_name='停止服役日期')), - ('is_occupied', models.BooleanField(default=False, verbose_name='是否被占用')), - ('description', models.TextField(blank=True, null=True, verbose_name='备注描述')), - ], - options={ - 'verbose_name': '设备信息', - 'verbose_name_plural': '设备信息', - }, - ), - ] diff --git a/basic_info/migrations/0011_vehicletype_alter_deviceinfo_options_and_more.py b/basic_info/migrations/0011_vehicletype_alter_deviceinfo_options_and_more.py deleted file mode 100644 index 9108adc..0000000 --- a/basic_info/migrations/0011_vehicletype_alter_deviceinfo_options_and_more.py +++ /dev/null @@ -1,49 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-03 07:26 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('basic_info', '0010_deviceinfo'), - ] - - operations = [ - migrations.CreateModel( - name='VehicleType', - fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), - ('id', models.BigAutoField(primary_key=True, serialize=False)), - ('name', models.CharField(max_length=50, verbose_name='车辆类型名称')), - ('description', models.TextField(blank=True, null=True, verbose_name='备注描述')), - ], - options={ - 'verbose_name': '车辆类型', - 'verbose_name_plural': '车辆类型', - }, - ), - migrations.AlterModelOptions( - name='deviceinfo', - options={'verbose_name': '设备资料', 'verbose_name_plural': '设备资料'}, - ), - migrations.CreateModel( - name='VehicleTransportRecord', - fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), - ('id', models.BigAutoField(primary_key=True, serialize=False)), - ('driver_name', models.CharField(max_length=100, verbose_name='司机姓名')), - ('contact_number', models.CharField(blank=True, max_length=20, null=True, verbose_name='联系电话')), - ('license_plate', models.CharField(blank=True, max_length=20, null=True, verbose_name='车牌号码')), - ('delivery_date', models.DateField(verbose_name='送货日期')), - ('vehicle_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='vehicles', to='basic_info.vehicletype', verbose_name='车次')), - ], - options={ - 'verbose_name': '司机车次', - 'verbose_name_plural': '司机车次', - }, - ), - ] diff --git a/basic_info/migrations/0012_alter_product_unit.py b/basic_info/migrations/0012_alter_product_unit.py deleted file mode 100644 index 7e3bee7..0000000 --- a/basic_info/migrations/0012_alter_product_unit.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-03 08:58 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('basic_info', '0011_vehicletype_alter_deviceinfo_options_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='product', - name='unit', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='products', to='basic_info.productunit', verbose_name='主单位'), - ), - ] diff --git a/basic_info/migrations/0013_remove_product_unit_product_main_unit_and_more.py b/basic_info/migrations/0013_remove_product_unit_product_main_unit_and_more.py deleted file mode 100644 index 925d79b..0000000 --- a/basic_info/migrations/0013_remove_product_unit_product_main_unit_and_more.py +++ /dev/null @@ -1,36 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-03 09:10 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('basic_info', '0012_alter_product_unit'), - ('stock', '0018_remove_inventory_unit'), - ] - - operations = [ - migrations.RemoveField( - model_name='product', - name='unit', - ), - migrations.AddField( - model_name='product', - name='main_unit', - field=models.IntegerField(choices=[(1, '米'), (2, '码'), (3, '公斤')], default=1, verbose_name='主单位'), - ), - migrations.AddField( - model_name='product', - name='sub_unit', - field=models.IntegerField(blank=True, choices=[(1, '米'), (2, '码'), (3, '公斤')], null=True, verbose_name='辅单位'), - ), - migrations.AddField( - model_name='product', - name='transform_rate', - field=models.DecimalField(blank=True, decimal_places=4, max_digits=10, null=True, verbose_name='转换率(主单位->辅单位)'), - ), - migrations.DeleteModel( - name='ProductUnit', - ), - ] diff --git a/basic_info/migrations/0014_remove_product_main_unit_remove_product_sub_unit_and_more.py b/basic_info/migrations/0014_remove_product_main_unit_remove_product_sub_unit_and_more.py deleted file mode 100644 index 0119583..0000000 --- a/basic_info/migrations/0014_remove_product_main_unit_remove_product_sub_unit_and_more.py +++ /dev/null @@ -1,31 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-03 09:30 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('basic_info', '0013_remove_product_unit_product_main_unit_and_more'), - ] - - operations = [ - migrations.RemoveField( - model_name='product', - name='main_unit', - ), - migrations.RemoveField( - model_name='product', - name='sub_unit', - ), - migrations.AlterField( - model_name='product', - name='single_price', - field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='单价(米)'), - ), - migrations.AlterField( - model_name='product', - name='transform_rate', - field=models.DecimalField(blank=True, decimal_places=4, max_digits=10, null=True, verbose_name='公斤转换率'), - ), - ] diff --git a/basic_info/migrations/0015_remove_product_single_price_product_single_price_in_and_more.py b/basic_info/migrations/0015_remove_product_single_price_product_single_price_in_and_more.py deleted file mode 100644 index e5fca47..0000000 --- a/basic_info/migrations/0015_remove_product_single_price_product_single_price_in_and_more.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-03 09:47 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('basic_info', '0014_remove_product_main_unit_remove_product_sub_unit_and_more'), - ] - - operations = [ - migrations.RemoveField( - model_name='product', - name='single_price', - ), - migrations.AddField( - model_name='product', - name='single_price_in', - field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='参考进价'), - ), - migrations.AddField( - model_name='product', - name='single_price_out', - field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='参考售价'), - ), - ] diff --git a/basic_info/migrations/0016_product_unit.py b/basic_info/migrations/0016_product_unit.py deleted file mode 100644 index 1cdfef7..0000000 --- a/basic_info/migrations/0016_product_unit.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-04 02:12 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('basic_info', '0015_remove_product_single_price_product_single_price_in_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='product', - name='unit', - field=models.IntegerField(choices=[(1, '米'), (2, '码'), (3, '公斤')], default=1, verbose_name='单位'), - ), - ] diff --git a/basic_info/migrations/0017_product_image.py b/basic_info/migrations/0017_product_image.py deleted file mode 100644 index b269168..0000000 --- a/basic_info/migrations/0017_product_image.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-04 05:07 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('basic_info', '0016_product_unit'), - ] - - operations = [ - migrations.AddField( - model_name='product', - name='image', - field=models.ImageField(blank=True, null=True, upload_to='product_images/', verbose_name='产品图片'), - ), - ] diff --git a/basic_info/models.py b/basic_info/models.py index d738af5..ced54ae 100644 --- a/basic_info/models.py +++ b/basic_info/models.py @@ -12,11 +12,19 @@ YARD_TO_METER_RATE = 1.09361 # ==================== 枚举类定义 ==================== +class WareHouseModeEnum(models.IntegerChoices): + """仓库出入库模式枚举""" + RESTRICT_IN = 1, '严进宽出' + RESTRICT_IN_OUT = 2, '严进严出' + UNRESTRICTED = 3, '宽进宽出' + + class MerchantTypeEnum(models.IntegerChoices): """商户类型枚举""" STORE = 1, '布行' FACTORY = 2, '印花厂' + class ProductUnitEnum(models.IntegerChoices): """产品单位枚举""" METER = 1, '米' @@ -54,6 +62,21 @@ class DeviceTypeEnum(models.TextChoices): # ==================== 模型定义 ==================== +class QuickInput(ModelBase): + id = models.BigAutoField(primary_key=True) + name = models.CharField(max_length=100, verbose_name='名') + value = models.CharField(max_length=200, verbose_name='值') + group = models.CharField(verbose_name='组') + + def __str__(self): + return f'{self.group}-{self.name}' + + class Meta: + verbose_name = '快捷输入' + verbose_name_plural = '快捷输入' + unique_together = ('name', 'group') + + class Merchant(ModelBase): id = models.BigAutoField(primary_key=True) name = models.CharField(max_length=100, verbose_name='商户名称') @@ -62,6 +85,10 @@ class Merchant(ModelBase): email = models.EmailField(blank=True, null=True, verbose_name='电子邮箱') mobile = models.CharField(max_length=20, blank=True, null=True, verbose_name='手机') contact = models.TextField(blank=True, null=True, verbose_name='联系人') + auto_complete_stock_change = models.BooleanField( + default=False, + verbose_name='自动确认库存变动', + ) description = models.TextField(blank=True, null=True, verbose_name='备注描述') def __str__(self): @@ -74,6 +101,7 @@ class Merchant(ModelBase): class VehicleType(ModelBase): id = models.BigAutoField(primary_key=True) + merchant = models.ForeignKey('Merchant', on_delete=models.PROTECT, related_name='vehicle_types', verbose_name='所属商户') name = models.CharField(max_length=50, verbose_name='车辆类型名称') description = models.TextField(blank=True, null=True, verbose_name='备注描述') @@ -87,8 +115,14 @@ class VehicleType(ModelBase): class WareHouse(ModelBase): id = models.BigAutoField(primary_key=True) + merchant = models.ForeignKey('Merchant', on_delete=models.PROTECT, related_name='warehouses', verbose_name='所属商户') name = models.CharField(max_length=100, verbose_name='仓库名称') location = models.CharField(max_length=200, blank=True, null=True, verbose_name='仓库地址') + mode = models.IntegerField( + choices=WareHouseModeEnum.choices, + default=WareHouseModeEnum.RESTRICT_IN, + verbose_name='出入库模式', + ) contact = models.CharField(max_length=100, blank=True, null=True, verbose_name='联系人') mobile = models.CharField(max_length=20, blank=True, null=True, verbose_name='手机') area = models.CharField(max_length=100, blank=True, null=True, verbose_name='地区') @@ -104,6 +138,7 @@ class WareHouse(ModelBase): class Supplier(ModelBase): id = models.BigAutoField(primary_key=True) + merchant = models.ForeignKey('Merchant', on_delete=models.PROTECT, related_name='suppliers', verbose_name='所属商户') name = models.CharField(max_length=100, verbose_name='供应商名称') area = models.CharField(max_length=100, blank=True, null=True, verbose_name='地区') email = models.EmailField(blank=True, null=True, verbose_name='电子邮箱') @@ -121,6 +156,7 @@ class Supplier(ModelBase): class BankAccount(ModelBase): id = models.BigAutoField(primary_key=True) + merchant = models.ForeignKey('Merchant', on_delete=models.PROTECT, related_name='bank_accounts', verbose_name='所属商户') name = models.CharField(max_length=100, verbose_name='名称') auto_number = models.CharField(max_length=50, verbose_name='自动编号') description = models.TextField(blank=True, null=True, verbose_name='备注描述') @@ -136,6 +172,7 @@ class BankAccount(ModelBase): class ProductCategory(ModelBase): id = models.BigAutoField(primary_key=True) + merchant = models.ForeignKey('Merchant', on_delete=models.PROTECT, related_name='product_categories', verbose_name='所属商户') name = models.CharField(max_length=100, verbose_name='类别名称') description = models.TextField(blank=True, null=True, verbose_name='类别描述') product_prefix = models.CharField(max_length=5, verbose_name='产品编号前缀') @@ -150,6 +187,7 @@ class ProductCategory(ModelBase): class Product(ModelBase): id = models.BigAutoField(primary_key=True) + merchant = models.ForeignKey('Merchant', on_delete=models.PROTECT, verbose_name='所属商户') category = models.ForeignKey( ProductCategory, on_delete=models.PROTECT, @@ -231,7 +269,21 @@ class Product(ModelBase): class Customer(ModelBase): id = models.BigAutoField(primary_key=True) + merchant = models.ForeignKey('Merchant', on_delete=models.PROTECT, related_name='customers', verbose_name='所属商户') name = models.CharField(max_length=100, verbose_name='客户名称') + visible_employees = models.ManyToManyField( + 'Employee', + blank=True, + related_name='visible_customers', + verbose_name='可见该客户的员工', + ) + created_by = models.ForeignKey( + 'Employee', + on_delete=models.PROTECT, + related_name='created_customers', + null=True, + verbose_name='创建者', + ) mobile = models.CharField(max_length=20, verbose_name='手机') area = models.CharField(max_length=100, blank=True, null=True, verbose_name='地区') email = models.EmailField(blank=True, null=True, verbose_name='电子邮箱') @@ -248,6 +300,7 @@ class Customer(ModelBase): class Employee(ModelBase): id = models.BigAutoField(primary_key=True) + merchant = models.ForeignKey('Merchant', on_delete=models.PROTECT, related_name='employees', verbose_name='所属商户') sys_user = models.OneToOneField( User, on_delete=models.PROTECT, @@ -283,6 +336,7 @@ class Employee(ModelBase): class DeviceInfo(ModelBase): id = models.BigAutoField(primary_key=True) + merchant = models.ForeignKey('Merchant', on_delete=models.PROTECT, related_name='devices', verbose_name='所属商户') name = models.CharField(max_length=100, verbose_name='设备名称') type = models.CharField( max_length=20, @@ -311,6 +365,7 @@ class DeviceInfo(ModelBase): class VehicleTransportRecord(ModelBase): id = models.BigAutoField(primary_key=True) + merchant = models.ForeignKey('Merchant', on_delete=models.PROTECT, related_name='vehicle_transport_records', verbose_name='所属商户') vehicle_type = models.ForeignKey( VehicleType, on_delete=models.PROTECT, diff --git a/basic_info/services.py b/basic_info/services.py new file mode 100644 index 0000000..192c167 --- /dev/null +++ b/basic_info/services.py @@ -0,0 +1,75 @@ +from django.db.models import Q +from . import models as basic_models + + +class CustomerVisibilityService: + @staticmethod + def filter_customers_for_employee(customer_queryset, user): + """ + 过滤客户查询集,使其仅包含对指定员工可见的客户。 + """ + + if user.is_superuser: + return customer_queryset + + emp = getattr(user, 'employee', None) + if emp is None: + return customer_queryset.none() + + print(f'Filtering customers for employee: {emp.id}') + # 员工创建的客户可见 + 员工被设置为可见员工的客户可见 + visibility_filter = Q(created_by=emp) | Q(visible_employees=emp) + return customer_queryset.filter(visibility_filter).distinct() + + @staticmethod + def is_customer_visible_to_employee(customer, user): + """ + 检查特定客户是否对指定员工可见。 + """ + + if user.is_superuser: + return True + + emp = getattr(user, 'employee', None) + if emp is None: + return False + + if customer.created_by == emp: + return True + + if emp in customer.visible_employees.all(): + return True + + return False + + +class DataVisibilityService: + @staticmethod + def is_product_visible_to_employee(product_id: int, user): + """ + 检查特定产品是否对指定员工可见。 + """ + + if user.is_superuser: + return True + + emp: basic_models.Employee = getattr(user, 'employee', None) + if emp is None: + return False + + return basic_models.Product.objects.filter(id=product_id).filter(merchant=emp.merchant).exists() + + @staticmethod + def is_warehouse_visible_to_employee(warehouse_id: int, user): + """ + 检查特定仓库是否对指定员工可见。 + """ + + if user.is_superuser: + return True + + emp: basic_models.Employee = getattr(user, 'employee', None) + if emp is None: + return False + + return basic_models.WareHouse.objects.filter(id=warehouse_id).filter(merchant=emp.merchant).exists() diff --git a/docker-compose.yml b/docker-compose.yml index b708854..4fd03a0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres - POSTGRES_DB: myapp + POSTGRES_DB: flower ports: - "5432:5432" volumes: diff --git a/flower/settings.py b/flower/settings.py index cd0caca..9db43e4 100644 --- a/flower/settings.py +++ b/flower/settings.py @@ -49,6 +49,28 @@ DEBUG = env('DEBUG', default=False) ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=[]) +# CORS 配置 +CORS_ALLOW_ALL_ORIGINS = DEBUG # 开发环境允许所有源,生产环境需要配置白名单 +CORS_ALLOWED_ORIGINS = env.list('CORS_ALLOWED_ORIGINS', default=[ + 'http://localhost:3000', + 'http://localhost:5173', + 'http://127.0.0.1:3000', + 'http://127.0.0.1:5173', +]) +CORS_ALLOW_CREDENTIALS = True # 允许携带凭证(如 Cookie、认证头) +CORS_ALLOW_HEADERS = [ + 'accept', + 'accept-encoding', + 'authorization', + 'content-type', + 'dnt', + 'origin', + 'user-agent', + 'x-csrftoken', + 'x-requested-with', +] + + # Application definition INSTALLED_APPS = [ @@ -59,6 +81,7 @@ INSTALLED_APPS = [ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', # 使用七牛云存储,不需要staticfiles app + 'corsheaders', # CORS 支持 'rest_framework', 'drf_spectacular', 'basic_info', @@ -70,6 +93,7 @@ INSTALLED_APPS = [ MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', + 'corsheaders.middleware.CorsMiddleware', # CORS 中间件,必须放在 CommonMiddleware 之前 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', diff --git a/flower/urls.py b/flower/urls.py index 8a16407..122b368 100644 --- a/flower/urls.py +++ b/flower/urls.py @@ -17,16 +17,37 @@ Including another URLconf from django.contrib import admin from django.urls import path, include from stock.views import router +from rest_framework.response import Response +from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView +from sse.views import create_sse_event, push_sse_event from rest_framework_simplejwt.views import ( TokenObtainPairView, - TokenRefreshView, + # TokenRefreshView, ) -from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView + +class CustomTokenObtainPairView(TokenObtainPairView): + """自定义登录视图""" + + def post(self, request, *args, **kwargs): + try: + resp = super().post(request, *args, **kwargs) + if resp.status_code == 200: + srz = self.get_serializer(data=request.data) + srz.is_valid() + user = srz.user + + if not hasattr(user, 'employee'): + # 非员工用户,直接返回登录失败 + return Response({'detail': '无绑定的员工身份'}, status=401) + + return resp + except Exception as e: + return Response({'detail': '无法登录'}, status=400) + urlpatterns = [ - # JWT 登录 - path('api/auth/login/', TokenObtainPairView.as_view(), name='token_obtain_pair'), + path('api/auth/login/', CustomTokenObtainPairView.as_view(), name='token_obtain_pair'), # path('api/auth/refresh/', TokenRefreshView.as_view(), name='token_refresh'), # API 文档 @@ -37,4 +58,6 @@ urlpatterns = [ path('stock/', router.urls), path('api/v1/', include('api_v1.urls')), path('api/backend/', include('api_man.urls')), + path('sse/', create_sse_event, name='sse_event'), + path('sse/push/', push_sse_event, name='push_sse_event'), ] diff --git a/pyproject.toml b/pyproject.toml index e4c87f4..0d10bc7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ readme = "README.md" requires-python = ">=3.14" dependencies = [ "django>=5.2.7", - "django-cors>=0.1", + "django-cors-headers>=4.9.0", "django-environ>=0.12.0", "django-filter>=25.2", "django-ninja>=1.4.5", diff --git a/sse/__init__.py b/sse/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sse/admin.py b/sse/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/sse/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/sse/apps.py b/sse/apps.py new file mode 100644 index 0000000..1ddac02 --- /dev/null +++ b/sse/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class SseConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'sse' diff --git a/sse/migrations/__init__.py b/sse/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sse/models.py b/sse/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/sse/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/sse/tests.py b/sse/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/sse/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/sse/views.py b/sse/views.py new file mode 100644 index 0000000..f586e48 --- /dev/null +++ b/sse/views.py @@ -0,0 +1,45 @@ +from django.http.response import StreamingHttpResponse +from rest_framework.decorators import api_view, permission_classes +from rest_framework.response import Response +from contextlib import suppress +import asyncio + + +_connections = set() + + +@api_view(['POST']) +@permission_classes([]) +def create_sse_event(request): + """ + 创建一个简单的SSE事件流响应,用于测试和演示目的。 + """ + + def sse_stream(): + queue = asyncio.Queue() + _connections.add(queue) + + try: + while True: + data = queue.get() + yield f"data: {data}\n\n" + finally: + _connections.remove(queue) + + response = StreamingHttpResponse(sse_stream(), content_type='text/event-stream') + response['Cache-Control'] = 'no-cache' + return response + + +@api_view(['POST']) +@permission_classes([]) +def push_sse_event(request): + """ + 向所有连接的客户端广播一个SSE事件。 + 请求体应包含一个 'message' 字段,表示要发送的消息内容。 + """ + message = request.data.get('message', 'Hello, SSE!') + for q in list(_connections): + with suppress(asyncio.QueueFull): + q.put_nowait(message) + return Response({'status': 'message sent'}) diff --git a/stock/admin.py b/stock/admin.py index b920718..1c065ff 100644 --- a/stock/admin.py +++ b/stock/admin.py @@ -4,6 +4,14 @@ from django.utils.safestring import mark_safe from . import models, services +class StockAdminBase(admin.ModelAdmin): + """库存管理基础类,定义通用配置""" + + def get_list_filter(self, request): + result = super().get_list_filter(request) + result = list(result) + ['created_at'] + return result + @admin.register(models.PurchaseOrder) class PurchaseOrderAdmin(admin.ModelAdmin): list_display = ('id', 'supplier', 'order_date', 'total_amount') @@ -13,7 +21,7 @@ class PurchaseOrderAdmin(admin.ModelAdmin): @admin.register(models.Inventory) -class InventoryAdmin(admin.ModelAdmin): +class InventoryAdmin(StockAdminBase): list_display = ( 'id', 'product', @@ -22,12 +30,20 @@ class InventoryAdmin(admin.ModelAdmin): 'quantity', 'num_of_rolls', 'minimum_quantity', + 'frozen_quantity', 'updated_at', ) search_fields = ('product__name', 'warehouse__name') list_filter = ('warehouse',) ordering = ('-updated_at',) + @admin.display(description='冻结中数量') + def frozen_quantity(self, obj): + return services.get_frozen_stock_count( + product_id=obj.product_id, + warehouse_id=obj.warehouse_id + ) + @admin.display(description='颜色') def product_color(self, obj): return obj.product.color @@ -45,7 +61,7 @@ class StockSnapshotAdmin(admin.ModelAdmin): 'stock_change_record', 'created_at', ) - search_fields = ('product__name', 'warehouse__name', 'stock_change_record__id') + search_fields = ('product', 'warehouse', 'stock_change_record__id') list_filter = ('stock_change_record__type',) ordering = ('-created_at', ) @@ -73,12 +89,17 @@ class StockChangeDetailAdmin(admin.ModelAdmin): 'stock_change_record', 'product', 'quantity', + 'direction', 'unit', ) search_fields = ('stock_change_record__id', 'product__name') list_filter = ('stock_change_record', 'stock_change_record__type') ordering = ('-created_at',) + @admin.display(description='出入方向') + def direction(self, obj): + return '入库' if obj.stock_change_record.is_incoming else '出库' + @admin.register(models.StockChangeRecord) class StockChangeRecordAdmin(admin.ModelAdmin): @@ -131,4 +152,29 @@ class StockChangeRecordAdmin(admin.ModelAdmin): ] return mark_safe('
'.join(detail_list)) - actions = [make_selected_stock_changes_completed] \ No newline at end of file + actions = [make_selected_stock_changes_completed] + + +@admin.register(models.StockFreeze) +class StockFreezeAdmin(admin.ModelAdmin): + list_display = ( + 'id', + 'product', + 'warehouse', + 'quantity', + 'reason', + 'created_at', + ) + readonly_fields = ( + 'frozen_by', + 'created_at', + 'updated_at', + 'cancelled_at', + 'cancelled_by', + 'completed_at', + 'completed_by', + 'completed_with', + ) + search_fields = ('product__name', 'warehouse__name', 'reason') + list_filter = ('warehouse', 'created_at') + ordering = ('-created_at',) diff --git a/stock/migrations/0001_initial.py b/stock/migrations/0001_initial.py index 047eeca..f8cc5e8 100644 --- a/stock/migrations/0001_initial.py +++ b/stock/migrations/0001_initial.py @@ -1,6 +1,7 @@ -# Generated by Django 5.2.7 on 2025-11-03 04:08 +# Generated by Django 5.2.7 on 2025-11-06 05:33 import django.db.models.deletion +from django.conf import settings from django.db import migrations, models @@ -9,30 +10,111 @@ class Migration(migrations.Migration): initial = True dependencies = [ + ('basic_info', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( - name='ProductCategory', + name='PurchaseOrder', fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100, verbose_name='类别名称')), - ('description', models.TextField(blank=True, null=True, verbose_name='类别描述')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('id', models.BigAutoField(primary_key=True, serialize=False)), + ('order_date', models.DateField(verbose_name='订单日期')), + ('total_amount', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='总金额')), + ('remarks', models.TextField(blank=True, null=True, verbose_name='备注')), + ('merchant', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='purchase_orders', to='basic_info.merchant', verbose_name='所属商户')), + ('supplier', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='purchase_orders', to='basic_info.supplier', verbose_name='供应商')), ], + options={ + 'verbose_name': '采购单', + 'verbose_name_plural': '采购单', + }, ), migrations.CreateModel( - name='Product', + name='StockChangeRecord', fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100, verbose_name='产品名')), - ('human_id', models.CharField(max_length=100, verbose_name='产品编号')), - ('color', models.CharField(blank=True, max_length=50, null=True, verbose_name='颜色')), - ('width_size', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='宽幅')), - ('single_price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='单价')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('id', models.BigAutoField(primary_key=True, serialize=False)), + ('type', models.IntegerField(choices=[(1, '入库'), (2, '出库')], verbose_name='变动类型')), + ('source_type', models.IntegerField(choices=[(1, '采购'), (2, '销退'), (3, '调入'), (4, '盘盈'), (5, '销售'), (6, '采购退货'), (7, '调出'), (8, '盘亏')], verbose_name='变动来源')), + ('source_id', models.BigIntegerField(blank=True, null=True, verbose_name='来源单据ID')), + ('is_finished', models.BooleanField(default=False, verbose_name='完成出入库')), + ('finished_at', models.DateTimeField(blank=True, null=True, verbose_name='完成时间')), + ('remarks', models.TextField(blank=True, null=True, verbose_name='备注')), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='操作人')), + ('merchant', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='stock_change_records', to='basic_info.merchant', verbose_name='所属商户')), + ('warehouse', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='stock_change_records', to='basic_info.warehouse', verbose_name='仓库')), + ], + options={ + 'verbose_name': '出入库记录', + 'verbose_name_plural': '出入库记录', + }, + ), + migrations.CreateModel( + name='StockChangeDetail', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('id', models.BigAutoField(primary_key=True, serialize=False)), + ('quantity', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='数量')), + ('unit', models.IntegerField(choices=[(1, '米'), (2, '码'), (3, '公斤')], verbose_name='单位')), + ('merchant', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='stock_change_details', to='basic_info.merchant', verbose_name='所属商户')), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='stock_change_details', to='basic_info.product', verbose_name='产品')), + ('stock_change_record', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='details', to='stock.stockchangerecord', verbose_name='库存变动记录')), + ], + options={ + 'verbose_name': '库存变动明细', + 'verbose_name_plural': '库存变动明细', + }, + ), + migrations.CreateModel( + name='StockSnapshot', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('id', models.BigAutoField(primary_key=True, serialize=False)), + ('delta', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='库存变动数量')), + ('quantity_before', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='变动前库存数量')), + ('quantity_after', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='变动后库存数量')), + ('unit', models.IntegerField(choices=[(1, '米'), (2, '码'), (3, '公斤')], verbose_name='单位')), + ('num_of_rolls', models.IntegerField(default=1, verbose_name='匹数')), + ('offset_to', models.BigIntegerField(blank=True, null=True, verbose_name='冲抵目标ID')), + ('offset_at', models.DateTimeField(blank=True, null=True, verbose_name='冲抵时间')), + ('cancelled', models.BooleanField(default=False, verbose_name='已取消')), + ('cancelled_at', models.DateTimeField(blank=True, null=True, verbose_name='取消时间')), + ('offset_id', models.BigIntegerField(blank=True, null=True, verbose_name='冲抵记录ID')), + ('merchant', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='stock_snapshots', to='basic_info.merchant', verbose_name='所属商户')), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='stock_snapshots', to='basic_info.product', verbose_name='产品')), + ('stock_change_record', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='stock_snapshots', to='stock.stockchangerecord', verbose_name='关联的库存变动记录')), + ('warehouse', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='stock_snapshots', to='basic_info.warehouse', verbose_name='仓库')), + ], + options={ + 'verbose_name': '库存快照', + 'verbose_name_plural': '库存快照', + }, + ), + migrations.CreateModel( + name='Inventory', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('id', models.BigAutoField(primary_key=True, serialize=False)), + ('quantity', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='库存数量')), + ('num_of_rolls', models.IntegerField(default=1, verbose_name='匹数')), + ('minimum_quantity', models.IntegerField(blank=True, null=True, verbose_name='最低库存量')), ('spec', models.CharField(blank=True, max_length=100, null=True, verbose_name='规格')), ('description', models.TextField(blank=True, null=True, verbose_name='备注描述')), - ('unit', models.CharField(default='米', max_length=20, verbose_name='单位')), - ('category', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='products', to='stock.productcategory', verbose_name='产品类别')), + ('merchant', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='inventories', to='basic_info.merchant', verbose_name='所属商户')), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='inventories', to='basic_info.product', verbose_name='产品')), + ('warehouse', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='inventories', to='basic_info.warehouse', verbose_name='仓库')), ], + options={ + 'verbose_name': '库存', + 'verbose_name_plural': '库存', + 'unique_together': {('product', 'warehouse')}, + }, ), ] diff --git a/stock/migrations/0002_alter_product_options_alter_productcategory_options.py b/stock/migrations/0002_alter_product_options_alter_productcategory_options.py deleted file mode 100644 index 276b093..0000000 --- a/stock/migrations/0002_alter_product_options_alter_productcategory_options.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-03 04:10 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('stock', '0001_initial'), - ] - - operations = [ - migrations.AlterModelOptions( - name='product', - options={'verbose_name': '产品资料', 'verbose_name_plural': '产品资料'}, - ), - migrations.AlterModelOptions( - name='productcategory', - options={'verbose_name': '产品类别', 'verbose_name_plural': '产品类别'}, - ), - ] diff --git a/stock/migrations/0002_stockfreeze.py b/stock/migrations/0002_stockfreeze.py new file mode 100644 index 0000000..ab7b01a --- /dev/null +++ b/stock/migrations/0002_stockfreeze.py @@ -0,0 +1,44 @@ +# Generated by Django 5.2.7 on 2025-11-07 05:30 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('basic_info', '0005_customer_created_by'), + ('stock', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='StockFreeze', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('id', models.BigAutoField(primary_key=True, serialize=False)), + ('quantity', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='冻结数量')), + ('unit', models.IntegerField(choices=[(1, '米'), (2, '码'), (3, '公斤')], verbose_name='单位')), + ('status', models.IntegerField(choices=[(1, '冻结中'), (2, '已取消'), (3, '已成单')], default=1, verbose_name='冻结状态')), + ('frozen_with', models.BigIntegerField(blank=True, null=True, verbose_name='冻结关联ID')), + ('completed_at', models.DateTimeField(blank=True, null=True, verbose_name='成单时间')), + ('completed_with', models.BigIntegerField(blank=True, null=True, verbose_name='成单关联ID')), + ('cancelled_at', models.DateTimeField(blank=True, null=True, verbose_name='取消时间')), + ('reason', models.TextField(blank=True, null=True, verbose_name='冻结原因')), + ('cancelled_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='cancelled_stock_freezes', to=settings.AUTH_USER_MODEL, verbose_name='取消操作人')), + ('completed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='completed_stock_freezes', to=settings.AUTH_USER_MODEL, verbose_name='成单操作人')), + ('frozen_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='冻结操作人')), + ('merchant', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='stock_freezes', to='basic_info.merchant', verbose_name='所属商户')), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='stock_freezes', to='basic_info.product', verbose_name='产品')), + ('stock_detail', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='stock_freezes', to='stock.stockchangedetail', verbose_name='关联的库存变动明细')), + ('warehouse', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='stock_freezes', to='basic_info.warehouse', verbose_name='仓库')), + ], + options={ + 'verbose_name': '库存冻结', + 'verbose_name_plural': '库存冻结', + }, + ), + ] diff --git a/stock/migrations/0003_supplier.py b/stock/migrations/0003_supplier.py deleted file mode 100644 index 29f0df2..0000000 --- a/stock/migrations/0003_supplier.py +++ /dev/null @@ -1,29 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-03 04:20 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('stock', '0002_alter_product_options_alter_productcategory_options'), - ] - - operations = [ - migrations.CreateModel( - name='Supplier', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100, verbose_name='供应商名称')), - ('area', models.CharField(blank=True, max_length=100, null=True, verbose_name='地区')), - ('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='电子邮箱')), - ('mobile', models.CharField(blank=True, max_length=20, null=True, verbose_name='手机')), - ('contact', models.TextField(blank=True, null=True, verbose_name='联系人')), - ('description', models.TextField(blank=True, null=True, verbose_name='备注描述')), - ], - options={ - 'verbose_name': '供应商', - 'verbose_name_plural': '供应商', - }, - ), - ] diff --git a/stock/migrations/0004_alter_product_id_alter_productcategory_id_and_more.py b/stock/migrations/0004_alter_product_id_alter_productcategory_id_and_more.py deleted file mode 100644 index fe8843f..0000000 --- a/stock/migrations/0004_alter_product_id_alter_productcategory_id_and_more.py +++ /dev/null @@ -1,43 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-03 04:24 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('stock', '0003_supplier'), - ] - - operations = [ - migrations.AlterField( - model_name='product', - name='id', - field=models.BigAutoField(primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='productcategory', - name='id', - field=models.BigAutoField(primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='supplier', - name='id', - field=models.BigAutoField(primary_key=True, serialize=False), - ), - migrations.CreateModel( - name='PurchaseOrder', - fields=[ - ('id', models.BigAutoField(primary_key=True, serialize=False)), - ('order_date', models.DateField(verbose_name='订单日期')), - ('total_amount', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='总金额')), - ('remarks', models.TextField(blank=True, null=True, verbose_name='备注')), - ('supplier', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='purchase_orders', to='stock.supplier', verbose_name='供应商')), - ], - options={ - 'verbose_name': '采购单', - 'verbose_name_plural': '采购单', - }, - ), - ] diff --git a/stock/migrations/0005_product_created_at_product_updated_at_and_more.py b/stock/migrations/0005_product_created_at_product_updated_at_and_more.py deleted file mode 100644 index 11f8ccb..0000000 --- a/stock/migrations/0005_product_created_at_product_updated_at_and_more.py +++ /dev/null @@ -1,58 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-03 04:29 - -import django.utils.timezone -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('stock', '0004_alter_product_id_alter_productcategory_id_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='product', - name='created_at', - field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='创建时间'), - preserve_default=False, - ), - migrations.AddField( - model_name='product', - name='updated_at', - field=models.DateTimeField(auto_now=True, verbose_name='更新时间'), - ), - migrations.AddField( - model_name='productcategory', - name='created_at', - field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='创建时间'), - preserve_default=False, - ), - migrations.AddField( - model_name='productcategory', - name='updated_at', - field=models.DateTimeField(auto_now=True, verbose_name='更新时间'), - ), - migrations.AddField( - model_name='purchaseorder', - name='created_at', - field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='创建时间'), - preserve_default=False, - ), - migrations.AddField( - model_name='purchaseorder', - name='updated_at', - field=models.DateTimeField(auto_now=True, verbose_name='更新时间'), - ), - migrations.AddField( - model_name='supplier', - name='created_at', - field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='创建时间'), - preserve_default=False, - ), - migrations.AddField( - model_name='supplier', - name='updated_at', - field=models.DateTimeField(auto_now=True, verbose_name='更新时间'), - ), - ] diff --git a/stock/migrations/0006_bankaccount.py b/stock/migrations/0006_bankaccount.py deleted file mode 100644 index c2697f2..0000000 --- a/stock/migrations/0006_bankaccount.py +++ /dev/null @@ -1,29 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-03 04:32 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('stock', '0005_product_created_at_product_updated_at_and_more'), - ] - - operations = [ - migrations.CreateModel( - name='BankAccount', - fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), - ('id', models.BigAutoField(primary_key=True, serialize=False)), - ('name', models.CharField(max_length=100, verbose_name='名称')), - ('auto_number', models.CharField(max_length=50, verbose_name='自动编号')), - ('description', models.TextField(blank=True, null=True, verbose_name='备注描述')), - ('attachment', models.FileField(blank=True, null=True, upload_to='bank_accounts/', verbose_name='附件')), - ], - options={ - 'verbose_name': '银行账户', - 'verbose_name_plural': '银行账户', - }, - ), - ] diff --git a/stock/migrations/0007_productcategory_product_prefix.py b/stock/migrations/0007_productcategory_product_prefix.py deleted file mode 100644 index d63d927..0000000 --- a/stock/migrations/0007_productcategory_product_prefix.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-03 05:07 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('stock', '0006_bankaccount'), - ] - - operations = [ - migrations.AddField( - model_name='productcategory', - name='product_prefix', - field=models.CharField(blank=True, max_length=5, null=True, verbose_name='产品编号前缀'), - ), - ] diff --git a/stock/migrations/0008_alter_product_human_id_and_more.py b/stock/migrations/0008_alter_product_human_id_and_more.py deleted file mode 100644 index 709caf9..0000000 --- a/stock/migrations/0008_alter_product_human_id_and_more.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-03 05:13 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('stock', '0007_productcategory_product_prefix'), - ] - - operations = [ - migrations.AlterField( - model_name='product', - name='human_id', - field=models.CharField(blank=True, max_length=100, verbose_name='产品编号'), - ), - migrations.AlterField( - model_name='productcategory', - name='product_prefix', - field=models.CharField(max_length=5, verbose_name='产品编号前缀'), - ), - ] diff --git a/stock/migrations/0009_alter_product_single_price.py b/stock/migrations/0009_alter_product_single_price.py deleted file mode 100644 index 97e7bd3..0000000 --- a/stock/migrations/0009_alter_product_single_price.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-03 05:20 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('stock', '0008_alter_product_human_id_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='product', - name='single_price', - field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='单价'), - ), - ] diff --git a/stock/migrations/0011_alter_product_unit.py b/stock/migrations/0011_alter_product_unit.py deleted file mode 100644 index 2ea2926..0000000 --- a/stock/migrations/0011_alter_product_unit.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-03 05:28 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('stock', '0010_prodictunit'), - ] - - operations = [ - migrations.AlterField( - model_name='product', - name='unit', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='products', to='stock.prodictunit', verbose_name='单位'), - ), - ] diff --git a/stock/migrations/0012_rename_prodictunit_productunit.py b/stock/migrations/0012_rename_prodictunit_productunit.py deleted file mode 100644 index ed71a0c..0000000 --- a/stock/migrations/0012_rename_prodictunit_productunit.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-03 05:34 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('stock', '0011_alter_product_unit'), - ] - - operations = [ - migrations.RenameModel( - old_name='ProdictUnit', - new_name='ProductUnit', - ), - ] diff --git a/stock/migrations/0013_warehouse.py b/stock/migrations/0013_warehouse.py deleted file mode 100644 index 2adf9c9..0000000 --- a/stock/migrations/0013_warehouse.py +++ /dev/null @@ -1,31 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-03 05:47 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('stock', '0012_rename_prodictunit_productunit'), - ] - - operations = [ - migrations.CreateModel( - name='WareHouse', - fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), - ('id', models.BigAutoField(primary_key=True, serialize=False)), - ('name', models.CharField(max_length=100, verbose_name='仓库名称')), - ('location', models.CharField(blank=True, max_length=200, null=True, verbose_name='仓库地址')), - ('contact', models.CharField(blank=True, max_length=100, null=True, verbose_name='联系人')), - ('mobile', models.CharField(blank=True, max_length=20, null=True, verbose_name='手机')), - ('area', models.CharField(blank=True, max_length=100, null=True, verbose_name='地区')), - ('description', models.TextField(blank=True, null=True, verbose_name='备注描述')), - ], - options={ - 'verbose_name': '仓库资料', - 'verbose_name_plural': '仓库资料', - }, - ), - ] diff --git a/stock/migrations/0014_delete_bankaccount_remove_product_category_and_more.py b/stock/migrations/0014_delete_bankaccount_remove_product_category_and_more.py deleted file mode 100644 index 4cae9a6..0000000 --- a/stock/migrations/0014_delete_bankaccount_remove_product_category_and_more.py +++ /dev/null @@ -1,46 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-03 06:06 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('stock', '0013_warehouse'), - ] - - operations = [ - migrations.DeleteModel( - name='BankAccount', - ), - migrations.RemoveField( - model_name='product', - name='category', - ), - migrations.RemoveField( - model_name='product', - name='unit', - ), - migrations.RemoveField( - model_name='purchaseorder', - name='supplier', - ), - migrations.DeleteModel( - name='WareHouse', - ), - migrations.DeleteModel( - name='ProductCategory', - ), - migrations.DeleteModel( - name='Product', - ), - migrations.DeleteModel( - name='ProductUnit', - ), - migrations.DeleteModel( - name='PurchaseOrder', - ), - migrations.DeleteModel( - name='Supplier', - ), - ] diff --git a/stock/migrations/0015_initial.py b/stock/migrations/0015_initial.py deleted file mode 100644 index c88e033..0000000 --- a/stock/migrations/0015_initial.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-03 06:11 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('basic_info', '0002_delete_purchaseorder'), - ('stock', '0014_delete_bankaccount_remove_product_category_and_more'), - ] - - operations = [ - migrations.CreateModel( - name='PurchaseOrder', - fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), - ('id', models.BigAutoField(primary_key=True, serialize=False)), - ('order_date', models.DateField(verbose_name='订单日期')), - ('total_amount', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='总金额')), - ('remarks', models.TextField(blank=True, null=True, verbose_name='备注')), - ('supplier', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='purchase_orders', to='basic_info.supplier', verbose_name='供应商')), - ], - options={ - 'verbose_name': '采购单', - 'verbose_name_plural': '采购单', - }, - ), - ] diff --git a/stock/migrations/0016_inventory.py b/stock/migrations/0016_inventory.py deleted file mode 100644 index 8e9fe97..0000000 --- a/stock/migrations/0016_inventory.py +++ /dev/null @@ -1,34 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-03 07:47 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('basic_info', '0011_vehicletype_alter_deviceinfo_options_and_more'), - ('stock', '0015_initial'), - ] - - operations = [ - migrations.CreateModel( - name='Inventory', - fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), - ('id', models.BigAutoField(primary_key=True, serialize=False)), - ('quantity', models.IntegerField(verbose_name='数量')), - ('minimum_quantity', models.IntegerField(blank=True, null=True, verbose_name='最低库存量')), - ('spec', models.CharField(blank=True, max_length=100, null=True, verbose_name='规格')), - ('description', models.TextField(blank=True, null=True, verbose_name='备注描述')), - ('product', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='inventories', to='basic_info.product', verbose_name='产品')), - ('unit', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='inventories', to='basic_info.productunit', verbose_name='单位')), - ('warehouse', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='inventories', to='basic_info.warehouse', verbose_name='仓库')), - ], - options={ - 'verbose_name': '库存', - 'verbose_name_plural': '库存', - }, - ), - ] diff --git a/stock/migrations/0017_inventory_num_of_rolls_alter_inventory_quantity.py b/stock/migrations/0017_inventory_num_of_rolls_alter_inventory_quantity.py deleted file mode 100644 index 8809434..0000000 --- a/stock/migrations/0017_inventory_num_of_rolls_alter_inventory_quantity.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-03 08:01 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('stock', '0016_inventory'), - ] - - operations = [ - migrations.AddField( - model_name='inventory', - name='num_of_rolls', - field=models.IntegerField(blank=True, null=True, verbose_name='匹数'), - ), - migrations.AlterField( - model_name='inventory', - name='quantity', - field=models.IntegerField(verbose_name='库存数量'), - ), - ] diff --git a/stock/migrations/0018_remove_inventory_unit.py b/stock/migrations/0018_remove_inventory_unit.py deleted file mode 100644 index ff5a3e1..0000000 --- a/stock/migrations/0018_remove_inventory_unit.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-03 09:10 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('stock', '0017_inventory_num_of_rolls_alter_inventory_quantity'), - ] - - operations = [ - migrations.RemoveField( - model_name='inventory', - name='unit', - ), - ] diff --git a/stock/migrations/0019_alter_inventory_unique_together.py b/stock/migrations/0019_alter_inventory_unique_together.py deleted file mode 100644 index 16d7d70..0000000 --- a/stock/migrations/0019_alter_inventory_unique_together.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-04 05:18 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('basic_info', '0017_product_image'), - ('stock', '0018_remove_inventory_unit'), - ] - - operations = [ - migrations.AlterUniqueTogether( - name='inventory', - unique_together={('product', 'warehouse')}, - ), - ] diff --git a/stock/migrations/0020_stockchangerecord.py b/stock/migrations/0020_stockchangerecord.py deleted file mode 100644 index 5387842..0000000 --- a/stock/migrations/0020_stockchangerecord.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-04 05:51 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('stock', '0019_alter_inventory_unique_together'), - ] - - operations = [ - migrations.CreateModel( - name='StockChangeRecord', - fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), - ('id', models.BigAutoField(primary_key=True, serialize=False)), - ('type', models.IntegerField(choices=[(1, '入库'), (2, '出库')], verbose_name='变动类型')), - ('source_type', models.IntegerField(choices=[(1, '采购'), (2, '销退'), (3, '调入'), (4, '盘盈'), (5, '销售'), (6, '采购退货'), (7, '调出'), (8, '盘亏')], verbose_name='变动来源')), - ('remarks', models.TextField(blank=True, null=True, verbose_name='备注')), - ], - options={ - 'verbose_name': '出入库记录', - 'verbose_name_plural': '出入库记录', - }, - ), - ] diff --git a/stock/migrations/0021_stockchangerecord_is_finished_and_more.py b/stock/migrations/0021_stockchangerecord_is_finished_and_more.py deleted file mode 100644 index f582a30..0000000 --- a/stock/migrations/0021_stockchangerecord_is_finished_and_more.py +++ /dev/null @@ -1,37 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-04 05:57 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('basic_info', '0017_product_image'), - ('stock', '0020_stockchangerecord'), - ] - - operations = [ - migrations.AddField( - model_name='stockchangerecord', - name='is_finished', - field=models.BooleanField(default=False, verbose_name='完成出入库'), - ), - migrations.AddField( - model_name='stockchangerecord', - name='product', - field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.PROTECT, related_name='stock_change_records', to='basic_info.product', verbose_name='产品'), - preserve_default=False, - ), - migrations.AddField( - model_name='stockchangerecord', - name='source_id', - field=models.BigIntegerField(blank=True, null=True, verbose_name='来源单据ID'), - ), - migrations.AddField( - model_name='stockchangerecord', - name='warehouse', - field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.PROTECT, related_name='stock_change_records', to='basic_info.warehouse', verbose_name='仓库'), - preserve_default=False, - ), - ] diff --git a/stock/migrations/0022_stockchangerecord_quantity_stockchangerecord_unit.py b/stock/migrations/0022_stockchangerecord_quantity_stockchangerecord_unit.py deleted file mode 100644 index 8fb9521..0000000 --- a/stock/migrations/0022_stockchangerecord_quantity_stockchangerecord_unit.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-04 06:05 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('stock', '0021_stockchangerecord_is_finished_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='stockchangerecord', - name='quantity', - field=models.IntegerField(default=1, verbose_name='数量'), - preserve_default=False, - ), - migrations.AddField( - model_name='stockchangerecord', - name='unit', - field=models.IntegerField(choices=[(1, '米'), (2, '码'), (3, '公斤')], default=1, verbose_name='单位'), - preserve_default=False, - ), - ] diff --git a/stock/migrations/0023_stockchangerecord_num_of_rolls.py b/stock/migrations/0023_stockchangerecord_num_of_rolls.py deleted file mode 100644 index 1dde35d..0000000 --- a/stock/migrations/0023_stockchangerecord_num_of_rolls.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-04 06:21 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('stock', '0022_stockchangerecord_quantity_stockchangerecord_unit'), - ] - - operations = [ - migrations.AddField( - model_name='stockchangerecord', - name='num_of_rolls', - field=models.IntegerField(blank=True, null=True, verbose_name='匹数'), - ), - ] diff --git a/stock/migrations/0024_stockchangerecord_created_by.py b/stock/migrations/0024_stockchangerecord_created_by.py deleted file mode 100644 index f93ce40..0000000 --- a/stock/migrations/0024_stockchangerecord_created_by.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-04 06:35 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('stock', '0023_stockchangerecord_num_of_rolls'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.AddField( - model_name='stockchangerecord', - name='created_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='创建人'), - ), - ] diff --git a/stock/migrations/0025_alter_stockchangerecord_created_by.py b/stock/migrations/0025_alter_stockchangerecord_created_by.py deleted file mode 100644 index 7fe182e..0000000 --- a/stock/migrations/0025_alter_stockchangerecord_created_by.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-04 06:35 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('stock', '0024_stockchangerecord_created_by'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.AlterField( - model_name='stockchangerecord', - name='created_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='操作人'), - ), - ] diff --git a/stock/migrations/0026_stockchangerecord_finished_at.py b/stock/migrations/0026_stockchangerecord_finished_at.py deleted file mode 100644 index fe88503..0000000 --- a/stock/migrations/0026_stockchangerecord_finished_at.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-04 06:44 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('stock', '0025_alter_stockchangerecord_created_by'), - ] - - operations = [ - migrations.AddField( - model_name='stockchangerecord', - name='finished_at', - field=models.DateTimeField(blank=True, null=True, verbose_name='完成时间'), - ), - ] diff --git a/stock/migrations/0027_alter_inventory_num_of_rolls_and_more.py b/stock/migrations/0027_alter_inventory_num_of_rolls_and_more.py deleted file mode 100644 index c2397ae..0000000 --- a/stock/migrations/0027_alter_inventory_num_of_rolls_and_more.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-04 07:09 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('stock', '0026_stockchangerecord_finished_at'), - ] - - operations = [ - migrations.AlterField( - model_name='inventory', - name='num_of_rolls', - field=models.IntegerField(default=1, verbose_name='匹数'), - ), - migrations.AlterField( - model_name='stockchangerecord', - name='num_of_rolls', - field=models.IntegerField(default=1, verbose_name='匹数'), - ), - ] diff --git a/stock/migrations/0028_stocksnapshot.py b/stock/migrations/0028_stocksnapshot.py deleted file mode 100644 index adcb732..0000000 --- a/stock/migrations/0028_stocksnapshot.py +++ /dev/null @@ -1,40 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-04 07:53 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('basic_info', '0017_product_image'), - ('stock', '0027_alter_inventory_num_of_rolls_and_more'), - ] - - operations = [ - migrations.CreateModel( - name='StockSnapshot', - fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), - ('id', models.BigAutoField(primary_key=True, serialize=False)), - ('delta', models.IntegerField(verbose_name='库存数量')), - ('quantity_before', models.IntegerField(verbose_name='变动前库存数量')), - ('quantity_after', models.IntegerField(verbose_name='变动后库存数量')), - ('unit', models.IntegerField(choices=[(1, '米'), (2, '码'), (3, '公斤')], verbose_name='单位')), - ('num_of_rolls', models.IntegerField(default=1, verbose_name='匹数')), - ('offset_to', models.BigIntegerField(blank=True, null=True, verbose_name='冲抵目标ID')), - ('offset_at', models.DateTimeField(blank=True, null=True, verbose_name='冲抵时间')), - ('cancelled', models.BooleanField(default=False, verbose_name='已取消')), - ('cancelled_at', models.DateTimeField(blank=True, null=True, verbose_name='取消时间')), - ('offset_id', models.BigIntegerField(blank=True, null=True, verbose_name='冲抵记录ID')), - ('product', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='stock_snapshots', to='basic_info.product', verbose_name='产品')), - ('stock_change_record', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='stock_snapshots', to='stock.stockchangerecord', verbose_name='关联的库存变动记录')), - ('warehouse', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='stock_snapshots', to='basic_info.warehouse', verbose_name='仓库')), - ], - options={ - 'verbose_name': '库存快照', - 'verbose_name_plural': '库存快照', - }, - ), - ] diff --git a/stock/migrations/0029_alter_inventory_quantity_and_more.py b/stock/migrations/0029_alter_inventory_quantity_and_more.py deleted file mode 100644 index bf9f54f..0000000 --- a/stock/migrations/0029_alter_inventory_quantity_and_more.py +++ /dev/null @@ -1,38 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-04 08:53 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('stock', '0028_stocksnapshot'), - ] - - operations = [ - migrations.AlterField( - model_name='inventory', - name='quantity', - field=models.DecimalField(decimal_places=2, max_digits=10, verbose_name='库存数量'), - ), - migrations.AlterField( - model_name='stockchangerecord', - name='quantity', - field=models.DecimalField(decimal_places=2, max_digits=10, verbose_name='数量'), - ), - migrations.AlterField( - model_name='stocksnapshot', - name='delta', - field=models.DecimalField(decimal_places=2, max_digits=10, verbose_name='库存数量'), - ), - migrations.AlterField( - model_name='stocksnapshot', - name='quantity_after', - field=models.DecimalField(decimal_places=2, max_digits=10, verbose_name='变动后库存数量'), - ), - migrations.AlterField( - model_name='stocksnapshot', - name='quantity_before', - field=models.DecimalField(decimal_places=2, max_digits=10, verbose_name='变动前库存数量'), - ), - ] diff --git a/stock/migrations/0030_alter_stocksnapshot_delta.py b/stock/migrations/0030_alter_stocksnapshot_delta.py deleted file mode 100644 index fd01fe5..0000000 --- a/stock/migrations/0030_alter_stocksnapshot_delta.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-04 09:50 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('stock', '0029_alter_inventory_quantity_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='stocksnapshot', - name='delta', - field=models.DecimalField(decimal_places=2, max_digits=10, verbose_name='库存变动数量'), - ), - ] diff --git a/stock/migrations/0031_remove_stockchangerecord_num_of_rolls_and_more.py b/stock/migrations/0031_remove_stockchangerecord_num_of_rolls_and_more.py deleted file mode 100644 index 20f4a87..0000000 --- a/stock/migrations/0031_remove_stockchangerecord_num_of_rolls_and_more.py +++ /dev/null @@ -1,43 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-05 05:34 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('basic_info', '0017_product_image'), - ('stock', '0030_alter_stocksnapshot_delta'), - ] - - operations = [ - migrations.RemoveField( - model_name='stockchangerecord', - name='num_of_rolls', - ), - migrations.RemoveField( - model_name='stockchangerecord', - name='product', - ), - migrations.RemoveField( - model_name='stockchangerecord', - name='quantity', - ), - migrations.CreateModel( - name='StockChangeDetail', - fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), - ('id', models.BigAutoField(primary_key=True, serialize=False)), - ('quantity', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='数量')), - ('unit', models.IntegerField(choices=[(1, '米'), (2, '码'), (3, '公斤')], verbose_name='单位')), - ('product', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='stock_change_details', to='basic_info.product', verbose_name='产品')), - ('stock_change_record', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='details', to='stock.stockchangerecord', verbose_name='库存变动记录')), - ], - options={ - 'verbose_name': '库存变动明细', - 'verbose_name_plural': '库存变动明细', - }, - ), - ] diff --git a/stock/migrations/0032_remove_stockchangerecord_unit.py b/stock/migrations/0032_remove_stockchangerecord_unit.py deleted file mode 100644 index d5b00d1..0000000 --- a/stock/migrations/0032_remove_stockchangerecord_unit.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-05 06:50 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('stock', '0031_remove_stockchangerecord_num_of_rolls_and_more'), - ] - - operations = [ - migrations.RemoveField( - model_name='stockchangerecord', - name='unit', - ), - ] diff --git a/stock/models.py b/stock/models.py index 36b437c..7483c80 100644 --- a/stock/models.py +++ b/stock/models.py @@ -8,6 +8,13 @@ import decimal # ==================== 枚举类定义 ==================== +class StockFreezeStatusEnum(models.IntegerChoices): + """库存冻结状态类型""" + + FROZEN = 1, '冻结中' + CANCELLED = 2, '已取消' + COMPLETED = 3, '已成单' + class StockChangeSourceEnum(models.IntegerChoices): """库存变动来源类型""" @@ -35,6 +42,12 @@ class PurchaseOrder(ModelBase): """采购单模型""" id = models.BigAutoField(primary_key=True) + merchant = models.ForeignKey( + basic_info_models.Merchant, + on_delete=models.PROTECT, + related_name='purchase_orders', + verbose_name='所属商户' + ) supplier = models.ForeignKey(basic_info_models.Supplier, on_delete=models.PROTECT, related_name='purchase_orders', verbose_name='供应商') order_date = models.DateField(verbose_name='订单日期') total_amount = models.DecimalField(max_digits=15, decimal_places=2, verbose_name='总金额') @@ -52,6 +65,12 @@ class StockChangeRecord(ModelBase): """库存变动记录模型""" id = models.BigAutoField(primary_key=True) + merchant = models.ForeignKey( + basic_info_models.Merchant, + on_delete=models.PROTECT, + related_name='stock_change_records', + verbose_name='所属商户' + ) type = models.IntegerField(choices=StockChangeTypeEnum.choices, verbose_name='变动类型') warehouse = models.ForeignKey( basic_info_models.WareHouse, @@ -127,6 +146,12 @@ class StockChangeDetail(ModelBase): """库存变动明细模型""" id = models.BigAutoField(primary_key=True) + merchant = models.ForeignKey( + basic_info_models.Merchant, + on_delete=models.PROTECT, + related_name='stock_change_details', + verbose_name='所属商户' + ) product = models.ForeignKey( basic_info_models.Product, on_delete=models.PROTECT, @@ -161,6 +186,12 @@ class Inventory(ModelBase): """库存模型""" id = models.BigAutoField(primary_key=True) + merchant = models.ForeignKey( + basic_info_models.Merchant, + on_delete=models.PROTECT, + related_name='inventories', + verbose_name='所属商户' + ) product = models.ForeignKey( basic_info_models.Product, on_delete=models.PROTECT, @@ -192,6 +223,12 @@ class StockSnapshot(ModelBase): """库存变动快照模型""" id = models.BigAutoField(primary_key=True) + merchant = models.ForeignKey( + basic_info_models.Merchant, + on_delete=models.PROTECT, + related_name='stock_snapshots', + verbose_name='所属商户' + ) product = models.ForeignKey( basic_info_models.Product, on_delete=models.PROTECT, @@ -252,3 +289,76 @@ class StockSnapshot(ModelBase): class Meta: verbose_name = '库存快照' verbose_name_plural = '库存快照' + + +class StockFreeze(ModelBase): + """库存冻结模型""" + + id = models.BigAutoField(primary_key=True) + merchant = models.ForeignKey( + basic_info_models.Merchant, + on_delete=models.PROTECT, + related_name='stock_freezes', + verbose_name='所属商户' + ) + product = models.ForeignKey( + basic_info_models.Product, + on_delete=models.PROTECT, + related_name='stock_freezes', + verbose_name='产品', + ) + warehouse = models.ForeignKey( + basic_info_models.WareHouse, + on_delete=models.PROTECT, + related_name='stock_freezes', + verbose_name='仓库', + ) + stock_detail = models.ForeignKey( + StockChangeDetail, + on_delete=models.PROTECT, + related_name='stock_freezes', + verbose_name='关联的库存变动明细', + ) + quantity = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='冻结数量') + unit = models.IntegerField( + verbose_name='单位', + choices=basic_info_models.ProductUnitEnum.choices, + ) + status = models.IntegerField( + choices=StockFreezeStatusEnum.choices, + default=StockFreezeStatusEnum.FROZEN, + verbose_name='冻结状态', + ) + frozen_by = models.ForeignKey( + 'auth.User', + on_delete=models.PROTECT, + verbose_name='冻结操作人', + ) + frozen_with = models.BigIntegerField(null=True, blank=True, verbose_name='冻结关联ID') + cancelled_by = models.ForeignKey( + 'auth.User', + on_delete=models.PROTECT, + null=True, + blank=True, + related_name='cancelled_stock_freezes', + verbose_name='取消操作人', + ) + completed_by = models.ForeignKey( + 'auth.User', + on_delete=models.PROTECT, + null=True, + blank=True, + related_name='completed_stock_freezes', + verbose_name='成单操作人', + ) + completed_at = models.DateTimeField(null=True, blank=True, verbose_name='成单时间') + completed_with = models.BigIntegerField(null=True, blank=True, verbose_name='成单关联ID') + cancelled_at = models.DateTimeField(null=True, blank=True, verbose_name='取消时间') + reason = models.TextField(blank=True, null=True, verbose_name='冻结原因') + + def __str__(self): + return f'库存冻结 {self.id} - 产品: {self.product.name}' + + class Meta: + verbose_name = '库存冻结' + verbose_name_plural = '库存冻结' diff --git a/stock/services.py b/stock/services.py index c93cdea..f379607 100644 --- a/stock/services.py +++ b/stock/services.py @@ -1,4 +1,5 @@ from . import models +from django.db.models import Sum from django.utils import timezone import logging @@ -30,6 +31,7 @@ def create_stock_snapshot(stock_change_detail: models.StockChangeDetail, invento # 必须在库存变动发生后创建快照 snapshot = models.StockSnapshot( + merchant_id=stock_change_detail.merchant_id, product_id=inventory.product_id, warehouse_id=inventory.warehouse_id, delta=delta, @@ -92,6 +94,7 @@ def make_stock_change_completed(stock_change_record: models.StockChangeRecord) - else: # 创建新库存记录 inventory_record = models.Inventory( + merchant_id=stock_change_record.merchant_id, product_id=detail.product_id, warehouse_id=stock_change_record.warehouse_id, quantity=detail.quantity * positive, @@ -114,3 +117,13 @@ def make_stock_change_completed(stock_change_record: models.StockChangeRecord) - f'库存变动记录 {stock_change_record.id} 标记为已完成。' ) return True + + +def get_frozen_stock_count(product_id: int, warehouse_id: int) -> int: + """获取指定产品在指定仓库的冻结库存数量""" + total_frozen = models.StockFreeze.objects.filter( + product_id=product_id, + warehouse_id=warehouse_id, + status=models.StockFreezeStatusEnum.FROZEN + ).aggregate(total=Sum('quantity'))['total'] or 0 + return total_frozen diff --git a/stock/tests.py b/stock/tests.py index 3ef0bcd..9e68fdf 100644 --- a/stock/tests.py +++ b/stock/tests.py @@ -4,7 +4,7 @@ from decimal import Decimal from unittest.mock import patch, MagicMock import logging -from basic_info.models import Product, WareHouse, Supplier, ProductUnitEnum, ProductCategory +from basic_info.models import Product, WareHouse, Supplier, ProductUnitEnum, ProductCategory, Merchant, MerchantTypeEnum from . import models, services @@ -18,8 +18,18 @@ class StockServicesTestCase(TestCase): cls.unit_meter = ProductUnitEnum.METER cls.unit_kg = ProductUnitEnum.KG + # 创建测试商户 + cls.merchant = Merchant.objects.create( + name="测试商户", + type=MerchantTypeEnum.STORE, + area="测试地区", + mobile="13800138000", + contact="测试联系人" + ) + # 创建产品类别 cls.fabric_category = ProductCategory.objects.create( + merchant=cls.merchant, name="布料类", description="各种布料产品", product_prefix="FAB" @@ -27,16 +37,19 @@ class StockServicesTestCase(TestCase): # 创建仓库 cls.warehouse_main = WareHouse.objects.create( + merchant=cls.merchant, name="主仓库", location="主仓库地址" ) cls.warehouse_backup = WareHouse.objects.create( + merchant=cls.merchant, name="备用仓库", location="备用仓库地址" ) # 创建供应商 cls.supplier = Supplier.objects.create( + merchant=cls.merchant, name="测试供应商", contact="张三", mobile="13800138000" @@ -44,6 +57,7 @@ class StockServicesTestCase(TestCase): # 创建产品 cls.product_fabric_a = Product.objects.create( + merchant=cls.merchant, category=cls.fabric_category, name="布料A", human_id="FABRIC-A-001", @@ -54,6 +68,7 @@ class StockServicesTestCase(TestCase): unit=cls.unit_meter ) cls.product_fabric_b = Product.objects.create( + merchant=cls.merchant, category=cls.fabric_category, name="布料B", human_id="FABRIC-B-001", @@ -68,6 +83,7 @@ class StockServicesTestCase(TestCase): """每个测试方法执行前的设置""" # 创建采购单 self.purchase_order = models.PurchaseOrder.objects.create( + merchant=self.merchant, supplier=self.supplier, order_date=timezone.now().date(), total_amount=Decimal('1000.00'), @@ -76,6 +92,7 @@ class StockServicesTestCase(TestCase): # 创建库存变动记录(入库) self.stock_change_in = models.StockChangeRecord.objects.create( + merchant=self.merchant, type=models.StockChangeTypeEnum.ADD, source_type=models.StockChangeSourceEnum.PURCHASE, source_id=self.purchase_order.id, @@ -85,6 +102,7 @@ class StockServicesTestCase(TestCase): # 创建库存变动记录(出库) self.stock_change_out = models.StockChangeRecord.objects.create( + merchant=self.merchant, type=models.StockChangeTypeEnum.REMOVE, source_type=models.StockChangeSourceEnum.SALES, source_id=1, @@ -94,6 +112,7 @@ class StockServicesTestCase(TestCase): # 创建库存变动明细 self.stock_detail_in = models.StockChangeDetail.objects.create( + merchant=self.merchant, stock_change_record=self.stock_change_in, product=self.product_fabric_a, quantity=Decimal('100'), @@ -101,6 +120,7 @@ class StockServicesTestCase(TestCase): ) self.stock_detail_out = models.StockChangeDetail.objects.create( + merchant=self.merchant, stock_change_record=self.stock_change_out, product=self.product_fabric_a, quantity=Decimal('50'), @@ -115,6 +135,7 @@ class FindInventoryTestCase(StockServicesTestCase): """测试查找已存在的库存记录""" # 先创建一个库存记录 inventory = models.Inventory.objects.create( + merchant=self.merchant, product=self.product_fabric_a, warehouse=self.warehouse_main, quantity=50, @@ -140,11 +161,11 @@ class FindInventoryTestCase(StockServicesTestCase): ) self.assertIsNone(result) - def test_find_inventory_wrong_warehouse(self): """测试在错误的仓库中查找库存""" # 在主仓库创建库存 models.Inventory.objects.create( + merchant=self.merchant, product=self.product_fabric_a, warehouse=self.warehouse_main, quantity=50, @@ -162,11 +183,11 @@ class FindInventoryTestCase(StockServicesTestCase): class CreateStockSnapshotTestCase(StockServicesTestCase): """测试 create_stock_snapshot 函数""" - def setUp(self): super().setUp() # 创建库存记录 self.inventory = models.Inventory.objects.create( + merchant=self.merchant, product=self.product_fabric_a, warehouse=self.warehouse_main, quantity=150, @@ -213,6 +234,7 @@ class CreateStockSnapshotTestCase(StockServicesTestCase): # 测试未保存的明细记录 unsaved_detail = models.StockChangeDetail( + merchant=self.merchant, stock_change_record=self.stock_change_in, product=self.product_fabric_a, quantity=50 @@ -231,6 +253,7 @@ class MakeStockChangeCompletedTestCase(StockServicesTestCase): # 测试未保存的记录 unsaved_record = models.StockChangeRecord( + merchant=self.merchant, type=models.StockChangeTypeEnum.ADD, warehouse=self.warehouse_main ) @@ -272,12 +295,12 @@ class MakeStockChangeCompletedTestCase(StockServicesTestCase): product_id=self.product_fabric_a.id, warehouse_id=self.warehouse_main.id ) - mock_snapshot.assert_called_once() def test_complete_inbound_record_existing_inventory(self): """测试完成入库记录(更新现有库存)""" # 创建现有库存 existing_inventory = models.Inventory.objects.create( + merchant=self.merchant, product=self.product_fabric_a, warehouse=self.warehouse_main, quantity=50, @@ -304,6 +327,7 @@ class MakeStockChangeCompletedTestCase(StockServicesTestCase): """测试完成出库记录""" # 创建现有库存 existing_inventory = models.Inventory.objects.create( + merchant=self.merchant, product=self.product_fabric_a, warehouse=self.warehouse_main, quantity=100, @@ -326,6 +350,7 @@ class MakeStockChangeCompletedTestCase(StockServicesTestCase): """测试完成包含多个明细的记录""" # 添加另一个明细 models.StockChangeDetail.objects.create( + merchant=self.merchant, stock_change_record=self.stock_change_in, product=self.product_fabric_b, quantity=Decimal('200'), diff --git a/uv.lock b/uv.lock index b14f914..45e0bac 100644 --- a/uv.lock +++ b/uv.lock @@ -78,10 +78,17 @@ wheels = [ ] [[package]] -name = "django-cors" -version = "0.1" +name = "django-cors-headers" +version = "4.9.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b6/94/59b5353380df1aacd28e1efc1e77ce9925f65ef9adab6c44238daa9cefa3/django-cors-0.1.tar.gz", hash = "sha256:ba31c9455ddb848e578b26bac3634049951149775b818a35476d2b50350640c3", size = 1066, upload-time = "2012-12-04T02:52:05.092Z" } +dependencies = [ + { name = "asgiref" }, + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/39/55822b15b7ec87410f34cd16ce04065ff390e50f9e29f31d6d116fc80456/django_cors_headers-4.9.0.tar.gz", hash = "sha256:fe5d7cb59fdc2c8c646ce84b727ac2bca8912a247e6e68e1fb507372178e59e8", size = 21458, upload-time = "2025-09-18T10:40:52.326Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/d8/19ed1e47badf477d17fb177c1c19b5a21da0fd2d9f093f23be3fb86c5fab/django_cors_headers-4.9.0-py3-none-any.whl", hash = "sha256:15c7f20727f90044dcee2216a9fd7303741a864865f0c3657e28b7056f61b449", size = 12809, upload-time = "2025-09-18T10:40:50.843Z" }, +] [[package]] name = "django-environ" @@ -186,7 +193,7 @@ version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "django" }, - { name = "django-cors" }, + { name = "django-cors-headers" }, { name = "django-environ" }, { name = "django-filter" }, { name = "django-ninja" }, @@ -207,7 +214,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "django", specifier = ">=5.2.7" }, - { name = "django-cors", specifier = ">=0.1" }, + { name = "django-cors-headers", specifier = ">=4.9.0" }, { name = "django-environ", specifier = ">=0.12.0" }, { name = "django-filter", specifier = ">=25.2" }, { name = "django-ninja", specifier = ">=1.4.5" },