1
0
forked from erp-dev/erp

fix: added merchant_id in models of shipment module

This commit is contained in:
2026-01-14 19:43:29 +08:00
parent 8846446ecf
commit 9a367caab7
11 changed files with 558 additions and 10 deletions

View File

@@ -25,7 +25,7 @@ from .views.products import ProductQuickViewSet
from .views.parameters import StateParameterViewSet from .views.parameters import StateParameterViewSet
from .views.users import CreateUserWithProfileView from .views.users import CreateUserWithProfileView
from .views.mingdaoyun import MDYPlateOrderStagingViewSet from .views.mingdaoyun import MDYPlateOrderStagingViewSet
from .views.shipment import SalesItemByPrintingOrderView, ShipmentCreateView from .views.shipment import SalesItemByPrintingOrderView, ShipmentCreateView, ShipmentExternalCreateView
# 创建 DRF Router for Stateflow # 创建 DRF Router for Stateflow
stateflow_router = DefaultRouter() stateflow_router = DefaultRouter()
@@ -105,6 +105,11 @@ urlpatterns = [
ShipmentCreateView.as_view(), ShipmentCreateView.as_view(),
name='shipment_create' name='shipment_create'
), ),
path(
'shipment/shipments/external/',
ShipmentExternalCreateView.as_view(),
name='shipment_create_external'
),
path( path(
'shipment/sales-items/by-printing-order/<int:printing_order_id>/', 'shipment/sales-items/by-printing-order/<int:printing_order_id>/',
SalesItemByPrintingOrderView.as_view(), SalesItemByPrintingOrderView.as_view(),

View File

@@ -3,6 +3,6 @@ Shipment API 模块
提供出货单和销售品相关的 API 接口 提供出货单和销售品相关的 API 接口
""" """
from .views import SalesItemByPrintingOrderView, ShipmentCreateView from .views import SalesItemByPrintingOrderView, ShipmentCreateView, ShipmentExternalCreateView
__all__ = ['SalesItemByPrintingOrderView', 'ShipmentCreateView'] __all__ = ['SalesItemByPrintingOrderView', 'ShipmentCreateView', 'ShipmentExternalCreateView']

View File

@@ -14,12 +14,23 @@ class ShipmentSerializer(serializers.ModelSerializer):
created_by_id = serializers.IntegerField(source='created_by.id', read_only=True, allow_null=True) created_by_id = serializers.IntegerField(source='created_by.id', read_only=True, allow_null=True)
created_by_name = serializers.SerializerMethodField() created_by_name = serializers.SerializerMethodField()
items_count = serializers.SerializerMethodField() items_count = serializers.SerializerMethodField()
cancelled_by_id = serializers.IntegerField(source='cancelled_by.id', read_only=True, allow_null=True)
cancelled_by_name = serializers.SerializerMethodField()
status_display = serializers.CharField(source='get_status_display', read_only=True)
external_finished_products_count = serializers.SerializerMethodField()
merchant_id = serializers.IntegerField(source='merchant.id', read_only=True)
merchant_name = serializers.CharField(source='merchant.name', read_only=True)
class Meta: class Meta:
model = Shipment model = Shipment
fields = [ fields = [
'id', 'customer', 'customer_name', 'shipment_date', 'remark', 'id', 'merchant_id', 'merchant_name',
'customer', 'customer_name', 'shipment_date', 'remark',
'status', 'status_display',
'external_id',
'cancelled_at', 'cancelled_by_id', 'cancelled_by_name',
'items_count', 'created_by_id', 'created_by_name', 'items_count', 'created_by_id', 'created_by_name',
'external_finished_products_count',
'created_at', 'updated_at' 'created_at', 'updated_at'
] ]
read_only_fields = ['id', 'created_at', 'updated_at'] read_only_fields = ['id', 'created_at', 'updated_at']
@@ -35,10 +46,21 @@ class ShipmentSerializer(serializers.ModelSerializer):
def get_items_count(self, obj): def get_items_count(self, obj):
return obj.items.count() return obj.items.count()
def get_cancelled_by_name(self, obj):
if obj.cancelled_by:
employee = getattr(obj.cancelled_by, 'employee', None)
if employee:
return employee.name
return obj.cancelled_by.username
return None
class ShipmentCreateSerializer(serializers.Serializer): def get_external_finished_products_count(self, obj):
return obj.external_finished_products.count()
class ShipmentCreateNormalSerializer(serializers.Serializer):
""" """
出货单创建序列化器 出货单创建序列化器(普通版)
""" """
customer = serializers.IntegerField(help_text='客户ID') customer = serializers.IntegerField(help_text='客户ID')
shipment_date = serializers.DateField(help_text='出货日期') shipment_date = serializers.DateField(help_text='出货日期')
@@ -54,6 +76,42 @@ class ShipmentCreateSerializer(serializers.Serializer):
# 去重 # 去重
return list(set(value)) if value else [] return list(set(value)) if value else []
class ExternalFinishedProductInputSerializer(serializers.Serializer):
"""外部成品表写入结构external create 专用)"""
style_name = serializers.CharField(max_length=200)
num_of_rolls = serializers.IntegerField(min_value=0)
class ShipmentCreateExternalSerializer(serializers.Serializer):
"""
出货单创建序列化器external 版)
特点:
- 不绑定任何 SalesItem
- 必须提供 external_id
- 同时写入 ExternalFinishedProduct 列表并关联到 Shipment
"""
customer = serializers.IntegerField(help_text='客户ID')
shipment_date = serializers.DateField(help_text='出货日期')
remark = serializers.CharField(required=False, default='', allow_blank=True, help_text='备注')
external_id = serializers.CharField(max_length=120, help_text='外部订单号(必填)')
external_finished_products = serializers.ListField(
child=ExternalFinishedProductInputSerializer(),
required=True,
help_text='外部成品表结构数组(必填)'
)
def validate_external_id(self, value):
value = (value or '').strip()
if not value:
raise serializers.ValidationError('external_id 不能为空')
return value
def validate_external_finished_products(self, value):
if not value:
raise serializers.ValidationError('external_finished_products 不能为空')
return value
class SalesItemSerializer(serializers.Serializer): class SalesItemSerializer(serializers.Serializer):
""" """

View File

@@ -106,6 +106,7 @@ class SalesItemByPrintingOrderAPITestCase(TestCase):
# 创建出货单 # 创建出货单
self.shipment = shipment_models.Shipment.objects.create( self.shipment = shipment_models.Shipment.objects.create(
merchant=self.merchant,
customer=self.customer, customer=self.customer,
shipment_date='2026-01-14', shipment_date='2026-01-14',
created_by=self.user, created_by=self.user,
@@ -113,6 +114,7 @@ class SalesItemByPrintingOrderAPITestCase(TestCase):
# 创建销售品 - 未关联出货单 # 创建销售品 - 未关联出货单
self.sales_item1 = shipment_models.SalesItem.objects.create( self.sales_item1 = shipment_models.SalesItem.objects.create(
merchant=self.merchant,
name='销售品1', name='销售品1',
quantity=Decimal('50.00'), quantity=Decimal('50.00'),
unit=shipment_models.UnitChoices.METER, unit=shipment_models.UnitChoices.METER,
@@ -121,6 +123,7 @@ class SalesItemByPrintingOrderAPITestCase(TestCase):
) )
self.sales_item2 = shipment_models.SalesItem.objects.create( self.sales_item2 = shipment_models.SalesItem.objects.create(
merchant=self.merchant,
name='销售品2', name='销售品2',
quantity=Decimal('30.00'), quantity=Decimal('30.00'),
unit=shipment_models.UnitChoices.METER, unit=shipment_models.UnitChoices.METER,
@@ -132,6 +135,7 @@ class SalesItemByPrintingOrderAPITestCase(TestCase):
# 创建销售品 - 已关联出货单 # 创建销售品 - 已关联出货单
self.sales_item3 = shipment_models.SalesItem.objects.create( self.sales_item3 = shipment_models.SalesItem.objects.create(
merchant=self.merchant,
name='销售品3已出货', name='销售品3已出货',
quantity=Decimal('100.00'), quantity=Decimal('100.00'),
unit=shipment_models.UnitChoices.METER, unit=shipment_models.UnitChoices.METER,
@@ -142,6 +146,7 @@ class SalesItemByPrintingOrderAPITestCase(TestCase):
# 创建与该订单无关的销售品 # 创建与该订单无关的销售品
self.sales_item_other = shipment_models.SalesItem.objects.create( self.sales_item_other = shipment_models.SalesItem.objects.create(
merchant=self.merchant,
name='其它销售品', name='其它销售品',
quantity=Decimal('999.00'), quantity=Decimal('999.00'),
unit=shipment_models.UnitChoices.PIECE, unit=shipment_models.UnitChoices.PIECE,
@@ -304,6 +309,7 @@ class ShipmentCreateAPITestCase(TestCase):
# 创建销售品(未关联出货单) # 创建销售品(未关联出货单)
self.sales_item1 = shipment_models.SalesItem.objects.create( self.sales_item1 = shipment_models.SalesItem.objects.create(
merchant=self.merchant,
name='销售品1', name='销售品1',
quantity=Decimal('50.00'), quantity=Decimal('50.00'),
unit=shipment_models.UnitChoices.METER, unit=shipment_models.UnitChoices.METER,
@@ -311,6 +317,7 @@ class ShipmentCreateAPITestCase(TestCase):
) )
self.sales_item2 = shipment_models.SalesItem.objects.create( self.sales_item2 = shipment_models.SalesItem.objects.create(
merchant=self.merchant,
name='销售品2', name='销售品2',
quantity=Decimal('30.00'), quantity=Decimal('30.00'),
unit=shipment_models.UnitChoices.METER, unit=shipment_models.UnitChoices.METER,
@@ -319,11 +326,13 @@ class ShipmentCreateAPITestCase(TestCase):
# 创建已关联出货单的销售品 # 创建已关联出货单的销售品
self.existing_shipment = shipment_models.Shipment.objects.create( self.existing_shipment = shipment_models.Shipment.objects.create(
merchant=self.merchant,
customer=self.customer, customer=self.customer,
shipment_date='2026-01-13', shipment_date='2026-01-13',
created_by=self.user, created_by=self.user,
) )
self.sales_item_shipped = shipment_models.SalesItem.objects.create( self.sales_item_shipped = shipment_models.SalesItem.objects.create(
merchant=self.merchant,
name='销售品3已出货', name='销售品3已出货',
quantity=Decimal('100.00'), quantity=Decimal('100.00'),
unit=shipment_models.UnitChoices.METER, unit=shipment_models.UnitChoices.METER,
@@ -426,3 +435,121 @@ class ShipmentCreateAPITestCase(TestCase):
response = self.client.post('/api/v1/shipment/shipments/', data, format='json') response = self.client.post('/api/v1/shipment/shipments/', data, format='json')
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
class ShipmentExternalCreateAPITestCase(TestCase):
"""测试创建出货单 external 版 API"""
def setUp(self):
self.client = APIClient()
# 创建商户
self.merchant = basic_models.Merchant.objects.create(
name='测试印花厂',
type=basic_models.MerchantTypeEnum.FACTORY
)
# 创建用户
self.user = User.objects.create_user(
username='testuser_ext',
password='testpass123',
email='test_ext@example.com'
)
# 创建员工并关联商户
self.employee = basic_models.Employee.objects.create(
sys_user=self.user,
merchant=self.merchant,
name='测试员工Ext',
mobile='13800138002',
status=basic_models.EmployeeStatusEnum.ACTIVE
)
# 创建客户(需要同 merchant
self.customer = basic_models.Customer.objects.create(
merchant=self.merchant,
name='测试客户Ext',
mobile='13900139002',
area='测试地区Ext'
)
# 创建一个销售品(用于验证 external 版不会绑定任何销售品)
self.sales_item = shipment_models.SalesItem.objects.create(
merchant=self.merchant,
name='销售品-不应被绑定',
quantity=Decimal('10.00'),
unit=shipment_models.UnitChoices.METER,
created_by=self.user,
)
self.client.force_authenticate(user=self.user)
def test_create_external_shipment_success(self):
data = {
'customer': self.customer.id,
'shipment_date': '2026-01-14',
'remark': 'external 备注',
'external_id': 'EXT-ORDER-001',
'external_finished_products': [
{'style_name': '款式A', 'num_of_rolls': 2},
{'style_name': '款式B', 'num_of_rolls': 5},
]
}
response = self.client.post('/api/v1/shipment/shipments/external/', data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
result = response.json()
self.assertEqual(result['customer'], self.customer.id)
self.assertEqual(result['external_id'], 'EXT-ORDER-001')
self.assertEqual(result['items_count'], 0)
self.assertEqual(result['external_finished_products_count'], 2)
shipment_id = result['id']
# 验证外部成品表写入并关联
self.assertEqual(
shipment_models.ExternalFinishedProduct.objects.filter(shipment_id=shipment_id).count(),
2
)
# 验证不会绑定任何销售品
self.sales_item.refresh_from_db()
self.assertIsNone(self.sales_item.shipment_id)
def test_create_external_shipment_external_id_required(self):
data = {
'customer': self.customer.id,
'shipment_date': '2026-01-14',
'external_id': ' ',
'external_finished_products': [
{'style_name': '款式A', 'num_of_rolls': 1},
]
}
response = self.client.post('/api/v1/shipment/shipments/external/', data, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn('external_id', response.json())
def test_create_external_shipment_products_required(self):
data = {
'customer': self.customer.id,
'shipment_date': '2026-01-14',
'external_id': 'EXT-ORDER-002',
'external_finished_products': []
}
response = self.client.post('/api/v1/shipment/shipments/external/', data, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn('external_finished_products', response.json())
def test_create_external_shipment_unauthenticated(self):
self.client.logout()
data = {
'customer': self.customer.id,
'shipment_date': '2026-01-14',
'external_id': 'EXT-ORDER-003',
'external_finished_products': [
{'style_name': '款式A', 'num_of_rolls': 1},
]
}
response = self.client.post('/api/v1/shipment/shipments/external/', data, format='json')
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

View File

@@ -6,7 +6,12 @@ from rest_framework.views import APIView
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from .serializers import SalesItemSerializer, ShipmentSerializer, ShipmentCreateSerializer from .serializers import (
SalesItemSerializer,
ShipmentSerializer,
ShipmentCreateNormalSerializer,
ShipmentCreateExternalSerializer,
)
class ShipmentCreateView(APIView): class ShipmentCreateView(APIView):
@@ -41,7 +46,7 @@ class ShipmentCreateView(APIView):
def post(self, request): def post(self, request):
# 验证请求数据 # 验证请求数据
serializer = ShipmentCreateSerializer(data=request.data) serializer = ShipmentCreateNormalSerializer(data=request.data)
if not serializer.is_valid(): if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -65,6 +70,43 @@ class ShipmentCreateView(APIView):
return Response(response_serializer.data, status=status.HTTP_201_CREATED) return Response(response_serializer.data, status=status.HTTP_201_CREATED)
class ShipmentExternalCreateView(APIView):
"""
创建出货单external 版)
POST /api/v1/shipment/shipments/external/
特点:
- external_id 必填
- external_finished_products 必填(数组)
- 不绑定任何销售品
"""
permission_classes = [IsAuthenticated]
def post(self, request):
serializer = ShipmentCreateExternalSerializer(data=request.data)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
data = serializer.validated_data
from shipment.services import create_external_shipment
try:
shipment = create_external_shipment(
customer_id=data['customer'],
shipment_date=data['shipment_date'],
external_id=data['external_id'],
external_finished_products=data['external_finished_products'],
created_by=request.user,
remark=data.get('remark', ''),
)
except ValueError as e:
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
response_serializer = ShipmentSerializer(shipment)
return Response(response_serializer.data, status=status.HTTP_201_CREATED)
class SalesItemByPrintingOrderView(APIView): class SalesItemByPrintingOrderView(APIView):
""" """
通过生产订单查询销售品 通过生产订单查询销售品

View File

@@ -156,6 +156,39 @@ python manage.py backfill_merchant <merchant_id>
--- ---
### 9. Shipment 创建 API普通版 & External 版
新增并完善了两个创建出货单接口:
#### 普通版(绑定销售品)
- **接口**`POST /api/v1/shipment/shipments/`
- **参数**`sales_items: [销售品ID...]`
- **行为**:创建出货单并把销售品关联到该出货单
#### External 版(不绑定销售品 + 外部成品表批量写入)
- **接口**`POST /api/v1/shipment/shipments/external/`
- **参数**
- `external_id` 必填
- `external_finished_products`(数组)必填,用于写入 `ExternalFinishedProduct` 并关联到 Shipment
- **行为**:创建出货单 + 批量创建外部成品表记录(不关联任何销售品)
#### 代码位置
- `shipment/services.py`: 新增 `create_external_shipment()`
- `api_v1/views/shipment/views.py`: 新增 `ShipmentExternalCreateView`
- `api_v1/views/shipment/serializers.py`: 新增 external 创建序列化器与响应字段扩展
- `api_v1/urls.py`: 注册 external 创建路由
- `api_v1/views/shipment/test_api.py`: 新增 external create 测试
#### merchant 强制归属(非空)
-`Shipment``SalesItem` 增加 `merchant` 外键且不允许为空
- 创建时从 `request.user.employee.merchant` 自动绑定,并校验 customer 同商户
- 普通版在关联销售品时校验销售品同商户,避免跨商户关联
#### 文档
- `docs/shipment_api.md`: 补充 external create API 文档
---
## 待办 ## 待办
--- ---

View File

@@ -44,6 +44,8 @@
```json ```json
{ {
"id": 1, "id": 1,
"merchant_id": 1,
"merchant_name": "测试印花厂",
"customer": 1, "customer": 1,
"customer_name": "客户A", "customer_name": "客户A",
"shipment_date": "2026-01-14", "shipment_date": "2026-01-14",
@@ -61,6 +63,8 @@
| 字段 | 类型 | 说明 | | 字段 | 类型 | 说明 |
|------|------|------| |------|------|------|
| id | int | 出货单ID | | id | int | 出货单ID |
| merchant_id | int | 所属商户ID从当前登录用户推导 |
| merchant_name | string | 所属商户名称(从当前登录用户推导) |
| customer | int | 客户ID | | customer | int | 客户ID |
| customer_name | string | 客户名称 | | customer_name | string | 客户名称 |
| shipment_date | string | 出货日期 | | shipment_date | string | 出货日期 |
@@ -99,6 +103,94 @@
--- ---
## 创建出货单external 版)
创建出货单但**不绑定任何销售品**,同时写入并关联“外部成品表”(用于兼容外部遗留系统)。
### 接口信息
- **URL**: `/api/v1/shipment/shipments/external/`
- **Method**: `POST`
- **认证**: 需要登录JWT Token
### 请求参数
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| customer | int | 是 | 客户ID |
| shipment_date | string | 是 | 出货日期YYYY-MM-DD |
| remark | string | 否 | 备注 |
| external_id | string | 是 | 外部订单号(长度<=120 |
| external_finished_products | array[object] | 是 | 外部成品表结构数组(至少 1 条) |
`external_finished_products` 每项结构:
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| style_name | string | 是 | 款式名称 |
| num_of_rolls | int | 是 | 卷数 |
### 请求示例
```json
{
"customer": 1,
"shipment_date": "2026-01-14",
"remark": "external 备注(可选)",
"external_id": "EXT-ORDER-001",
"external_finished_products": [
{"style_name": "款式A", "num_of_rolls": 2},
{"style_name": "款式B", "num_of_rolls": 5}
]
}
```
### 响应示例
```json
{
"id": 1,
"merchant_id": 1,
"merchant_name": "测试印花厂",
"customer": 1,
"customer_name": "客户A",
"shipment_date": "2026-01-14",
"remark": "external 备注(可选)",
"status": 1,
"status_display": "待送货",
"external_id": "EXT-ORDER-001",
"cancelled_at": null,
"cancelled_by_id": null,
"cancelled_by_name": null,
"items_count": 0,
"external_finished_products_count": 2,
"created_by_id": 1,
"created_by_name": "张三",
"created_at": "2026-01-14T10:00:00Z",
"updated_at": "2026-01-14T10:00:00Z"
}
```
### 错误响应
#### 400 Bad Request - external_id 为空
```json
{
"external_id": ["external_id 不能为空"]
}
```
#### 400 Bad Request - external_finished_products 为空
```json
{
"external_finished_products": ["external_finished_products 不能为空"]
}
```
---
## 通过生产订单查询销售品 ## 通过生产订单查询销售品
查询与指定生产订单PrintingOrder关联的所有销售品SalesItem 查询与指定生产订单PrintingOrder关联的所有销售品SalesItem

View File

@@ -114,8 +114,22 @@ def on_printing_job_process_completed(sender, **kwargs):
try: try:
with transaction.atomic(): with transaction.atomic():
# SalesItem.merchant 不允许为空,优先取 printing_job.merchant
sales_merchant = getattr(printing_job, 'merchant', None)
if not sales_merchant and getattr(printing_job, 'printing_order', None):
sales_merchant = getattr(printing_job.printing_order, 'merchant', None)
if not sales_merchant and last_completed_by and hasattr(last_completed_by, 'employee'):
sales_merchant = getattr(last_completed_by.employee, 'merchant', None)
if not sales_merchant:
logger.warning(
f'PrintingJob #{printing_job.id} 流程完成,'
f'但无法确定 merchant跳过销售品创建'
)
return
sales_item = SalesItem.objects.create( sales_item = SalesItem.objects.create(
shipment=None, # 暂不关联出货单,待后续分配 shipment=None, # 暂不关联出货单,待后续分配
merchant=sales_merchant,
name=product_name, name=product_name,
quantity=quantity, quantity=quantity,
unit=sales_unit, unit=sales_unit,

View File

@@ -0,0 +1,90 @@
from django.db import migrations, models
import django.db.models.deletion
def backfill_shipment_salesitem_merchant(apps, schema_editor):
"""
为历史数据补齐 merchant本项目当前声称未上线但迁移要可重复执行且安全
- Shipment.merchant: 从 customer.merchant 推导
- SalesItem.merchant:
- 优先从 shipment.merchant 推导
- 否则从 created_by.employee.merchant 推导(如果存在)
"""
Shipment = apps.get_model('shipment', 'Shipment')
SalesItem = apps.get_model('shipment', 'SalesItem')
# Shipment: merchant = customer.merchant
for s in Shipment.objects.filter(merchant__isnull=True).select_related('customer'):
if s.customer_id:
s.merchant_id = s.customer.merchant_id
s.save(update_fields=['merchant'])
# SalesItem: merchant = shipment.merchant else created_by.employee.merchant
# 注意:迁移态下 employee 关系可能不存在,需 try/except
for item in SalesItem.objects.filter(merchant__isnull=True).select_related('shipment', 'shipment__merchant', 'created_by'):
if item.shipment_id and getattr(item.shipment, 'merchant_id', None):
item.merchant_id = item.shipment.merchant_id
item.save(update_fields=['merchant'])
continue
user = getattr(item, 'created_by', None)
emp = getattr(user, 'employee', None) if user else None
merchant_id = getattr(emp, 'merchant_id', None) if emp else None
if merchant_id:
item.merchant_id = merchant_id
item.save(update_fields=['merchant'])
class Migration(migrations.Migration):
dependencies = [
('basic_info', '0013_alter_customer_options'),
('shipment', '0005_fix_shipment_external_finished_product_relation'),
]
operations = [
migrations.AddField(
model_name='shipment',
name='merchant',
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name='shipments',
to='basic_info.merchant',
verbose_name='所属商户',
),
),
migrations.AddField(
model_name='salesitem',
name='merchant',
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name='sales_items',
to='basic_info.merchant',
verbose_name='所属商户',
),
),
migrations.RunPython(backfill_shipment_salesitem_merchant, migrations.RunPython.noop),
migrations.AlterField(
model_name='shipment',
name='merchant',
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name='shipments',
to='basic_info.merchant',
verbose_name='所属商户',
),
),
migrations.AlterField(
model_name='salesitem',
name='merchant',
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name='sales_items',
to='basic_info.merchant',
verbose_name='所属商户',
),
),
]

View File

@@ -103,6 +103,13 @@ class Shipment(ModelBase):
help_text='用于兼容外部遗留系统的订单号(可空)', help_text='用于兼容外部遗留系统的订单号(可空)',
) )
merchant = models.ForeignKey(
basic_models.Merchant,
on_delete=models.PROTECT,
related_name='shipments',
verbose_name='所属商户',
)
customer = models.ForeignKey( customer = models.ForeignKey(
basic_models.Customer, basic_models.Customer,
on_delete=models.PROTECT, on_delete=models.PROTECT,
@@ -168,6 +175,13 @@ class SalesItem(ModelBase):
help_text='为空表示待分配' help_text='为空表示待分配'
) )
merchant = models.ForeignKey(
basic_models.Merchant,
on_delete=models.PROTECT,
related_name='sales_items',
verbose_name='所属商户',
)
name = models.CharField( name = models.CharField(
max_length=200, max_length=200,
verbose_name='名称' verbose_name='名称'

View File

@@ -8,7 +8,7 @@ from typing import List
from django.db import transaction from django.db import transaction
from django.db.models import QuerySet from django.db.models import QuerySet
from shipment.models import SalesItem, Shipment from shipment.models import ExternalFinishedProduct, SalesItem, Shipment
def get_sales_items_by_printing_order( def get_sales_items_by_printing_order(
@@ -74,6 +74,15 @@ def create_shipment(
except Customer.DoesNotExist: except Customer.DoesNotExist:
raise ValueError(f'客户 {customer_id} 不存在') raise ValueError(f'客户 {customer_id} 不存在')
# merchant 隔离:必须能解析出当前用户 merchant
emp = getattr(created_by, 'employee', None)
merchant = getattr(emp, 'merchant', None) if emp else None
if not merchant:
# superuser 也必须绑定 merchant避免产生无法隔离的数据
raise ValueError('用户未关联商户,无法创建出货单')
if customer.merchant_id != merchant.id:
raise ValueError('无权限为该客户创建出货单')
# 验证销售品 # 验证销售品
if sales_item_ids: if sales_item_ids:
# 查询销售品 # 查询销售品
@@ -92,6 +101,7 @@ def create_shipment(
# 创建出货单 # 创建出货单
shipment = Shipment.objects.create( shipment = Shipment.objects.create(
merchant=merchant,
customer=customer, customer=customer,
shipment_date=shipment_date, shipment_date=shipment_date,
remark=remark, remark=remark,
@@ -100,6 +110,69 @@ def create_shipment(
# 关联销售品 # 关联销售品
if sales_item_ids: if sales_item_ids:
SalesItem.objects.filter(id__in=sales_item_ids).update(shipment=shipment) updated = SalesItem.objects.filter(id__in=sales_item_ids, merchant=merchant).update(shipment=shipment)
if updated != len(sales_item_ids):
raise ValueError('存在不属于当前商户的销售品,无法关联到出货单')
return shipment return shipment
@transaction.atomic
def create_external_shipment(
customer_id: int,
shipment_date,
external_id: str,
external_finished_products: List[dict],
created_by,
remark: str = '',
) -> Shipment:
"""
创建出货单external 版),并批量写入外部成品表并关联到出货单。
特点:
- 不绑定任何 SalesItem
- external_id 必填
- external_finished_products 必填(至少 1 条)
"""
from basic_info.models import Customer
# 验证客户存在
try:
customer = Customer.objects.get(id=customer_id)
except Customer.DoesNotExist:
raise ValueError(f'客户 {customer_id} 不存在')
# merchant 隔离
emp = getattr(created_by, 'employee', None)
merchant = getattr(emp, 'merchant', None) if emp else None
if not merchant:
raise ValueError('用户未关联商户,无法创建出货单')
if customer.merchant_id != merchant.id:
raise ValueError('无权限为该客户创建出货单')
external_id = (external_id or '').strip()
if not external_id:
raise ValueError('external_id 不能为空')
if not external_finished_products:
raise ValueError('external_finished_products 不能为空')
shipment = Shipment.objects.create(
merchant=merchant,
customer=customer,
shipment_date=shipment_date,
remark=remark,
created_by=created_by,
external_id=external_id,
)
objs = []
for item in external_finished_products:
objs.append(ExternalFinishedProduct(
shipment=shipment,
style_name=item.get('style_name', ''),
num_of_rolls=item.get('num_of_rolls', 0),
created_by=created_by,
))
ExternalFinishedProduct.objects.bulk_create(objs)
return shipment