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=*,
DB_HOST=localhost
DB_PORT=5432
DB_NAME=myapp
DB_NAME=flower
DB_USER=postgres
DB_PASSWORD=postgres

View File

@@ -2,62 +2,105 @@ from rest_framework import serializers
from basic_info import models as basic_models
class ProductCategorySerializer(serializers.ModelSerializer):
class BaseSerializer(serializers.ModelSerializer):
merchant = serializers.PrimaryKeyRelatedField(
queryset=basic_models.Merchant.objects.all(),
required=False # 创建时由视图层设置
)
def to_representation(self, instance):
try:
merchant = self.context['request'].user.employee.merchant
if instance.merchant != merchant:
# 数据保护手段
raise PermissionError("无权限访问该对象")
except (KeyError, AttributeError):
# TODO: log something?
raise PermissionError("无权限访问该对象")
result = super().to_representation(instance)
if self.context.get('expand_merchant', False):
result['merchant'] = {
'id': instance.merchant.id,
'name': instance.merchant.name
}
return result
class QuickInputSerializer(serializers.ModelSerializer):
class Meta:
model = basic_models.QuickInput
fields = ['name', 'value', 'group']
class ProductCategorySerializer(BaseSerializer):
class Meta:
model = basic_models.ProductCategory
fields = '__all__'
class ProductSerializer(serializers.ModelSerializer):
class ProductSerializer(BaseSerializer):
class Meta:
model = basic_models.Product
fields = '__all__'
depth = 1 # 展开外键关系
def to_representation(self, instance):
result = super().to_representation(instance)
result['category'] = {
'id': instance.category.id,
'name': instance.category.name
}
return result
class WareHouseSerializer(serializers.ModelSerializer):
class WareHouseSerializer(BaseSerializer):
class Meta:
model = basic_models.WareHouse
fields = '__all__'
class SupplierSerializer(serializers.ModelSerializer):
class SupplierSerializer(BaseSerializer):
class Meta:
model = basic_models.Supplier
fields = '__all__'
class EmployeeSerializer(serializers.ModelSerializer):
class EmployeeSerializer(BaseSerializer):
class Meta:
model = basic_models.Employee
fields = '__all__'
class CustomerSerializer(serializers.ModelSerializer):
class CustomerSerializer(BaseSerializer):
created_by = serializers.PrimaryKeyRelatedField(
queryset=basic_models.Employee.objects.all(),
required=False,
)
class Meta:
model = basic_models.Customer
fields = '__all__'
class VehicleTypeSerializer(serializers.ModelSerializer):
class VehicleTypeSerializer(BaseSerializer):
class Meta:
model = basic_models.VehicleType
fields = '__all__'
class BankAccountSerializer(serializers.ModelSerializer):
class BankAccountSerializer(BaseSerializer):
class Meta:
model = basic_models.BankAccount
fields = '__all__'
class DeviceInfoSerializer(serializers.ModelSerializer):
class DeviceInfoSerializer(BaseSerializer):
class Meta:
model = basic_models.DeviceInfo
fields = '__all__'
class VehicleTransportRecordSerializer(serializers.ModelSerializer):
class VehicleTransportRecordSerializer(BaseSerializer):
vehicle_type_name = serializers.CharField(source='vehicle_type.name', read_only=True)
class Meta:

View File

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

View File

@@ -1,9 +1,38 @@
from rest_framework import viewsets
from rest_framework.exceptions import PermissionDenied
from rest_framework.pagination import LimitOffsetPagination
from . import serializers
class BaseViewSet(viewsets.ModelViewSet):
permission_classes = [] # 允许所有访问
pagination_class = LimitOffsetPagination
def get_queryset(self):
qs = super().get_queryset()
# 如果是超级用户,返回所有数据
# TODO: 测试结束后要取注释
# if self.request.user.is_superuser:
# return qs
return qs.filter(merchant=self.request.user.employee.merchant)
def perform_create(self, serializer):
try:
merchant = self.request.user.employee.merchant
serializer.save(merchant_id=merchant.id)
except AttributeError:
raise PermissionDenied("无权限创建该对象")
class QuickInputViewSet(viewsets.ModelViewSet):
queryset = serializers.basic_models.QuickInput.objects
serializer_class = serializers.QuickInputSerializer
def filter_queryset(self, queryset):
qs = super().filter_queryset(queryset)
group = self.request.query_params.get('group')
if group:
qs = qs.filter(group=group)
return qs
class ProductViewSet(BaseViewSet):
@@ -35,6 +64,17 @@ class CustomerViewSet(BaseViewSet):
queryset = serializers.basic_models.Customer.objects
serializer_class = serializers.CustomerSerializer
def perform_create(self, serializer):
merchant = self.request.user.employee.merchant
employee = self.request.user.employee
serializer.save(merchant=merchant, created_by=employee)
def get_queryset(self):
qs = super().get_queryset()
# 应用可见性过滤
from basic_info.services import CustomerVisibilityService
return CustomerVisibilityService.filter_customers_for_employee(qs, self.request.user)
class VehicleTypeViewSet(BaseViewSet):
queryset = serializers.basic_models.VehicleType.objects

View File

@@ -84,3 +84,10 @@ class CreateStockChangeResponseSerializer(serializers.Serializer):
details = StockChangeDetailResponseSerializer(many=True)
message = serializers.CharField()
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 = [
# 库存变动相关API
path('stock-change/', views.create_full_stock_change, name='create_full_stock_change'),
path(
'set-merchant-auto-complete-stock-change/',
views.set_merchant_auto_complete_stock_change,
name='set_merchant_auto_complete_stock_change',
),
]

View File

@@ -1,9 +1,8 @@
from rest_framework import status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from basic_info.services import DataVisibilityService
from django.db import transaction
from typing import List, Dict, Any
import logging
from stock import models as stock_models
@@ -58,6 +57,20 @@ def create_full_stock_change(request):
'error': '产品列表不能为空'
}, status=status.HTTP_400_BAD_REQUEST)
# 验证仓库是否本员工可见
if not DataVisibilityService.is_warehouse_visible_to_employee(record_data['warehouse'], request.user):
return Response({
'error': f'仓库ID {record_data["warehouse"]} 对当前用户不可见'
}, status=status.HTTP_403_FORBIDDEN)
# 验证产品是否本员工可见
for p in products_data:
product_id = p.get('product')
if not DataVisibilityService.is_product_visible_to_employee(product_id, request.user):
return Response({
'error': f'产品ID {product_id} 对当前用户不可见'
}, status=status.HTTP_403_FORBIDDEN)
# 验证产品数据
product_serializer = serializers.CreateStockChangeSerializer(data={
'products': products_data
@@ -83,7 +96,8 @@ def create_full_stock_change(request):
warehouse=warehouse,
source_type=record_data['source_type'],
source_id=record_data['source_id'],
# created_by=request.user
merchant=request.user.employee.merchant,
created_by=request.user
)
logger.info(f"创建新库存变动记录 ID: {stock_change_record.id}")
@@ -108,11 +122,19 @@ def create_full_stock_change(request):
stock_change_record=stock_change_record,
product=product,
quantity=quantity,
merchant=request.user.employee.merchant,
unit=product.unit
)
created_details.append(detail)
created_count += 1
if warehouse.merchant.auto_complete_stock_change:
# 自动确认库存变动
from stock import services as stock_services
stock_services.make_stock_change_completed(stock_change_record)
logger.info(f"自动确认库存变动记录 ID: {stock_change_record.id}")
# 3. 构建响应
response_serializer = serializers.CreateStockChangeResponseSerializer({
'stock_change_record': stock_change_record,
@@ -134,3 +156,30 @@ def create_full_stock_change(request):
'error': '创建库存变动记录失败',
'message': str(e)
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@extend_schema(tags=['修改商户自动确认出入库设定'])
@api_view(['POST'])
def set_merchant_auto_complete_stock_change(request):
"""
设置商户自动确认库存变动设定
POST /api/v1/merchant/set-auto-complete-stock-change/
请求参数:
{
"auto_complete_stock_change": true // 是否自动确认库存变动
}
"""
try:
emp = request.user.employee
merchant = emp.merchant
serializer = serializers.SetMerchantAutoCompleteStockChangeSerializer(data=request.data)
if serializer.is_valid():
auto_complete = serializer.validated_data['auto_complete']
merchant.auto_complete_stock_change = auto_complete
merchant.save()
logger.info(f"用户 {request.user.username} 设置商户 {merchant.name} 自动确认库存变动为 {auto_complete}")
return Response({'status': 'success'})
return Response({'error': serializer.errors}, status=status.HTTP_400_BAD_REQUEST)
except AttributeError:
logger.error(f"用户 {request.user.username} 无权限设置商户自动确认库存变动", exc_info=True)
return Response({'error': '无权限操作'}, status=status.HTTP_403_FORBIDDEN)

View File

@@ -1,21 +1,63 @@
from django.contrib import admin
from basic_info.services import CustomerVisibilityService
from . import models
class AdminBase(admin.ModelAdmin):
"""管理员基础类,定义通用配置"""
def get_readonly_fields(self, request, obj = ...):
readonly_fields = list(super().get_readonly_fields(request, obj))
if request.user.is_superuser:
readonly_fields.append('merchant')
return readonly_fields
def get_list_display(self, request):
result = super().get_list_display(request)
result = list(result)
if 'id' not in result:
result = ['id'] + result
if request.user.is_superuser and 'merchant' not in result:
result = result + ['merchant']
return result
def save_model(self, request, obj, form, change):
if not change:
if not obj.merchant_id:
obj.merchant_id = request.user.employee.merchant.id
return super().save_model(request, obj, form, change)
@admin.register(models.QuickInput)
class QuickInputAdmin(admin.ModelAdmin):
list_display = ('name', 'value', 'group')
list_filter = ('group',)
@admin.register(models.Merchant)
class MerchantAdmin(admin.ModelAdmin):
list_display = ('name', 'type', 'email', 'mobile', 'auto_complete_stock_change')
search_fields = ('name', 'mobile')
list_filter = ('type', 'auto_complete_stock_change')
@admin.register(models.Supplier)
class SupplierAdmin(admin.ModelAdmin):
class SupplierAdmin(AdminBase):
list_display = ('name', 'area', 'email', 'mobile')
search_fields = ('name', 'area', 'email', 'mobile')
@admin.register(models.ProductCategory)
class ProductCategoryAdmin(admin.ModelAdmin):
class ProductCategoryAdmin(AdminBase):
list_display = ('name', 'product_prefix', 'description')
search_fields = ('name',)
@admin.register(models.Product)
class ProductAdmin(admin.ModelAdmin):
class ProductAdmin(AdminBase):
list_display = (
'name',
'category',
@@ -31,29 +73,35 @@ class ProductAdmin(admin.ModelAdmin):
@admin.register(models.BankAccount)
class BankAccountAdmin(admin.ModelAdmin):
class BankAccountAdmin(AdminBase):
list_display = ('name', 'auto_number')
search_fields = ('name', 'auto_number')
@admin.register(models.WareHouse)
class WareHouseAdmin(admin.ModelAdmin):
list_display = ('name', 'location', 'area', 'mobile')
search_fields = ('name', 'area')
class WareHouseAdmin(AdminBase):
list_display = ('name', 'location', 'area', 'mobile', 'mode')
list_filter = ('area', 'mode')
search_fields = ('name', 'location', 'mobile')
@admin.register(models.Customer)
class CustomerAdmin(admin.ModelAdmin):
list_display = ('name', 'area', 'email', 'mobile', 'total_debt')
class CustomerAdmin(AdminBase):
list_display = ('name', 'area', 'email', 'mobile', 'created_by', 'total_debt')
search_fields = ('name', 'area', 'email', 'mobile')
def get_queryset(self, request):
qs = super().get_queryset(request)
qs = CustomerVisibilityService.filter_customers_for_employee(qs, request.user)
return qs
@admin.display(boolean=False, description='累欠金额')
def total_debt(self, obj):
return '0 (动态逻辑字段待实现)'
@admin.register(models.Employee)
class EmployeeAdmin(admin.ModelAdmin):
class EmployeeAdmin(AdminBase):
list_display = ('name', 'mobile', 'status', 'job_type', 'sys_user')
search_fields = ('name', 'mobile')
list_filter = (
@@ -63,20 +111,20 @@ class EmployeeAdmin(admin.ModelAdmin):
@admin.register(models.DeviceInfo)
class DeviceInfoAdmin(admin.ModelAdmin):
class DeviceInfoAdmin(AdminBase):
list_display = ('name', 'type', 'is_occupied', 'status')
search_fields = ('name', 'start_working_at', 'stop_working_at')
list_filter = ('type', 'status')
@admin.register(models.VehicleType)
class VehicleTypeAdmin(admin.ModelAdmin):
class VehicleTypeAdmin(AdminBase):
list_display = ('name', 'description')
search_fields = ('name',)
@admin.register(models.VehicleTransportRecord)
class VehicleTransportRecordAdmin(admin.ModelAdmin):
class VehicleTransportRecordAdmin(AdminBase):
list_display = ('driver_name', 'contact_number', 'delivery_date')
search_fields = ('driver_name', 'contact_number')
list_filter = ('vehicle_type',)

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
from django.conf import settings
from django.db import migrations, models
@@ -9,9 +10,88 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Merchant',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=100, verbose_name='商户名称')),
('type', models.IntegerField(choices=[(1, '布行'), (2, '印花厂')], verbose_name='商户类型')),
('area', models.CharField(blank=True, max_length=100, null=True, verbose_name='地区')),
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='电子邮箱')),
('mobile', models.CharField(blank=True, max_length=20, null=True, verbose_name='手机')),
('contact', models.TextField(blank=True, null=True, verbose_name='联系人')),
('description', models.TextField(blank=True, null=True, verbose_name='备注描述')),
],
options={
'verbose_name': '商户资料',
'verbose_name_plural': '商户资料',
},
),
migrations.CreateModel(
name='Employee',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=100, verbose_name='员工姓名')),
('job_type', models.CharField(choices=[('打纸', '打纸'), ('滚筒', '滚筒'), ('仓库', '仓库'), ('送货', '送货')], default='打纸', max_length=20, verbose_name='职位')),
('mobile', models.CharField(blank=True, max_length=20, null=True, verbose_name='手机')),
('area', models.CharField(blank=True, max_length=100, null=True, verbose_name='地区')),
('description', models.TextField(blank=True, null=True, verbose_name='备注描述')),
('status', models.CharField(choices=[('在职', '在职'), ('离职', '离职')], default='在职', max_length=20, verbose_name='员工状态')),
('sys_user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='employee', to=settings.AUTH_USER_MODEL, verbose_name='系统用户')),
('merchant', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='employees', to='basic_info.merchant', verbose_name='所属商户')),
],
options={
'verbose_name': '员工资料',
'verbose_name_plural': '员工资料',
},
),
migrations.CreateModel(
name='DeviceInfo',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=100, verbose_name='设备名称')),
('type', models.CharField(choices=[('滚筒', '滚筒'), ('打印', '打印')], default='滚筒', max_length=20, verbose_name='设备类型')),
('status', models.CharField(choices=[('使用中', '使用中'), ('停用', '停用'), ('维修中', '维修中')], default='使用中', max_length=20, verbose_name='设备状态')),
('start_working_at', models.DateField(blank=True, null=True, verbose_name='开始服役日期')),
('stop_working_at', models.DateField(blank=True, null=True, verbose_name='停止服役日期')),
('is_occupied', models.BooleanField(default=False, verbose_name='是否被占用')),
('description', models.TextField(blank=True, null=True, verbose_name='备注描述')),
('merchant', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='basic_info.merchant', verbose_name='所属商户')),
],
options={
'verbose_name': '设备资料',
'verbose_name_plural': '设备资料',
},
),
migrations.CreateModel(
name='Customer',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=100, verbose_name='客户名称')),
('mobile', models.CharField(max_length=20, verbose_name='手机')),
('area', models.CharField(blank=True, max_length=100, null=True, verbose_name='地区')),
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='电子邮箱')),
('contact', models.CharField(blank=True, null=True, verbose_name='联系人')),
('description', models.TextField(blank=True, null=True, verbose_name='备注描述')),
('merchant', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='customers', to='basic_info.merchant', verbose_name='所属商户')),
],
options={
'verbose_name': '客户资料',
'verbose_name_plural': '客户资料',
},
),
migrations.CreateModel(
name='BankAccount',
fields=[
@@ -22,6 +102,7 @@ class Migration(migrations.Migration):
('auto_number', models.CharField(max_length=50, verbose_name='自动编号')),
('description', models.TextField(blank=True, null=True, verbose_name='备注描述')),
('attachment', models.FileField(blank=True, null=True, upload_to='bank_accounts/', verbose_name='附件')),
('merchant', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='bank_accounts', to='basic_info.merchant', verbose_name='所属商户')),
],
options={
'verbose_name': '银行账户',
@@ -37,6 +118,7 @@ class Migration(migrations.Migration):
('name', models.CharField(max_length=100, verbose_name='类别名称')),
('description', models.TextField(blank=True, null=True, verbose_name='类别描述')),
('product_prefix', models.CharField(max_length=5, verbose_name='产品编号前缀')),
('merchant', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='product_categories', to='basic_info.merchant', verbose_name='所属商户')),
],
options={
'verbose_name': '产品类别',
@@ -44,17 +126,28 @@ class Migration(migrations.Migration):
},
),
migrations.CreateModel(
name='ProductUnit',
name='Product',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=20, verbose_name='单位名称')),
('name', models.CharField(max_length=100, verbose_name='产品名')),
('human_id', models.CharField(blank=True, max_length=100, verbose_name='产品编号')),
('color', models.CharField(blank=True, max_length=50, null=True, verbose_name='颜色')),
('width_size', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='宽幅')),
('single_price_in', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='参考进价')),
('single_price_out', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='参考售价')),
('unit', models.IntegerField(choices=[(1, ''), (2, ''), (3, '公斤')], default=1, verbose_name='单位')),
('spec', models.CharField(blank=True, max_length=100, null=True, verbose_name='规格')),
('description', models.TextField(blank=True, null=True, verbose_name='备注描述')),
('image', models.ImageField(blank=True, null=True, upload_to='product_images/', verbose_name='产品图片')),
('transform_rate', models.DecimalField(blank=True, decimal_places=4, max_digits=10, null=True, verbose_name='公斤转换率')),
('merchant', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='products', to='basic_info.merchant', verbose_name='所属商户')),
('category', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='products', to='basic_info.productcategory', verbose_name='产品类别')),
],
options={
'verbose_name': '产品单位',
'verbose_name_plural': '产品单位',
'verbose_name': '产品资料',
'verbose_name_plural': '产品资料',
},
),
migrations.CreateModel(
@@ -69,12 +162,46 @@ class Migration(migrations.Migration):
('mobile', models.CharField(blank=True, max_length=20, null=True, verbose_name='手机')),
('contact', models.TextField(blank=True, null=True, verbose_name='联系人')),
('description', models.TextField(blank=True, null=True, verbose_name='备注描述')),
('merchant', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='suppliers', to='basic_info.merchant', verbose_name='所属商户')),
],
options={
'verbose_name': '供应商',
'verbose_name_plural': '供应商',
},
),
migrations.CreateModel(
name='VehicleType',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=50, verbose_name='车辆类型名称')),
('description', models.TextField(blank=True, null=True, verbose_name='备注描述')),
('merchant', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='vehicle_types', to='basic_info.merchant', verbose_name='所属商户')),
],
options={
'verbose_name': '车辆类型',
'verbose_name_plural': '车辆类型',
},
),
migrations.CreateModel(
name='VehicleTransportRecord',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('driver_name', models.CharField(max_length=100, verbose_name='司机姓名')),
('contact_number', models.CharField(blank=True, max_length=20, null=True, verbose_name='联系电话')),
('license_plate', models.CharField(blank=True, max_length=20, null=True, verbose_name='车牌号码')),
('delivery_date', models.DateField(verbose_name='送货日期')),
('merchant', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='vehicle_transport_records', to='basic_info.merchant', verbose_name='所属商户')),
('vehicle_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='vehicles', to='basic_info.vehicletype', verbose_name='车次')),
],
options={
'verbose_name': '司机车次',
'verbose_name_plural': '司机车次',
},
),
migrations.CreateModel(
name='WareHouse',
fields=[
@@ -87,47 +214,11 @@ class Migration(migrations.Migration):
('mobile', models.CharField(blank=True, max_length=20, null=True, verbose_name='手机')),
('area', models.CharField(blank=True, max_length=100, null=True, verbose_name='地区')),
('description', models.TextField(blank=True, null=True, verbose_name='备注描述')),
('merchant', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='warehouses', to='basic_info.merchant', verbose_name='所属商户')),
],
options={
'verbose_name': '仓库资料',
'verbose_name_plural': '仓库资料',
},
),
migrations.CreateModel(
name='Product',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=100, verbose_name='产品名')),
('human_id', models.CharField(blank=True, max_length=100, verbose_name='产品编号')),
('color', models.CharField(blank=True, max_length=50, null=True, verbose_name='颜色')),
('width_size', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='宽幅')),
('single_price', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='单价')),
('spec', models.CharField(blank=True, max_length=100, null=True, verbose_name='规格')),
('description', models.TextField(blank=True, null=True, verbose_name='备注描述')),
('category', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='products', to='basic_info.productcategory', verbose_name='产品类别')),
('unit', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='products', to='basic_info.productunit', verbose_name='单位')),
],
options={
'verbose_name': '产品资料',
'verbose_name_plural': '产品资料',
},
),
migrations.CreateModel(
name='PurchaseOrder',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('order_date', models.DateField(verbose_name='订单日期')),
('total_amount', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='总金额')),
('remarks', models.TextField(blank=True, null=True, verbose_name='备注')),
('supplier', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='purchase_orders', to='basic_info.supplier', verbose_name='供应商')),
],
options={
'verbose_name': '采购单',
'verbose_name_plural': '采购单',
},
),
]

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

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):
"""商户类型枚举"""
STORE = 1, '布行'
FACTORY = 2, '印花厂'
class ProductUnitEnum(models.IntegerChoices):
"""产品单位枚举"""
METER = 1, ''
@@ -54,6 +62,21 @@ class DeviceTypeEnum(models.TextChoices):
# ==================== 模型定义 ====================
class QuickInput(ModelBase):
id = models.BigAutoField(primary_key=True)
name = models.CharField(max_length=100, verbose_name='')
value = models.CharField(max_length=200, verbose_name='')
group = models.CharField(verbose_name='')
def __str__(self):
return f'{self.group}-{self.name}'
class Meta:
verbose_name = '快捷输入'
verbose_name_plural = '快捷输入'
unique_together = ('name', 'group')
class Merchant(ModelBase):
id = models.BigAutoField(primary_key=True)
name = models.CharField(max_length=100, verbose_name='商户名称')
@@ -62,6 +85,10 @@ class Merchant(ModelBase):
email = models.EmailField(blank=True, null=True, verbose_name='电子邮箱')
mobile = models.CharField(max_length=20, blank=True, null=True, verbose_name='手机')
contact = models.TextField(blank=True, null=True, verbose_name='联系人')
auto_complete_stock_change = models.BooleanField(
default=False,
verbose_name='自动确认库存变动',
)
description = models.TextField(blank=True, null=True, verbose_name='备注描述')
def __str__(self):
@@ -74,6 +101,7 @@ class Merchant(ModelBase):
class VehicleType(ModelBase):
id = models.BigAutoField(primary_key=True)
merchant = models.ForeignKey('Merchant', on_delete=models.PROTECT, related_name='vehicle_types', verbose_name='所属商户')
name = models.CharField(max_length=50, verbose_name='车辆类型名称')
description = models.TextField(blank=True, null=True, verbose_name='备注描述')
@@ -87,8 +115,14 @@ class VehicleType(ModelBase):
class WareHouse(ModelBase):
id = models.BigAutoField(primary_key=True)
merchant = models.ForeignKey('Merchant', on_delete=models.PROTECT, related_name='warehouses', verbose_name='所属商户')
name = models.CharField(max_length=100, verbose_name='仓库名称')
location = models.CharField(max_length=200, blank=True, null=True, verbose_name='仓库地址')
mode = models.IntegerField(
choices=WareHouseModeEnum.choices,
default=WareHouseModeEnum.RESTRICT_IN,
verbose_name='出入库模式',
)
contact = models.CharField(max_length=100, blank=True, null=True, verbose_name='联系人')
mobile = models.CharField(max_length=20, blank=True, null=True, verbose_name='手机')
area = models.CharField(max_length=100, blank=True, null=True, verbose_name='地区')
@@ -104,6 +138,7 @@ class WareHouse(ModelBase):
class Supplier(ModelBase):
id = models.BigAutoField(primary_key=True)
merchant = models.ForeignKey('Merchant', on_delete=models.PROTECT, related_name='suppliers', verbose_name='所属商户')
name = models.CharField(max_length=100, verbose_name='供应商名称')
area = models.CharField(max_length=100, blank=True, null=True, verbose_name='地区')
email = models.EmailField(blank=True, null=True, verbose_name='电子邮箱')
@@ -121,6 +156,7 @@ class Supplier(ModelBase):
class BankAccount(ModelBase):
id = models.BigAutoField(primary_key=True)
merchant = models.ForeignKey('Merchant', on_delete=models.PROTECT, related_name='bank_accounts', verbose_name='所属商户')
name = models.CharField(max_length=100, verbose_name='名称')
auto_number = models.CharField(max_length=50, verbose_name='自动编号')
description = models.TextField(blank=True, null=True, verbose_name='备注描述')
@@ -136,6 +172,7 @@ class BankAccount(ModelBase):
class ProductCategory(ModelBase):
id = models.BigAutoField(primary_key=True)
merchant = models.ForeignKey('Merchant', on_delete=models.PROTECT, related_name='product_categories', verbose_name='所属商户')
name = models.CharField(max_length=100, verbose_name='类别名称')
description = models.TextField(blank=True, null=True, verbose_name='类别描述')
product_prefix = models.CharField(max_length=5, verbose_name='产品编号前缀')
@@ -150,6 +187,7 @@ class ProductCategory(ModelBase):
class Product(ModelBase):
id = models.BigAutoField(primary_key=True)
merchant = models.ForeignKey('Merchant', on_delete=models.PROTECT, verbose_name='所属商户')
category = models.ForeignKey(
ProductCategory,
on_delete=models.PROTECT,
@@ -231,7 +269,21 @@ class Product(ModelBase):
class Customer(ModelBase):
id = models.BigAutoField(primary_key=True)
merchant = models.ForeignKey('Merchant', on_delete=models.PROTECT, related_name='customers', verbose_name='所属商户')
name = models.CharField(max_length=100, verbose_name='客户名称')
visible_employees = models.ManyToManyField(
'Employee',
blank=True,
related_name='visible_customers',
verbose_name='可见该客户的员工',
)
created_by = models.ForeignKey(
'Employee',
on_delete=models.PROTECT,
related_name='created_customers',
null=True,
verbose_name='创建者',
)
mobile = models.CharField(max_length=20, verbose_name='手机')
area = models.CharField(max_length=100, blank=True, null=True, verbose_name='地区')
email = models.EmailField(blank=True, null=True, verbose_name='电子邮箱')
@@ -248,6 +300,7 @@ class Customer(ModelBase):
class Employee(ModelBase):
id = models.BigAutoField(primary_key=True)
merchant = models.ForeignKey('Merchant', on_delete=models.PROTECT, related_name='employees', verbose_name='所属商户')
sys_user = models.OneToOneField(
User,
on_delete=models.PROTECT,
@@ -283,6 +336,7 @@ class Employee(ModelBase):
class DeviceInfo(ModelBase):
id = models.BigAutoField(primary_key=True)
merchant = models.ForeignKey('Merchant', on_delete=models.PROTECT, related_name='devices', verbose_name='所属商户')
name = models.CharField(max_length=100, verbose_name='设备名称')
type = models.CharField(
max_length=20,
@@ -311,6 +365,7 @@ class DeviceInfo(ModelBase):
class VehicleTransportRecord(ModelBase):
id = models.BigAutoField(primary_key=True)
merchant = models.ForeignKey('Merchant', on_delete=models.PROTECT, related_name='vehicle_transport_records', verbose_name='所属商户')
vehicle_type = models.ForeignKey(
VehicleType,
on_delete=models.PROTECT,

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:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: myapp
POSTGRES_DB: flower
ports:
- "5432:5432"
volumes:

View File

@@ -49,6 +49,28 @@ DEBUG = env('DEBUG', default=False)
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=[])
# CORS 配置
CORS_ALLOW_ALL_ORIGINS = DEBUG # 开发环境允许所有源,生产环境需要配置白名单
CORS_ALLOWED_ORIGINS = env.list('CORS_ALLOWED_ORIGINS', default=[
'http://localhost:3000',
'http://localhost:5173',
'http://127.0.0.1:3000',
'http://127.0.0.1:5173',
])
CORS_ALLOW_CREDENTIALS = True # 允许携带凭证(如 Cookie、认证头
CORS_ALLOW_HEADERS = [
'accept',
'accept-encoding',
'authorization',
'content-type',
'dnt',
'origin',
'user-agent',
'x-csrftoken',
'x-requested-with',
]
# Application definition
INSTALLED_APPS = [
@@ -59,6 +81,7 @@ INSTALLED_APPS = [
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles', # 使用七牛云存储不需要staticfiles app
'corsheaders', # CORS 支持
'rest_framework',
'drf_spectacular',
'basic_info',
@@ -70,6 +93,7 @@ INSTALLED_APPS = [
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware', # CORS 中间件,必须放在 CommonMiddleware 之前
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',

View File

@@ -17,16 +17,37 @@ Including another URLconf
from django.contrib import admin
from django.urls import path, include
from stock.views import router
from rest_framework.response import Response
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
from sse.views import create_sse_event, push_sse_event
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
# TokenRefreshView,
)
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
class CustomTokenObtainPairView(TokenObtainPairView):
"""自定义登录视图"""
def post(self, request, *args, **kwargs):
try:
resp = super().post(request, *args, **kwargs)
if resp.status_code == 200:
srz = self.get_serializer(data=request.data)
srz.is_valid()
user = srz.user
if not hasattr(user, 'employee'):
# 非员工用户,直接返回登录失败
return Response({'detail': '无绑定的员工身份'}, status=401)
return resp
except Exception as e:
return Response({'detail': '无法登录'}, status=400)
urlpatterns = [
# JWT 登录
path('api/auth/login/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api/auth/login/', CustomTokenObtainPairView.as_view(), name='token_obtain_pair'),
# path('api/auth/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
# API 文档
@@ -37,4 +58,6 @@ urlpatterns = [
path('stock/', router.urls),
path('api/v1/', include('api_v1.urls')),
path('api/backend/', include('api_man.urls')),
path('sse/', create_sse_event, name='sse_event'),
path('sse/push/', push_sse_event, name='push_sse_event'),
]

View File

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

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
class StockAdminBase(admin.ModelAdmin):
"""库存管理基础类,定义通用配置"""
def get_list_filter(self, request):
result = super().get_list_filter(request)
result = list(result) + ['created_at']
return result
@admin.register(models.PurchaseOrder)
class PurchaseOrderAdmin(admin.ModelAdmin):
list_display = ('id', 'supplier', 'order_date', 'total_amount')
@@ -13,7 +21,7 @@ class PurchaseOrderAdmin(admin.ModelAdmin):
@admin.register(models.Inventory)
class InventoryAdmin(admin.ModelAdmin):
class InventoryAdmin(StockAdminBase):
list_display = (
'id',
'product',
@@ -22,12 +30,20 @@ class InventoryAdmin(admin.ModelAdmin):
'quantity',
'num_of_rolls',
'minimum_quantity',
'frozen_quantity',
'updated_at',
)
search_fields = ('product__name', 'warehouse__name')
list_filter = ('warehouse',)
ordering = ('-updated_at',)
@admin.display(description='冻结中数量')
def frozen_quantity(self, obj):
return services.get_frozen_stock_count(
product_id=obj.product_id,
warehouse_id=obj.warehouse_id
)
@admin.display(description='颜色')
def product_color(self, obj):
return obj.product.color
@@ -45,7 +61,7 @@ class StockSnapshotAdmin(admin.ModelAdmin):
'stock_change_record',
'created_at',
)
search_fields = ('product__name', 'warehouse__name', 'stock_change_record__id')
search_fields = ('product', 'warehouse', 'stock_change_record__id')
list_filter = ('stock_change_record__type',)
ordering = ('-created_at', )
@@ -73,12 +89,17 @@ class StockChangeDetailAdmin(admin.ModelAdmin):
'stock_change_record',
'product',
'quantity',
'direction',
'unit',
)
search_fields = ('stock_change_record__id', 'product__name')
list_filter = ('stock_change_record', 'stock_change_record__type')
ordering = ('-created_at',)
@admin.display(description='出入方向')
def direction(self, obj):
return '入库' if obj.stock_change_record.is_incoming else '出库'
@admin.register(models.StockChangeRecord)
class StockChangeRecordAdmin(admin.ModelAdmin):
@@ -132,3 +153,28 @@ class StockChangeRecordAdmin(admin.ModelAdmin):
return mark_safe('<br>'.join(detail_list))
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
from django.conf import settings
from django.db import migrations, models
@@ -9,30 +10,111 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('basic_info', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='ProductCategory',
name='PurchaseOrder',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, verbose_name='类别名称')),
('description', models.TextField(blank=True, null=True, verbose_name='类别描述')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('order_date', models.DateField(verbose_name='订单日期')),
('total_amount', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='总金额')),
('remarks', models.TextField(blank=True, null=True, verbose_name='备注')),
('merchant', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='purchase_orders', to='basic_info.merchant', verbose_name='所属商户')),
('supplier', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='purchase_orders', to='basic_info.supplier', verbose_name='供应商')),
],
options={
'verbose_name': '采购单',
'verbose_name_plural': '采购单',
},
),
migrations.CreateModel(
name='Product',
name='StockChangeRecord',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, verbose_name='产品名')),
('human_id', models.CharField(max_length=100, verbose_name='产品编号')),
('color', models.CharField(blank=True, max_length=50, null=True, verbose_name='颜色')),
('width_size', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='宽幅')),
('single_price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='单价')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('type', models.IntegerField(choices=[(1, '入库'), (2, '出库')], verbose_name='变动类型')),
('source_type', models.IntegerField(choices=[(1, '采购'), (2, '销退'), (3, '调入'), (4, '盘盈'), (5, '销售'), (6, '采购退货'), (7, '调出'), (8, '盘亏')], verbose_name='变动来源')),
('source_id', models.BigIntegerField(blank=True, null=True, verbose_name='来源单据ID')),
('is_finished', models.BooleanField(default=False, verbose_name='完成出入库')),
('finished_at', models.DateTimeField(blank=True, null=True, verbose_name='完成时间')),
('remarks', models.TextField(blank=True, null=True, verbose_name='备注')),
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='操作人')),
('merchant', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='stock_change_records', to='basic_info.merchant', verbose_name='所属商户')),
('warehouse', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='stock_change_records', to='basic_info.warehouse', verbose_name='仓库')),
],
options={
'verbose_name': '出入库记录',
'verbose_name_plural': '出入库记录',
},
),
migrations.CreateModel(
name='StockChangeDetail',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('quantity', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='数量')),
('unit', models.IntegerField(choices=[(1, ''), (2, ''), (3, '公斤')], verbose_name='单位')),
('merchant', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='stock_change_details', to='basic_info.merchant', verbose_name='所属商户')),
('product', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='stock_change_details', to='basic_info.product', verbose_name='产品')),
('stock_change_record', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='details', to='stock.stockchangerecord', verbose_name='库存变动记录')),
],
options={
'verbose_name': '库存变动明细',
'verbose_name_plural': '库存变动明细',
},
),
migrations.CreateModel(
name='StockSnapshot',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('delta', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='库存变动数量')),
('quantity_before', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='变动前库存数量')),
('quantity_after', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='变动后库存数量')),
('unit', models.IntegerField(choices=[(1, ''), (2, ''), (3, '公斤')], verbose_name='单位')),
('num_of_rolls', models.IntegerField(default=1, verbose_name='匹数')),
('offset_to', models.BigIntegerField(blank=True, null=True, verbose_name='冲抵目标ID')),
('offset_at', models.DateTimeField(blank=True, null=True, verbose_name='冲抵时间')),
('cancelled', models.BooleanField(default=False, verbose_name='已取消')),
('cancelled_at', models.DateTimeField(blank=True, null=True, verbose_name='取消时间')),
('offset_id', models.BigIntegerField(blank=True, null=True, verbose_name='冲抵记录ID')),
('merchant', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='stock_snapshots', to='basic_info.merchant', verbose_name='所属商户')),
('product', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='stock_snapshots', to='basic_info.product', verbose_name='产品')),
('stock_change_record', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='stock_snapshots', to='stock.stockchangerecord', verbose_name='关联的库存变动记录')),
('warehouse', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='stock_snapshots', to='basic_info.warehouse', verbose_name='仓库')),
],
options={
'verbose_name': '库存快照',
'verbose_name_plural': '库存快照',
},
),
migrations.CreateModel(
name='Inventory',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('quantity', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='库存数量')),
('num_of_rolls', models.IntegerField(default=1, verbose_name='匹数')),
('minimum_quantity', models.IntegerField(blank=True, null=True, verbose_name='最低库存量')),
('spec', models.CharField(blank=True, max_length=100, null=True, verbose_name='规格')),
('description', models.TextField(blank=True, null=True, verbose_name='备注描述')),
('unit', models.CharField(default='', max_length=20, verbose_name='单位')),
('category', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='products', to='stock.productcategory', verbose_name='产品类别')),
('merchant', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='inventories', to='basic_info.merchant', verbose_name='所属商户')),
('product', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='inventories', to='basic_info.product', verbose_name='产品')),
('warehouse', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='inventories', to='basic_info.warehouse', verbose_name='仓库')),
],
options={
'verbose_name': '库存',
'verbose_name_plural': '库存',
'unique_together': {('product', 'warehouse')},
},
),
]

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):
"""库存变动来源类型"""
@@ -35,6 +42,12 @@ class PurchaseOrder(ModelBase):
"""采购单模型"""
id = models.BigAutoField(primary_key=True)
merchant = models.ForeignKey(
basic_info_models.Merchant,
on_delete=models.PROTECT,
related_name='purchase_orders',
verbose_name='所属商户'
)
supplier = models.ForeignKey(basic_info_models.Supplier, on_delete=models.PROTECT, related_name='purchase_orders', verbose_name='供应商')
order_date = models.DateField(verbose_name='订单日期')
total_amount = models.DecimalField(max_digits=15, decimal_places=2, verbose_name='总金额')
@@ -52,6 +65,12 @@ class StockChangeRecord(ModelBase):
"""库存变动记录模型"""
id = models.BigAutoField(primary_key=True)
merchant = models.ForeignKey(
basic_info_models.Merchant,
on_delete=models.PROTECT,
related_name='stock_change_records',
verbose_name='所属商户'
)
type = models.IntegerField(choices=StockChangeTypeEnum.choices, verbose_name='变动类型')
warehouse = models.ForeignKey(
basic_info_models.WareHouse,
@@ -127,6 +146,12 @@ class StockChangeDetail(ModelBase):
"""库存变动明细模型"""
id = models.BigAutoField(primary_key=True)
merchant = models.ForeignKey(
basic_info_models.Merchant,
on_delete=models.PROTECT,
related_name='stock_change_details',
verbose_name='所属商户'
)
product = models.ForeignKey(
basic_info_models.Product,
on_delete=models.PROTECT,
@@ -161,6 +186,12 @@ class Inventory(ModelBase):
"""库存模型"""
id = models.BigAutoField(primary_key=True)
merchant = models.ForeignKey(
basic_info_models.Merchant,
on_delete=models.PROTECT,
related_name='inventories',
verbose_name='所属商户'
)
product = models.ForeignKey(
basic_info_models.Product,
on_delete=models.PROTECT,
@@ -192,6 +223,12 @@ class StockSnapshot(ModelBase):
"""库存变动快照模型"""
id = models.BigAutoField(primary_key=True)
merchant = models.ForeignKey(
basic_info_models.Merchant,
on_delete=models.PROTECT,
related_name='stock_snapshots',
verbose_name='所属商户'
)
product = models.ForeignKey(
basic_info_models.Product,
on_delete=models.PROTECT,
@@ -252,3 +289,76 @@ class StockSnapshot(ModelBase):
class Meta:
verbose_name = '库存快照'
verbose_name_plural = '库存快照'
class StockFreeze(ModelBase):
"""库存冻结模型"""
id = models.BigAutoField(primary_key=True)
merchant = models.ForeignKey(
basic_info_models.Merchant,
on_delete=models.PROTECT,
related_name='stock_freezes',
verbose_name='所属商户'
)
product = models.ForeignKey(
basic_info_models.Product,
on_delete=models.PROTECT,
related_name='stock_freezes',
verbose_name='产品',
)
warehouse = models.ForeignKey(
basic_info_models.WareHouse,
on_delete=models.PROTECT,
related_name='stock_freezes',
verbose_name='仓库',
)
stock_detail = models.ForeignKey(
StockChangeDetail,
on_delete=models.PROTECT,
related_name='stock_freezes',
verbose_name='关联的库存变动明细',
)
quantity = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='冻结数量')
unit = models.IntegerField(
verbose_name='单位',
choices=basic_info_models.ProductUnitEnum.choices,
)
status = models.IntegerField(
choices=StockFreezeStatusEnum.choices,
default=StockFreezeStatusEnum.FROZEN,
verbose_name='冻结状态',
)
frozen_by = models.ForeignKey(
'auth.User',
on_delete=models.PROTECT,
verbose_name='冻结操作人',
)
frozen_with = models.BigIntegerField(null=True, blank=True, verbose_name='冻结关联ID')
cancelled_by = models.ForeignKey(
'auth.User',
on_delete=models.PROTECT,
null=True,
blank=True,
related_name='cancelled_stock_freezes',
verbose_name='取消操作人',
)
completed_by = models.ForeignKey(
'auth.User',
on_delete=models.PROTECT,
null=True,
blank=True,
related_name='completed_stock_freezes',
verbose_name='成单操作人',
)
completed_at = models.DateTimeField(null=True, blank=True, verbose_name='成单时间')
completed_with = models.BigIntegerField(null=True, blank=True, verbose_name='成单关联ID')
cancelled_at = models.DateTimeField(null=True, blank=True, verbose_name='取消时间')
reason = models.TextField(blank=True, null=True, verbose_name='冻结原因')
def __str__(self):
return f'库存冻结 {self.id} - 产品: {self.product.name}'
class Meta:
verbose_name = '库存冻结'
verbose_name_plural = '库存冻结'

View File

@@ -1,4 +1,5 @@
from . import models
from django.db.models import Sum
from django.utils import timezone
import logging
@@ -30,6 +31,7 @@ def create_stock_snapshot(stock_change_detail: models.StockChangeDetail, invento
# 必须在库存变动发生后创建快照
snapshot = models.StockSnapshot(
merchant_id=stock_change_detail.merchant_id,
product_id=inventory.product_id,
warehouse_id=inventory.warehouse_id,
delta=delta,
@@ -92,6 +94,7 @@ def make_stock_change_completed(stock_change_record: models.StockChangeRecord) -
else:
# 创建新库存记录
inventory_record = models.Inventory(
merchant_id=stock_change_record.merchant_id,
product_id=detail.product_id,
warehouse_id=stock_change_record.warehouse_id,
quantity=detail.quantity * positive,
@@ -114,3 +117,13 @@ def make_stock_change_completed(stock_change_record: models.StockChangeRecord) -
f'库存变动记录 {stock_change_record.id} 标记为已完成。'
)
return True
def get_frozen_stock_count(product_id: int, warehouse_id: int) -> int:
"""获取指定产品在指定仓库的冻结库存数量"""
total_frozen = models.StockFreeze.objects.filter(
product_id=product_id,
warehouse_id=warehouse_id,
status=models.StockFreezeStatusEnum.FROZEN
).aggregate(total=Sum('quantity'))['total'] or 0
return total_frozen

View File

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

17
uv.lock generated
View File

@@ -78,10 +78,17 @@ wheels = [
]
[[package]]
name = "django-cors"
version = "0.1"
name = "django-cors-headers"
version = "4.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b6/94/59b5353380df1aacd28e1efc1e77ce9925f65ef9adab6c44238daa9cefa3/django-cors-0.1.tar.gz", hash = "sha256:ba31c9455ddb848e578b26bac3634049951149775b818a35476d2b50350640c3", size = 1066, upload-time = "2012-12-04T02:52:05.092Z" }
dependencies = [
{ name = "asgiref" },
{ name = "django" },
]
sdist = { url = "https://files.pythonhosted.org/packages/21/39/55822b15b7ec87410f34cd16ce04065ff390e50f9e29f31d6d116fc80456/django_cors_headers-4.9.0.tar.gz", hash = "sha256:fe5d7cb59fdc2c8c646ce84b727ac2bca8912a247e6e68e1fb507372178e59e8", size = 21458, upload-time = "2025-09-18T10:40:52.326Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/30/d8/19ed1e47badf477d17fb177c1c19b5a21da0fd2d9f093f23be3fb86c5fab/django_cors_headers-4.9.0-py3-none-any.whl", hash = "sha256:15c7f20727f90044dcee2216a9fd7303741a864865f0c3657e28b7056f61b449", size = 12809, upload-time = "2025-09-18T10:40:50.843Z" },
]
[[package]]
name = "django-environ"
@@ -186,7 +193,7 @@ version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "django" },
{ name = "django-cors" },
{ name = "django-cors-headers" },
{ name = "django-environ" },
{ name = "django-filter" },
{ name = "django-ninja" },
@@ -207,7 +214,7 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "django", specifier = ">=5.2.7" },
{ name = "django-cors", specifier = ">=0.1" },
{ name = "django-cors-headers", specifier = ">=4.9.0" },
{ name = "django-environ", specifier = ">=0.12.0" },
{ name = "django-filter", specifier = ">=25.2" },
{ name = "django-ninja", specifier = ">=1.4.5" },