1
0
forked from erp-dev/erp

feat: auto complete stock change

This commit is contained in:
2025-11-10 09:01:25 +08:00
parent 37c2e7d723
commit b2078dfa46
82 changed files with 1071 additions and 1336 deletions

2
.env
View File

@@ -3,6 +3,6 @@ DEBUG=True
ALLOWED_HOSTS=*, ALLOWED_HOSTS=*,
DB_HOST=localhost DB_HOST=localhost
DB_PORT=5432 DB_PORT=5432
DB_NAME=myapp DB_NAME=flower
DB_USER=postgres DB_USER=postgres
DB_PASSWORD=postgres DB_PASSWORD=postgres

View File

@@ -2,62 +2,105 @@ from rest_framework import serializers
from basic_info import models as basic_models 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: class Meta:
model = basic_models.ProductCategory model = basic_models.ProductCategory
fields = '__all__' fields = '__all__'
class ProductSerializer(serializers.ModelSerializer): class ProductSerializer(BaseSerializer):
class Meta: class Meta:
model = basic_models.Product model = basic_models.Product
fields = '__all__' 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: class Meta:
model = basic_models.WareHouse model = basic_models.WareHouse
fields = '__all__' fields = '__all__'
class SupplierSerializer(serializers.ModelSerializer): class SupplierSerializer(BaseSerializer):
class Meta: class Meta:
model = basic_models.Supplier model = basic_models.Supplier
fields = '__all__' fields = '__all__'
class EmployeeSerializer(serializers.ModelSerializer): class EmployeeSerializer(BaseSerializer):
class Meta: class Meta:
model = basic_models.Employee model = basic_models.Employee
fields = '__all__' fields = '__all__'
class CustomerSerializer(serializers.ModelSerializer): class CustomerSerializer(BaseSerializer):
created_by = serializers.PrimaryKeyRelatedField(
queryset=basic_models.Employee.objects.all(),
required=False,
)
class Meta: class Meta:
model = basic_models.Customer model = basic_models.Customer
fields = '__all__' fields = '__all__'
class VehicleTypeSerializer(serializers.ModelSerializer): class VehicleTypeSerializer(BaseSerializer):
class Meta: class Meta:
model = basic_models.VehicleType model = basic_models.VehicleType
fields = '__all__' fields = '__all__'
class BankAccountSerializer(serializers.ModelSerializer): class BankAccountSerializer(BaseSerializer):
class Meta: class Meta:
model = basic_models.BankAccount model = basic_models.BankAccount
fields = '__all__' fields = '__all__'
class DeviceInfoSerializer(serializers.ModelSerializer): class DeviceInfoSerializer(BaseSerializer):
class Meta: class Meta:
model = basic_models.DeviceInfo model = basic_models.DeviceInfo
fields = '__all__' fields = '__all__'
class VehicleTransportRecordSerializer(serializers.ModelSerializer): class VehicleTransportRecordSerializer(BaseSerializer):
vehicle_type_name = serializers.CharField(source='vehicle_type.name', read_only=True) vehicle_type_name = serializers.CharField(source='vehicle_type.name', read_only=True)
class Meta: class Meta:

View File

@@ -2,6 +2,8 @@ from django.urls import path, include
from rest_framework import routers from rest_framework import routers
from . import views from . import views
quick_input_router = routers.DefaultRouter()
quick_input_router.register(prefix='', viewset=views.QuickInputViewSet)
product_router = routers.DefaultRouter() product_router = routers.DefaultRouter()
product_router.register(prefix='', viewset=views.ProductViewSet) product_router.register(prefix='', viewset=views.ProductViewSet)
@@ -35,6 +37,7 @@ vehicle_transport_record_router.register(prefix='', viewset=views.VehicleTranspo
urlpatterns = [ urlpatterns = [
path('quick-inputs/', include(quick_input_router.urls)),
path('products/', include(product_router.urls)), path('products/', include(product_router.urls)),
path('warehouses/', include(warehouse_router.urls)), path('warehouses/', include(warehouse_router.urls)),
path('product-categories/', include(product_category_router.urls)), path('product-categories/', include(product_category_router.urls)),

View File

@@ -1,9 +1,38 @@
from rest_framework import viewsets from rest_framework import viewsets
from rest_framework.exceptions import PermissionDenied
from rest_framework.pagination import LimitOffsetPagination
from . import serializers from . import serializers
class BaseViewSet(viewsets.ModelViewSet): 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): class ProductViewSet(BaseViewSet):
@@ -35,6 +64,17 @@ class CustomerViewSet(BaseViewSet):
queryset = serializers.basic_models.Customer.objects queryset = serializers.basic_models.Customer.objects
serializer_class = serializers.CustomerSerializer 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): class VehicleTypeViewSet(BaseViewSet):
queryset = serializers.basic_models.VehicleType.objects queryset = serializers.basic_models.VehicleType.objects

View File

@@ -83,4 +83,11 @@ class CreateStockChangeResponseSerializer(serializers.Serializer):
stock_change_record = StockChangeRecordResponseSerializer() stock_change_record = StockChangeRecordResponseSerializer()
details = StockChangeDetailResponseSerializer(many=True) details = StockChangeDetailResponseSerializer(many=True)
message = serializers.CharField() message = serializers.CharField()
created_details_count = serializers.IntegerField() created_details_count = serializers.IntegerField()
class SetMerchantAutoCompleteStockChangeSerializer(serializers.Serializer):
"""设置商户自动确认库存变动序列化器"""
auto_complete = serializers.BooleanField(
help_text="是否自动确认库存变动"
)

View File

@@ -4,4 +4,9 @@ from . import views
urlpatterns = [ urlpatterns = [
# 库存变动相关API # 库存变动相关API
path('stock-change/', views.create_full_stock_change, name='create_full_stock_change'), 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',
),
] ]

View File

@@ -1,9 +1,8 @@
from rest_framework import status from rest_framework import status
from rest_framework.decorators import api_view, permission_classes from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
from basic_info.services import DataVisibilityService
from django.db import transaction from django.db import transaction
from typing import List, Dict, Any
import logging import logging
from stock import models as stock_models from stock import models as stock_models
@@ -57,6 +56,20 @@ def create_full_stock_change(request):
return Response({ return Response({
'error': '产品列表不能为空' 'error': '产品列表不能为空'
}, status=status.HTTP_400_BAD_REQUEST) }, 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={ product_serializer = serializers.CreateStockChangeSerializer(data={
@@ -83,7 +96,8 @@ def create_full_stock_change(request):
warehouse=warehouse, warehouse=warehouse,
source_type=record_data['source_type'], source_type=record_data['source_type'],
source_id=record_data['source_id'], 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}") logger.info(f"创建新库存变动记录 ID: {stock_change_record.id}")
@@ -108,10 +122,18 @@ def create_full_stock_change(request):
stock_change_record=stock_change_record, stock_change_record=stock_change_record,
product=product, product=product,
quantity=quantity, quantity=quantity,
merchant=request.user.employee.merchant,
unit=product.unit unit=product.unit
) )
created_details.append(detail) created_details.append(detail)
created_count += 1 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. 构建响应 # 3. 构建响应
response_serializer = serializers.CreateStockChangeResponseSerializer({ response_serializer = serializers.CreateStockChangeResponseSerializer({
@@ -134,3 +156,30 @@ def create_full_stock_change(request):
'error': '创建库存变动记录失败', 'error': '创建库存变动记录失败',
'message': str(e) 'message': str(e)
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) }, 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)

View File

@@ -1,21 +1,63 @@
from django.contrib import admin from django.contrib import admin
from basic_info.services import CustomerVisibilityService
from . import models 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) @admin.register(models.Supplier)
class SupplierAdmin(admin.ModelAdmin): class SupplierAdmin(AdminBase):
list_display = ('name', 'area', 'email', 'mobile') list_display = ('name', 'area', 'email', 'mobile')
search_fields = ('name', 'area', 'email', 'mobile') search_fields = ('name', 'area', 'email', 'mobile')
@admin.register(models.ProductCategory) @admin.register(models.ProductCategory)
class ProductCategoryAdmin(admin.ModelAdmin): class ProductCategoryAdmin(AdminBase):
list_display = ('name', 'product_prefix', 'description') list_display = ('name', 'product_prefix', 'description')
search_fields = ('name',) search_fields = ('name',)
@admin.register(models.Product) @admin.register(models.Product)
class ProductAdmin(admin.ModelAdmin): class ProductAdmin(AdminBase):
list_display = ( list_display = (
'name', 'name',
'category', 'category',
@@ -31,29 +73,35 @@ class ProductAdmin(admin.ModelAdmin):
@admin.register(models.BankAccount) @admin.register(models.BankAccount)
class BankAccountAdmin(admin.ModelAdmin): class BankAccountAdmin(AdminBase):
list_display = ('name', 'auto_number') list_display = ('name', 'auto_number')
search_fields = ('name', 'auto_number') search_fields = ('name', 'auto_number')
@admin.register(models.WareHouse) @admin.register(models.WareHouse)
class WareHouseAdmin(admin.ModelAdmin): class WareHouseAdmin(AdminBase):
list_display = ('name', 'location', 'area', 'mobile') list_display = ('name', 'location', 'area', 'mobile', 'mode')
search_fields = ('name', 'area') list_filter = ('area', 'mode')
search_fields = ('name', 'location', 'mobile')
@admin.register(models.Customer) @admin.register(models.Customer)
class CustomerAdmin(admin.ModelAdmin): class CustomerAdmin(AdminBase):
list_display = ('name', 'area', 'email', 'mobile', 'total_debt') list_display = ('name', 'area', 'email', 'mobile', 'created_by', 'total_debt')
search_fields = ('name', 'area', 'email', 'mobile') 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='累欠金额') @admin.display(boolean=False, description='累欠金额')
def total_debt(self, obj): def total_debt(self, obj):
return '0 (动态逻辑字段待实现)' return '0 (动态逻辑字段待实现)'
@admin.register(models.Employee) @admin.register(models.Employee)
class EmployeeAdmin(admin.ModelAdmin): class EmployeeAdmin(AdminBase):
list_display = ('name', 'mobile', 'status', 'job_type', 'sys_user') list_display = ('name', 'mobile', 'status', 'job_type', 'sys_user')
search_fields = ('name', 'mobile') search_fields = ('name', 'mobile')
list_filter = ( list_filter = (
@@ -63,20 +111,20 @@ class EmployeeAdmin(admin.ModelAdmin):
@admin.register(models.DeviceInfo) @admin.register(models.DeviceInfo)
class DeviceInfoAdmin(admin.ModelAdmin): class DeviceInfoAdmin(AdminBase):
list_display = ('name', 'type', 'is_occupied', 'status') list_display = ('name', 'type', 'is_occupied', 'status')
search_fields = ('name', 'start_working_at', 'stop_working_at') search_fields = ('name', 'start_working_at', 'stop_working_at')
list_filter = ('type', 'status') list_filter = ('type', 'status')
@admin.register(models.VehicleType) @admin.register(models.VehicleType)
class VehicleTypeAdmin(admin.ModelAdmin): class VehicleTypeAdmin(AdminBase):
list_display = ('name', 'description') list_display = ('name', 'description')
search_fields = ('name',) search_fields = ('name',)
@admin.register(models.VehicleTransportRecord) @admin.register(models.VehicleTransportRecord)
class VehicleTransportRecordAdmin(admin.ModelAdmin): class VehicleTransportRecordAdmin(AdminBase):
list_display = ('driver_name', 'contact_number', 'delivery_date') list_display = ('driver_name', 'contact_number', 'delivery_date')
search_fields = ('driver_name', 'contact_number') search_fields = ('driver_name', 'contact_number')
list_filter = ('vehicle_type',) list_filter = ('vehicle_type',)

View File

@@ -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 import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
@@ -9,9 +10,88 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
] ]
operations = [ 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( migrations.CreateModel(
name='BankAccount', name='BankAccount',
fields=[ fields=[
@@ -22,6 +102,7 @@ class Migration(migrations.Migration):
('auto_number', models.CharField(max_length=50, verbose_name='自动编号')), ('auto_number', models.CharField(max_length=50, verbose_name='自动编号')),
('description', models.TextField(blank=True, null=True, verbose_name='备注描述')), ('description', models.TextField(blank=True, null=True, verbose_name='备注描述')),
('attachment', models.FileField(blank=True, null=True, upload_to='bank_accounts/', 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={ options={
'verbose_name': '银行账户', 'verbose_name': '银行账户',
@@ -37,6 +118,7 @@ class Migration(migrations.Migration):
('name', models.CharField(max_length=100, verbose_name='类别名称')), ('name', models.CharField(max_length=100, verbose_name='类别名称')),
('description', models.TextField(blank=True, null=True, verbose_name='类别描述')), ('description', models.TextField(blank=True, null=True, verbose_name='类别描述')),
('product_prefix', models.CharField(max_length=5, 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={ options={
'verbose_name': '产品类别', 'verbose_name': '产品类别',
@@ -44,17 +126,28 @@ class Migration(migrations.Migration):
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='ProductUnit', name='Product',
fields=[ fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('id', models.BigAutoField(primary_key=True, serialize=False)), ('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='备注描述')), ('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={ options={
'verbose_name': '产品单位', 'verbose_name': '产品资料',
'verbose_name_plural': '产品单位', 'verbose_name_plural': '产品资料',
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
@@ -69,12 +162,46 @@ class Migration(migrations.Migration):
('mobile', models.CharField(blank=True, max_length=20, 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='联系人')), ('contact', models.TextField(blank=True, null=True, verbose_name='联系人')),
('description', 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={ options={
'verbose_name': '供应商', 'verbose_name': '供应商',
'verbose_name_plural': '供应商', '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( migrations.CreateModel(
name='WareHouse', name='WareHouse',
fields=[ fields=[
@@ -87,47 +214,11 @@ class Migration(migrations.Migration):
('mobile', models.CharField(blank=True, max_length=20, 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='地区')), ('area', models.CharField(blank=True, max_length=100, null=True, verbose_name='地区')),
('description', 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='warehouses', to='basic_info.merchant', verbose_name='所属商户')),
], ],
options={ options={
'verbose_name': '仓库资料', 'verbose_name': '仓库资料',
'verbose_name_plural': '仓库资料', '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': '采购单',
},
),
] ]

View File

@@ -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',
),
]

View File

@@ -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 from django.db import migrations, models
@@ -6,22 +6,23 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('stock', '0009_alter_product_single_price'), ('basic_info', '0001_initial'),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='ProdictUnit', name='QuickInput',
fields=[ fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('id', models.BigAutoField(primary_key=True, serialize=False)), ('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=20, verbose_name='单位名称')), ('name', models.CharField(max_length=100, verbose_name='')),
('description', models.TextField(blank=True, null=True, verbose_name='备注描述')), ('value', models.CharField(max_length=200, verbose_name='')),
('group', models.CharField(verbose_name='')),
], ],
options={ options={
'verbose_name': '产品单位', 'verbose_name': '快捷输入',
'verbose_name_plural': '产品单位', 'verbose_name_plural': '快捷输入',
}, },
), ),
] ]

View File

@@ -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')},
),
]

View File

@@ -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': '客户资料',
},
),
]

View File

@@ -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='联系人'),
),
]

View File

@@ -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='可见该客户的员工'),
),
]

View File

@@ -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='手机'),
),
]

View File

@@ -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='创建者'),
),
]

View File

@@ -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='所属商户'),
),
]

View File

@@ -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': '员工资料',
},
),
]

View File

@@ -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',
),
]

View File

@@ -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='出入库模式'),
),
]

View File

@@ -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='自动确认库存变动'),
),
]

View File

@@ -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',
),
]

View File

@@ -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='系统用户'),
),
]

View File

@@ -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': '设备信息',
},
),
]

View File

@@ -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': '司机车次',
},
),
]

View File

@@ -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='主单位'),
),
]

View File

@@ -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',
),
]

View File

@@ -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='公斤转换率'),
),
]

View File

@@ -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='参考售价'),
),
]

View File

@@ -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='单位'),
),
]

View File

@@ -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='产品图片'),
),
]

View File

@@ -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): class MerchantTypeEnum(models.IntegerChoices):
"""商户类型枚举""" """商户类型枚举"""
STORE = 1, '布行' STORE = 1, '布行'
FACTORY = 2, '印花厂' FACTORY = 2, '印花厂'
class ProductUnitEnum(models.IntegerChoices): class ProductUnitEnum(models.IntegerChoices):
"""产品单位枚举""" """产品单位枚举"""
METER = 1, '' 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): class Merchant(ModelBase):
id = models.BigAutoField(primary_key=True) id = models.BigAutoField(primary_key=True)
name = models.CharField(max_length=100, verbose_name='商户名称') name = models.CharField(max_length=100, verbose_name='商户名称')
@@ -62,6 +85,10 @@ class Merchant(ModelBase):
email = models.EmailField(blank=True, null=True, verbose_name='电子邮箱') email = models.EmailField(blank=True, null=True, verbose_name='电子邮箱')
mobile = models.CharField(max_length=20, 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='联系人') 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='备注描述') description = models.TextField(blank=True, null=True, verbose_name='备注描述')
def __str__(self): def __str__(self):
@@ -74,6 +101,7 @@ class Merchant(ModelBase):
class VehicleType(ModelBase): class VehicleType(ModelBase):
id = models.BigAutoField(primary_key=True) 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='车辆类型名称') name = models.CharField(max_length=50, verbose_name='车辆类型名称')
description = models.TextField(blank=True, null=True, verbose_name='备注描述') description = models.TextField(blank=True, null=True, verbose_name='备注描述')
@@ -87,8 +115,14 @@ class VehicleType(ModelBase):
class WareHouse(ModelBase): class WareHouse(ModelBase):
id = models.BigAutoField(primary_key=True) 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='仓库名称') name = models.CharField(max_length=100, verbose_name='仓库名称')
location = models.CharField(max_length=200, blank=True, null=True, 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='联系人') contact = models.CharField(max_length=100, blank=True, null=True, verbose_name='联系人')
mobile = models.CharField(max_length=20, 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='地区') area = models.CharField(max_length=100, blank=True, null=True, verbose_name='地区')
@@ -104,6 +138,7 @@ class WareHouse(ModelBase):
class Supplier(ModelBase): class Supplier(ModelBase):
id = models.BigAutoField(primary_key=True) 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='供应商名称') name = models.CharField(max_length=100, verbose_name='供应商名称')
area = models.CharField(max_length=100, blank=True, null=True, verbose_name='地区') area = models.CharField(max_length=100, blank=True, null=True, verbose_name='地区')
email = models.EmailField(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): class BankAccount(ModelBase):
id = models.BigAutoField(primary_key=True) 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='名称') name = models.CharField(max_length=100, verbose_name='名称')
auto_number = models.CharField(max_length=50, verbose_name='自动编号') auto_number = models.CharField(max_length=50, verbose_name='自动编号')
description = models.TextField(blank=True, null=True, verbose_name='备注描述') description = models.TextField(blank=True, null=True, verbose_name='备注描述')
@@ -136,6 +172,7 @@ class BankAccount(ModelBase):
class ProductCategory(ModelBase): class ProductCategory(ModelBase):
id = models.BigAutoField(primary_key=True) 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='类别名称') name = models.CharField(max_length=100, verbose_name='类别名称')
description = models.TextField(blank=True, null=True, verbose_name='类别描述') description = models.TextField(blank=True, null=True, verbose_name='类别描述')
product_prefix = models.CharField(max_length=5, verbose_name='产品编号前缀') product_prefix = models.CharField(max_length=5, verbose_name='产品编号前缀')
@@ -150,6 +187,7 @@ class ProductCategory(ModelBase):
class Product(ModelBase): class Product(ModelBase):
id = models.BigAutoField(primary_key=True) id = models.BigAutoField(primary_key=True)
merchant = models.ForeignKey('Merchant', on_delete=models.PROTECT, verbose_name='所属商户')
category = models.ForeignKey( category = models.ForeignKey(
ProductCategory, ProductCategory,
on_delete=models.PROTECT, on_delete=models.PROTECT,
@@ -231,7 +269,21 @@ class Product(ModelBase):
class Customer(ModelBase): class Customer(ModelBase):
id = models.BigAutoField(primary_key=True) 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='客户名称') 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='手机') mobile = models.CharField(max_length=20, verbose_name='手机')
area = models.CharField(max_length=100, blank=True, null=True, verbose_name='地区') area = models.CharField(max_length=100, blank=True, null=True, verbose_name='地区')
email = models.EmailField(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): class Employee(ModelBase):
id = models.BigAutoField(primary_key=True) id = models.BigAutoField(primary_key=True)
merchant = models.ForeignKey('Merchant', on_delete=models.PROTECT, related_name='employees', verbose_name='所属商户')
sys_user = models.OneToOneField( sys_user = models.OneToOneField(
User, User,
on_delete=models.PROTECT, on_delete=models.PROTECT,
@@ -283,6 +336,7 @@ class Employee(ModelBase):
class DeviceInfo(ModelBase): class DeviceInfo(ModelBase):
id = models.BigAutoField(primary_key=True) 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='设备名称') name = models.CharField(max_length=100, verbose_name='设备名称')
type = models.CharField( type = models.CharField(
max_length=20, max_length=20,
@@ -311,6 +365,7 @@ class DeviceInfo(ModelBase):
class VehicleTransportRecord(ModelBase): class VehicleTransportRecord(ModelBase):
id = models.BigAutoField(primary_key=True) 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( vehicle_type = models.ForeignKey(
VehicleType, VehicleType,
on_delete=models.PROTECT, on_delete=models.PROTECT,

75
basic_info/services.py Normal file
View File

@@ -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()

View File

@@ -7,7 +7,7 @@ services:
environment: environment:
POSTGRES_USER: postgres POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD: postgres
POSTGRES_DB: myapp POSTGRES_DB: flower
ports: ports:
- "5432:5432" - "5432:5432"
volumes: volumes:

View File

@@ -49,6 +49,28 @@ DEBUG = env('DEBUG', default=False)
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=[]) 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 # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
@@ -59,6 +81,7 @@ INSTALLED_APPS = [
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', # 使用七牛云存储不需要staticfiles app 'django.contrib.staticfiles', # 使用七牛云存储不需要staticfiles app
'corsheaders', # CORS 支持
'rest_framework', 'rest_framework',
'drf_spectacular', 'drf_spectacular',
'basic_info', 'basic_info',
@@ -70,6 +93,7 @@ INSTALLED_APPS = [
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware', # CORS 中间件,必须放在 CommonMiddleware 之前
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',

View File

@@ -17,16 +17,37 @@ Including another URLconf
from django.contrib import admin from django.contrib import admin
from django.urls import path, include from django.urls import path, include
from stock.views import router 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 ( from rest_framework_simplejwt.views import (
TokenObtainPairView, 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 = [ urlpatterns = [
# JWT 登录 # 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'), # path('api/auth/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
# API 文档 # API 文档
@@ -37,4 +58,6 @@ urlpatterns = [
path('stock/', router.urls), path('stock/', router.urls),
path('api/v1/', include('api_v1.urls')), path('api/v1/', include('api_v1.urls')),
path('api/backend/', include('api_man.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'),
] ]

View File

@@ -6,7 +6,7 @@ readme = "README.md"
requires-python = ">=3.14" requires-python = ">=3.14"
dependencies = [ dependencies = [
"django>=5.2.7", "django>=5.2.7",
"django-cors>=0.1", "django-cors-headers>=4.9.0",
"django-environ>=0.12.0", "django-environ>=0.12.0",
"django-filter>=25.2", "django-filter>=25.2",
"django-ninja>=1.4.5", "django-ninja>=1.4.5",

0
sse/__init__.py Normal file
View File

3
sse/admin.py Normal file
View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
sse/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class SseConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'sse'

View File

3
sse/models.py Normal file
View File

@@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

3
sse/tests.py Normal file
View File

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

45
sse/views.py Normal file
View File

@@ -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'})

View File

@@ -4,6 +4,14 @@ from django.utils.safestring import mark_safe
from . import models, services 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) @admin.register(models.PurchaseOrder)
class PurchaseOrderAdmin(admin.ModelAdmin): class PurchaseOrderAdmin(admin.ModelAdmin):
list_display = ('id', 'supplier', 'order_date', 'total_amount') list_display = ('id', 'supplier', 'order_date', 'total_amount')
@@ -13,7 +21,7 @@ class PurchaseOrderAdmin(admin.ModelAdmin):
@admin.register(models.Inventory) @admin.register(models.Inventory)
class InventoryAdmin(admin.ModelAdmin): class InventoryAdmin(StockAdminBase):
list_display = ( list_display = (
'id', 'id',
'product', 'product',
@@ -22,12 +30,20 @@ class InventoryAdmin(admin.ModelAdmin):
'quantity', 'quantity',
'num_of_rolls', 'num_of_rolls',
'minimum_quantity', 'minimum_quantity',
'frozen_quantity',
'updated_at', 'updated_at',
) )
search_fields = ('product__name', 'warehouse__name') search_fields = ('product__name', 'warehouse__name')
list_filter = ('warehouse',) list_filter = ('warehouse',)
ordering = ('-updated_at',) 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='颜色') @admin.display(description='颜色')
def product_color(self, obj): def product_color(self, obj):
return obj.product.color return obj.product.color
@@ -45,7 +61,7 @@ class StockSnapshotAdmin(admin.ModelAdmin):
'stock_change_record', 'stock_change_record',
'created_at', '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',) list_filter = ('stock_change_record__type',)
ordering = ('-created_at', ) ordering = ('-created_at', )
@@ -73,12 +89,17 @@ class StockChangeDetailAdmin(admin.ModelAdmin):
'stock_change_record', 'stock_change_record',
'product', 'product',
'quantity', 'quantity',
'direction',
'unit', 'unit',
) )
search_fields = ('stock_change_record__id', 'product__name') search_fields = ('stock_change_record__id', 'product__name')
list_filter = ('stock_change_record', 'stock_change_record__type') list_filter = ('stock_change_record', 'stock_change_record__type')
ordering = ('-created_at',) ordering = ('-created_at',)
@admin.display(description='出入方向')
def direction(self, obj):
return '入库' if obj.stock_change_record.is_incoming else '出库'
@admin.register(models.StockChangeRecord) @admin.register(models.StockChangeRecord)
class StockChangeRecordAdmin(admin.ModelAdmin): class StockChangeRecordAdmin(admin.ModelAdmin):
@@ -131,4 +152,29 @@ class StockChangeRecordAdmin(admin.ModelAdmin):
] ]
return mark_safe('<br>'.join(detail_list)) return mark_safe('<br>'.join(detail_list))
actions = [make_selected_stock_changes_completed] 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',)

View File

@@ -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 import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
@@ -9,30 +10,111 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('basic_info', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='ProductCategory', name='PurchaseOrder',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('name', models.CharField(max_length=100, verbose_name='类别名称')), ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('description', models.TextField(blank=True, null=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( migrations.CreateModel(
name='Product', name='StockChangeRecord',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('name', models.CharField(max_length=100, verbose_name='产品名')), ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('human_id', models.CharField(max_length=100, verbose_name='产品编号')), ('id', models.BigAutoField(primary_key=True, serialize=False)),
('color', models.CharField(blank=True, max_length=50, null=True, verbose_name='颜色')), ('type', models.IntegerField(choices=[(1, '入库'), (2, '出库')], verbose_name='变动类型')),
('width_size', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='宽幅')), ('source_type', models.IntegerField(choices=[(1, '采购'), (2, '销退'), (3, '调入'), (4, '盘盈'), (5, '销售'), (6, '采购退货'), (7, '调出'), (8, '盘亏')], verbose_name='变动来源')),
('single_price', models.DecimalField(decimal_places=2, max_digits=10, 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='规格')), ('spec', models.CharField(blank=True, max_length=100, null=True, verbose_name='规格')),
('description', models.TextField(blank=True, null=True, verbose_name='备注描述')), ('description', models.TextField(blank=True, null=True, verbose_name='备注描述')),
('unit', models.CharField(default='', max_length=20, verbose_name='单位')), ('merchant', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='inventories', to='basic_info.merchant', verbose_name='所属商户')),
('category', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='products', to='stock.productcategory', 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')},
},
), ),
] ]

View File

@@ -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': '产品类别'},
),
]

View File

@@ -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': '库存冻结',
},
),
]

View File

@@ -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': '供应商',
},
),
]

View File

@@ -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': '采购单',
},
),
]

View File

@@ -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='更新时间'),
),
]

View File

@@ -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': '银行账户',
},
),
]

View File

@@ -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='产品编号前缀'),
),
]

View File

@@ -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='产品编号前缀'),
),
]

View File

@@ -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='单价'),
),
]

View File

@@ -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='单位'),
),
]

View File

@@ -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',
),
]

View File

@@ -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': '仓库资料',
},
),
]

View File

@@ -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',
),
]

View File

@@ -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': '采购单',
},
),
]

View File

@@ -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': '库存',
},
),
]

View File

@@ -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='库存数量'),
),
]

View File

@@ -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',
),
]

View File

@@ -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')},
),
]

View File

@@ -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': '出入库记录',
},
),
]

View File

@@ -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,
),
]

View File

@@ -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,
),
]

View File

@@ -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='匹数'),
),
]

View File

@@ -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='创建人'),
),
]

View File

@@ -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='操作人'),
),
]

View File

@@ -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='完成时间'),
),
]

View File

@@ -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='匹数'),
),
]

View File

@@ -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': '库存快照',
},
),
]

View File

@@ -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='变动前库存数量'),
),
]

View File

@@ -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='库存变动数量'),
),
]

View File

@@ -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': '库存变动明细',
},
),
]

View File

@@ -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',
),
]

View File

@@ -8,6 +8,13 @@ import decimal
# ==================== 枚举类定义 ==================== # ==================== 枚举类定义 ====================
class StockFreezeStatusEnum(models.IntegerChoices):
"""库存冻结状态类型"""
FROZEN = 1, '冻结中'
CANCELLED = 2, '已取消'
COMPLETED = 3, '已成单'
class StockChangeSourceEnum(models.IntegerChoices): class StockChangeSourceEnum(models.IntegerChoices):
"""库存变动来源类型""" """库存变动来源类型"""
@@ -35,6 +42,12 @@ class PurchaseOrder(ModelBase):
"""采购单模型""" """采购单模型"""
id = models.BigAutoField(primary_key=True) 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='供应商') supplier = models.ForeignKey(basic_info_models.Supplier, on_delete=models.PROTECT, related_name='purchase_orders', verbose_name='供应商')
order_date = models.DateField(verbose_name='订单日期') order_date = models.DateField(verbose_name='订单日期')
total_amount = models.DecimalField(max_digits=15, decimal_places=2, 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) 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='变动类型') type = models.IntegerField(choices=StockChangeTypeEnum.choices, verbose_name='变动类型')
warehouse = models.ForeignKey( warehouse = models.ForeignKey(
basic_info_models.WareHouse, basic_info_models.WareHouse,
@@ -127,6 +146,12 @@ class StockChangeDetail(ModelBase):
"""库存变动明细模型""" """库存变动明细模型"""
id = models.BigAutoField(primary_key=True) 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( product = models.ForeignKey(
basic_info_models.Product, basic_info_models.Product,
on_delete=models.PROTECT, on_delete=models.PROTECT,
@@ -161,6 +186,12 @@ class Inventory(ModelBase):
"""库存模型""" """库存模型"""
id = models.BigAutoField(primary_key=True) 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( product = models.ForeignKey(
basic_info_models.Product, basic_info_models.Product,
on_delete=models.PROTECT, on_delete=models.PROTECT,
@@ -192,6 +223,12 @@ class StockSnapshot(ModelBase):
"""库存变动快照模型""" """库存变动快照模型"""
id = models.BigAutoField(primary_key=True) 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( product = models.ForeignKey(
basic_info_models.Product, basic_info_models.Product,
on_delete=models.PROTECT, on_delete=models.PROTECT,
@@ -252,3 +289,76 @@ class StockSnapshot(ModelBase):
class Meta: class Meta:
verbose_name = '库存快照' verbose_name = '库存快照'
verbose_name_plural = '库存快照' 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 = '库存冻结'

View File

@@ -1,4 +1,5 @@
from . import models from . import models
from django.db.models import Sum
from django.utils import timezone from django.utils import timezone
import logging import logging
@@ -30,6 +31,7 @@ def create_stock_snapshot(stock_change_detail: models.StockChangeDetail, invento
# 必须在库存变动发生后创建快照 # 必须在库存变动发生后创建快照
snapshot = models.StockSnapshot( snapshot = models.StockSnapshot(
merchant_id=stock_change_detail.merchant_id,
product_id=inventory.product_id, product_id=inventory.product_id,
warehouse_id=inventory.warehouse_id, warehouse_id=inventory.warehouse_id,
delta=delta, delta=delta,
@@ -92,6 +94,7 @@ def make_stock_change_completed(stock_change_record: models.StockChangeRecord) -
else: else:
# 创建新库存记录 # 创建新库存记录
inventory_record = models.Inventory( inventory_record = models.Inventory(
merchant_id=stock_change_record.merchant_id,
product_id=detail.product_id, product_id=detail.product_id,
warehouse_id=stock_change_record.warehouse_id, warehouse_id=stock_change_record.warehouse_id,
quantity=detail.quantity * positive, quantity=detail.quantity * positive,
@@ -114,3 +117,13 @@ def make_stock_change_completed(stock_change_record: models.StockChangeRecord) -
f'库存变动记录 {stock_change_record.id} 标记为已完成。' f'库存变动记录 {stock_change_record.id} 标记为已完成。'
) )
return True 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

View File

@@ -4,7 +4,7 @@ from decimal import Decimal
from unittest.mock import patch, MagicMock from unittest.mock import patch, MagicMock
import logging 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 from . import models, services
@@ -18,8 +18,18 @@ class StockServicesTestCase(TestCase):
cls.unit_meter = ProductUnitEnum.METER cls.unit_meter = ProductUnitEnum.METER
cls.unit_kg = ProductUnitEnum.KG cls.unit_kg = ProductUnitEnum.KG
# 创建测试商户
cls.merchant = Merchant.objects.create(
name="测试商户",
type=MerchantTypeEnum.STORE,
area="测试地区",
mobile="13800138000",
contact="测试联系人"
)
# 创建产品类别 # 创建产品类别
cls.fabric_category = ProductCategory.objects.create( cls.fabric_category = ProductCategory.objects.create(
merchant=cls.merchant,
name="布料类", name="布料类",
description="各种布料产品", description="各种布料产品",
product_prefix="FAB" product_prefix="FAB"
@@ -27,16 +37,19 @@ class StockServicesTestCase(TestCase):
# 创建仓库 # 创建仓库
cls.warehouse_main = WareHouse.objects.create( cls.warehouse_main = WareHouse.objects.create(
merchant=cls.merchant,
name="主仓库", name="主仓库",
location="主仓库地址" location="主仓库地址"
) )
cls.warehouse_backup = WareHouse.objects.create( cls.warehouse_backup = WareHouse.objects.create(
merchant=cls.merchant,
name="备用仓库", name="备用仓库",
location="备用仓库地址" location="备用仓库地址"
) )
# 创建供应商 # 创建供应商
cls.supplier = Supplier.objects.create( cls.supplier = Supplier.objects.create(
merchant=cls.merchant,
name="测试供应商", name="测试供应商",
contact="张三", contact="张三",
mobile="13800138000" mobile="13800138000"
@@ -44,6 +57,7 @@ class StockServicesTestCase(TestCase):
# 创建产品 # 创建产品
cls.product_fabric_a = Product.objects.create( cls.product_fabric_a = Product.objects.create(
merchant=cls.merchant,
category=cls.fabric_category, category=cls.fabric_category,
name="布料A", name="布料A",
human_id="FABRIC-A-001", human_id="FABRIC-A-001",
@@ -54,6 +68,7 @@ class StockServicesTestCase(TestCase):
unit=cls.unit_meter unit=cls.unit_meter
) )
cls.product_fabric_b = Product.objects.create( cls.product_fabric_b = Product.objects.create(
merchant=cls.merchant,
category=cls.fabric_category, category=cls.fabric_category,
name="布料B", name="布料B",
human_id="FABRIC-B-001", human_id="FABRIC-B-001",
@@ -68,6 +83,7 @@ class StockServicesTestCase(TestCase):
"""每个测试方法执行前的设置""" """每个测试方法执行前的设置"""
# 创建采购单 # 创建采购单
self.purchase_order = models.PurchaseOrder.objects.create( self.purchase_order = models.PurchaseOrder.objects.create(
merchant=self.merchant,
supplier=self.supplier, supplier=self.supplier,
order_date=timezone.now().date(), order_date=timezone.now().date(),
total_amount=Decimal('1000.00'), total_amount=Decimal('1000.00'),
@@ -76,6 +92,7 @@ class StockServicesTestCase(TestCase):
# 创建库存变动记录(入库) # 创建库存变动记录(入库)
self.stock_change_in = models.StockChangeRecord.objects.create( self.stock_change_in = models.StockChangeRecord.objects.create(
merchant=self.merchant,
type=models.StockChangeTypeEnum.ADD, type=models.StockChangeTypeEnum.ADD,
source_type=models.StockChangeSourceEnum.PURCHASE, source_type=models.StockChangeSourceEnum.PURCHASE,
source_id=self.purchase_order.id, source_id=self.purchase_order.id,
@@ -85,6 +102,7 @@ class StockServicesTestCase(TestCase):
# 创建库存变动记录(出库) # 创建库存变动记录(出库)
self.stock_change_out = models.StockChangeRecord.objects.create( self.stock_change_out = models.StockChangeRecord.objects.create(
merchant=self.merchant,
type=models.StockChangeTypeEnum.REMOVE, type=models.StockChangeTypeEnum.REMOVE,
source_type=models.StockChangeSourceEnum.SALES, source_type=models.StockChangeSourceEnum.SALES,
source_id=1, source_id=1,
@@ -94,6 +112,7 @@ class StockServicesTestCase(TestCase):
# 创建库存变动明细 # 创建库存变动明细
self.stock_detail_in = models.StockChangeDetail.objects.create( self.stock_detail_in = models.StockChangeDetail.objects.create(
merchant=self.merchant,
stock_change_record=self.stock_change_in, stock_change_record=self.stock_change_in,
product=self.product_fabric_a, product=self.product_fabric_a,
quantity=Decimal('100'), quantity=Decimal('100'),
@@ -101,6 +120,7 @@ class StockServicesTestCase(TestCase):
) )
self.stock_detail_out = models.StockChangeDetail.objects.create( self.stock_detail_out = models.StockChangeDetail.objects.create(
merchant=self.merchant,
stock_change_record=self.stock_change_out, stock_change_record=self.stock_change_out,
product=self.product_fabric_a, product=self.product_fabric_a,
quantity=Decimal('50'), quantity=Decimal('50'),
@@ -115,6 +135,7 @@ class FindInventoryTestCase(StockServicesTestCase):
"""测试查找已存在的库存记录""" """测试查找已存在的库存记录"""
# 先创建一个库存记录 # 先创建一个库存记录
inventory = models.Inventory.objects.create( inventory = models.Inventory.objects.create(
merchant=self.merchant,
product=self.product_fabric_a, product=self.product_fabric_a,
warehouse=self.warehouse_main, warehouse=self.warehouse_main,
quantity=50, quantity=50,
@@ -140,11 +161,11 @@ class FindInventoryTestCase(StockServicesTestCase):
) )
self.assertIsNone(result) self.assertIsNone(result)
def test_find_inventory_wrong_warehouse(self): def test_find_inventory_wrong_warehouse(self):
"""测试在错误的仓库中查找库存""" """测试在错误的仓库中查找库存"""
# 在主仓库创建库存 # 在主仓库创建库存
models.Inventory.objects.create( models.Inventory.objects.create(
merchant=self.merchant,
product=self.product_fabric_a, product=self.product_fabric_a,
warehouse=self.warehouse_main, warehouse=self.warehouse_main,
quantity=50, quantity=50,
@@ -162,11 +183,11 @@ class FindInventoryTestCase(StockServicesTestCase):
class CreateStockSnapshotTestCase(StockServicesTestCase): class CreateStockSnapshotTestCase(StockServicesTestCase):
"""测试 create_stock_snapshot 函数""" """测试 create_stock_snapshot 函数"""
def setUp(self): def setUp(self):
super().setUp() super().setUp()
# 创建库存记录 # 创建库存记录
self.inventory = models.Inventory.objects.create( self.inventory = models.Inventory.objects.create(
merchant=self.merchant,
product=self.product_fabric_a, product=self.product_fabric_a,
warehouse=self.warehouse_main, warehouse=self.warehouse_main,
quantity=150, quantity=150,
@@ -213,6 +234,7 @@ class CreateStockSnapshotTestCase(StockServicesTestCase):
# 测试未保存的明细记录 # 测试未保存的明细记录
unsaved_detail = models.StockChangeDetail( unsaved_detail = models.StockChangeDetail(
merchant=self.merchant,
stock_change_record=self.stock_change_in, stock_change_record=self.stock_change_in,
product=self.product_fabric_a, product=self.product_fabric_a,
quantity=50 quantity=50
@@ -231,6 +253,7 @@ class MakeStockChangeCompletedTestCase(StockServicesTestCase):
# 测试未保存的记录 # 测试未保存的记录
unsaved_record = models.StockChangeRecord( unsaved_record = models.StockChangeRecord(
merchant=self.merchant,
type=models.StockChangeTypeEnum.ADD, type=models.StockChangeTypeEnum.ADD,
warehouse=self.warehouse_main warehouse=self.warehouse_main
) )
@@ -272,12 +295,12 @@ class MakeStockChangeCompletedTestCase(StockServicesTestCase):
product_id=self.product_fabric_a.id, product_id=self.product_fabric_a.id,
warehouse_id=self.warehouse_main.id warehouse_id=self.warehouse_main.id
) )
mock_snapshot.assert_called_once()
def test_complete_inbound_record_existing_inventory(self): def test_complete_inbound_record_existing_inventory(self):
"""测试完成入库记录(更新现有库存)""" """测试完成入库记录(更新现有库存)"""
# 创建现有库存 # 创建现有库存
existing_inventory = models.Inventory.objects.create( existing_inventory = models.Inventory.objects.create(
merchant=self.merchant,
product=self.product_fabric_a, product=self.product_fabric_a,
warehouse=self.warehouse_main, warehouse=self.warehouse_main,
quantity=50, quantity=50,
@@ -304,6 +327,7 @@ class MakeStockChangeCompletedTestCase(StockServicesTestCase):
"""测试完成出库记录""" """测试完成出库记录"""
# 创建现有库存 # 创建现有库存
existing_inventory = models.Inventory.objects.create( existing_inventory = models.Inventory.objects.create(
merchant=self.merchant,
product=self.product_fabric_a, product=self.product_fabric_a,
warehouse=self.warehouse_main, warehouse=self.warehouse_main,
quantity=100, quantity=100,
@@ -326,6 +350,7 @@ class MakeStockChangeCompletedTestCase(StockServicesTestCase):
"""测试完成包含多个明细的记录""" """测试完成包含多个明细的记录"""
# 添加另一个明细 # 添加另一个明细
models.StockChangeDetail.objects.create( models.StockChangeDetail.objects.create(
merchant=self.merchant,
stock_change_record=self.stock_change_in, stock_change_record=self.stock_change_in,
product=self.product_fabric_b, product=self.product_fabric_b,
quantity=Decimal('200'), quantity=Decimal('200'),

17
uv.lock generated
View File

@@ -78,10 +78,17 @@ wheels = [
] ]
[[package]] [[package]]
name = "django-cors" name = "django-cors-headers"
version = "0.1" version = "4.9.0"
source = { registry = "https://pypi.org/simple" } 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]] [[package]]
name = "django-environ" name = "django-environ"
@@ -186,7 +193,7 @@ version = "0.1.0"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "django" }, { name = "django" },
{ name = "django-cors" }, { name = "django-cors-headers" },
{ name = "django-environ" }, { name = "django-environ" },
{ name = "django-filter" }, { name = "django-filter" },
{ name = "django-ninja" }, { name = "django-ninja" },
@@ -207,7 +214,7 @@ dev = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "django", specifier = ">=5.2.7" }, { 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-environ", specifier = ">=0.12.0" },
{ name = "django-filter", specifier = ">=25.2" }, { name = "django-filter", specifier = ">=25.2" },
{ name = "django-ninja", specifier = ">=1.4.5" }, { name = "django-ninja", specifier = ">=1.4.5" },