1
0
forked from erp-dev/erp

feat: shipment customer aou

This commit is contained in:
2026-03-30 22:03:14 +08:00
parent 38f40644a3
commit aff10b8925
7 changed files with 690 additions and 6 deletions

View File

@@ -33,8 +33,10 @@ from .views.parameters import StateParameterViewSet
from .views.users import CreateUserWithProfileView
from .views.mingdaoyun import MDYPlateOrderStagingViewSet
from .views.shipment import (
SalesItemByCustomerView,
SalesItemByPrintingOrderView,
SalesItemCreateView,
ShipmentSalesItemCustomerListView,
ShipmentListCreateView,
ShipmentDetailView,
ShipmentExternalCreateView,
@@ -305,6 +307,16 @@ urlpatterns = [
ShipmentExternalCreateView.as_view(),
name="shipment_create_external",
),
path(
"shipment/sales-items/customers/",
ShipmentSalesItemCustomerListView.as_view(),
name="sales_item_customers",
),
path(
"shipment/sales-items/by-customer/<int:customer_id>/",
SalesItemByCustomerView.as_view(),
name="sales_items_by_customer",
),
path(
"shipment/sales-items/by-printing-order/<int:printing_order_id>/",
SalesItemByPrintingOrderView.as_view(),

View File

@@ -4,16 +4,20 @@ Shipment API 模块
提供出货单和销售品相关的 API 接口
"""
from .views import (
SalesItemByCustomerView,
SalesItemByPrintingOrderView,
SalesItemCreateView,
ShipmentSalesItemCustomerListView,
ShipmentListCreateView,
ShipmentDetailView,
ShipmentExternalCreateView,
)
__all__ = [
'SalesItemByCustomerView',
'SalesItemByPrintingOrderView',
'SalesItemCreateView',
'ShipmentSalesItemCustomerListView',
'ShipmentListCreateView',
'ShipmentDetailView',
'ShipmentExternalCreateView',

View File

@@ -248,7 +248,9 @@ class SalesItemSerializer(serializers.Serializer):
position = serializers.CharField(read_only=True)
remark = serializers.CharField(read_only=True)
printing_job_id = serializers.IntegerField(read_only=True)
printing_order_id = serializers.SerializerMethodField()
customer_id = serializers.IntegerField(read_only=True)
customer_name = serializers.SerializerMethodField()
shipment_id = serializers.IntegerField(
source="shipment.id", read_only=True, allow_null=True
)
@@ -264,6 +266,24 @@ class SalesItemSerializer(serializers.Serializer):
def get_unit_display(self, obj):
return obj.get_unit_display()
def get_printing_order_id(self, obj):
printing_order_map = self.context.get("printing_order_map", {})
if obj.printing_job_id in printing_order_map:
return printing_order_map[obj.printing_job_id]
printing_job = obj.get_printing_job()
if printing_job and getattr(printing_job, "printing_order_id", None):
return printing_job.printing_order_id
return None
def get_customer_name(self, obj):
customer_name_map = self.context.get("customer_name_map", {})
if obj.customer_id in customer_name_map:
return customer_name_map[obj.customer_id]
customer = obj.get_customer()
return customer.name if customer else None
def get_created_by_name(self, obj):
if obj.created_by:
employee = getattr(obj.created_by, "employee", None)
@@ -273,6 +293,18 @@ class SalesItemSerializer(serializers.Serializer):
return None
class ShipmentSalesItemCustomerSerializer(serializers.Serializer):
"""
由未出货销售品反推的客户摘要序列化器。
"""
customer_id = serializers.IntegerField(source="id", read_only=True)
customer_name = serializers.CharField(source="name", read_only=True)
mobile = serializers.CharField(read_only=True, allow_null=True)
area = serializers.CharField(read_only=True, allow_null=True)
unshipped_sales_items_count = serializers.IntegerField(read_only=True)
class SalesItemCreateSerializer(serializers.Serializer):
"""
销售品创建序列化器

View File

@@ -6,7 +6,7 @@ from decimal import Decimal
from django.test import TestCase
from django.conf import settings
from rest_framework.test import APIClient
from rest_framework.test import APIClient, APITestCase
from rest_framework import status
from django.contrib.auth import get_user_model
@@ -279,6 +279,311 @@ class SalesItemByPrintingOrderAPITestCase(TestCase):
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
class ShipmentSalesItemCustomersAPITestCase(TestCase):
"""测试由未出货销售品汇总客户 API"""
def setUp(self):
self.client = APIClient()
self.merchant = basic_models.Merchant.objects.create(
name="测试印花厂", type=basic_models.MerchantTypeEnum.FACTORY
)
self.other_merchant = basic_models.Merchant.objects.create(
name="其它商户", type=basic_models.MerchantTypeEnum.FACTORY
)
self.user = User.objects.create_user(
username="shipment_customer_user",
password="testpass123",
email="shipment_customer@example.com",
)
self.employee = basic_models.Employee.objects.create(
sys_user=self.user,
merchant=self.merchant,
name="测试员工",
mobile="13800138020",
status=basic_models.EmployeeStatusEnum.ACTIVE,
)
self.customer1 = basic_models.Customer.objects.create(
merchant=self.merchant,
name="客户A",
mobile="13900139020",
area="杭州",
)
self.customer2 = basic_models.Customer.objects.create(
merchant=self.merchant,
name="客户B",
mobile="13900139021",
area="绍兴",
)
self.other_customer = basic_models.Customer.objects.create(
merchant=self.other_merchant,
name="客户C",
mobile="13900139022",
area="苏州",
)
shipment_models.SalesItem.objects.create(
merchant=self.merchant,
name="客户A销售品1",
quantity=Decimal("10.00"),
unit=shipment_models.UnitChoices.METER,
customer_id=self.customer1.id,
created_by=self.user,
)
shipment_models.SalesItem.objects.create(
merchant=self.merchant,
name="客户A销售品2",
quantity=Decimal("20.00"),
unit=shipment_models.UnitChoices.METER,
customer_id=self.customer1.id,
created_by=self.user,
)
shipment_models.SalesItem.objects.create(
merchant=self.merchant,
name="客户B销售品1",
quantity=Decimal("30.00"),
unit=shipment_models.UnitChoices.METER,
customer_id=self.customer2.id,
created_by=self.user,
)
shipped = shipment_models.Shipment.objects.create(
merchant=self.merchant,
customer=self.customer1,
shipment_date="2026-01-14",
created_by=self.user,
)
shipment_models.SalesItem.objects.create(
merchant=self.merchant,
name="客户A已出货销售品",
quantity=Decimal("40.00"),
unit=shipment_models.UnitChoices.METER,
customer_id=self.customer1.id,
shipment=shipped,
created_by=self.user,
)
shipment_models.SalesItem.objects.create(
merchant=self.merchant,
name="无客户销售品",
quantity=Decimal("50.00"),
unit=shipment_models.UnitChoices.METER,
created_by=self.user,
)
shipment_models.SalesItem.objects.create(
merchant=self.other_merchant,
name="其它商户销售品",
quantity=Decimal("60.00"),
unit=shipment_models.UnitChoices.METER,
customer_id=self.other_customer.id,
created_by=self.user,
)
self.client.force_authenticate(user=self.user)
def test_list_customers_with_unshipped_sales_items(self):
resp = self.client.get("/api/v1/shipment/sales-items/customers/")
self.assertEqual(resp.status_code, status.HTTP_200_OK)
data = resp.json()
self.assertEqual(data["count"], 2)
self.assertIn("next", data)
self.assertIn("previous", data)
by_customer_id = {
item["customer_id"]: item
for item in data["results"]
}
self.assertEqual(set(by_customer_id.keys()), {self.customer1.id, self.customer2.id})
self.assertEqual(by_customer_id[self.customer1.id]["customer_name"], "客户A")
self.assertEqual(by_customer_id[self.customer1.id]["unshipped_sales_items_count"], 2)
self.assertEqual(by_customer_id[self.customer2.id]["unshipped_sales_items_count"], 1)
def test_list_customers_with_unshipped_sales_items_supports_limit_offset(self):
resp = self.client.get("/api/v1/shipment/sales-items/customers/?limit=1&offset=1")
self.assertEqual(resp.status_code, status.HTTP_200_OK)
data = resp.json()
self.assertEqual(data["count"], 2)
self.assertEqual(len(data["results"]), 1)
self.assertEqual(data["results"][0]["customer_id"], self.customer2.id)
def test_list_customers_with_unshipped_sales_items_unauthenticated(self):
self.client.logout()
resp = self.client.get("/api/v1/shipment/sales-items/customers/")
self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED)
class SalesItemByCustomerAPITestCase(TestCase):
"""测试按客户查询销售品 API"""
def setUp(self):
self.client = APIClient()
self.merchant = basic_models.Merchant.objects.create(
name="测试印花厂", type=basic_models.MerchantTypeEnum.FACTORY
)
self.other_merchant = basic_models.Merchant.objects.create(
name="其它印花厂", type=basic_models.MerchantTypeEnum.FACTORY
)
self.user = User.objects.create_user(
username="sales_item_by_customer_user",
password="testpass123",
email="sales_item_by_customer@example.com",
)
self.employee = basic_models.Employee.objects.create(
sys_user=self.user,
merchant=self.merchant,
name="测试员工",
mobile="13800138030",
status=basic_models.EmployeeStatusEnum.ACTIVE,
)
self.customer = basic_models.Customer.objects.create(
merchant=self.merchant,
name="测试客户",
mobile="13900139030",
area="杭州",
)
self.other_customer = basic_models.Customer.objects.create(
merchant=self.merchant,
name="其它客户",
mobile="13900139031",
area="绍兴",
)
self.foreign_customer = basic_models.Customer.objects.create(
merchant=self.other_merchant,
name="外部客户",
mobile="13900139032",
area="苏州",
)
self.state = stateflow_models.State.objects.create(name="待生产")
self.process = stateflow_models.Process.objects.create(name="生产流程")
self.process.replace_nodes([self.state])
self.category = basic_models.ProductCategory.objects.create(
merchant=self.merchant,
name="测试分类",
)
self.product = basic_models.Product.objects.create(
merchant=self.merchant,
category=self.category,
name="测试产品",
human_id="PROD001",
)
self.printing_order = printing_models.PrintingOrder.objects.create(
merchant=self.merchant,
customer=self.customer,
fabric="测试面料",
width="150cm",
process=self.process,
created_by=self.user,
)
self.printing_job = printing_models.PrintingJob.objects.create(
merchant=self.merchant,
printing_order=self.printing_order,
product=self.product,
quantity=100,
unit="",
created_by=self.user,
)
self.sales_item1 = shipment_models.SalesItem.objects.create(
merchant=self.merchant,
name="销售品1",
quantity=Decimal("10.00"),
unit=shipment_models.UnitChoices.METER,
printing_job_id=self.printing_job.id,
customer_id=self.customer.id,
position="A1-01",
created_by=self.user,
)
self.sales_item2 = shipment_models.SalesItem.objects.create(
merchant=self.merchant,
name="销售品2",
quantity=Decimal("20.00"),
unit=shipment_models.UnitChoices.PIECE,
printing_job_id=self.printing_job.id,
customer_id=self.customer.id,
created_by=self.user,
)
self.shipment = shipment_models.Shipment.objects.create(
merchant=self.merchant,
customer=self.customer,
shipment_date="2026-01-14",
created_by=self.user,
)
self.sales_item_shipped = shipment_models.SalesItem.objects.create(
merchant=self.merchant,
name="已出货销售品",
quantity=Decimal("30.00"),
unit=shipment_models.UnitChoices.METER,
printing_job_id=self.printing_job.id,
customer_id=self.customer.id,
shipment=self.shipment,
created_by=self.user,
)
shipment_models.SalesItem.objects.create(
merchant=self.merchant,
name="其它客户销售品",
quantity=Decimal("40.00"),
unit=shipment_models.UnitChoices.METER,
customer_id=self.other_customer.id,
created_by=self.user,
)
self.client.force_authenticate(user=self.user)
def test_get_sales_items_by_customer_exclude_shipped(self):
resp = self.client.get(
f"/api/v1/shipment/sales-items/by-customer/{self.customer.id}/"
)
self.assertEqual(resp.status_code, status.HTTP_200_OK)
data = resp.json()
self.assertEqual(data["count"], 2)
self.assertEqual(len(data["results"]), 2)
item = next(it for it in data["results"] if it["id"] == self.sales_item1.id)
self.assertEqual(item["customer_id"], self.customer.id)
self.assertEqual(item["customer_name"], self.customer.name)
self.assertEqual(item["printing_job_id"], self.printing_job.id)
self.assertEqual(item["printing_order_id"], self.printing_order.id)
self.assertEqual(item["position"], "A1-01")
def test_get_sales_items_by_customer_include_shipped_and_paginate(self):
resp = self.client.get(
f"/api/v1/shipment/sales-items/by-customer/{self.customer.id}/"
"?include_already_has_shipment=true&limit=2&offset=1"
)
self.assertEqual(resp.status_code, status.HTTP_200_OK)
data = resp.json()
self.assertEqual(data["count"], 3)
self.assertEqual(len(data["results"]), 2)
result_ids = [item["id"] for item in data["results"]]
self.assertEqual(result_ids, [self.sales_item2.id, self.sales_item_shipped.id])
def test_get_sales_items_by_customer_not_found(self):
resp = self.client.get("/api/v1/shipment/sales-items/by-customer/99999/")
self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND)
def test_get_sales_items_by_customer_other_merchant_404(self):
resp = self.client.get(
f"/api/v1/shipment/sales-items/by-customer/{self.foreign_customer.id}/"
)
self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND)
class ShipmentCreateAPITestCase(TestCase):
"""测试创建出货单 API"""

View File

@@ -3,11 +3,11 @@ Shipment API ViewSet
"""
from rest_framework import status
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from rest_framework.generics import GenericAPIView
from rest_framework.mixins import ListModelMixin, RetrieveModelMixin
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from flower.viewsets import LimitedLimitOffsetPagination
from shipment.models import Shipment
@@ -17,10 +17,39 @@ from .serializers import (
ShipmentSerializer,
ShipmentCreateNormalSerializer,
ShipmentCreateExternalSerializer,
ShipmentSalesItemCustomerSerializer,
ShipmentUpdateSerializer,
)
def _build_sales_item_serializer_context(items):
customer_ids = {item.customer_id for item in items if item.customer_id}
printing_job_ids = {item.printing_job_id for item in items if item.printing_job_id}
customer_name_map = {}
if customer_ids:
from basic_info.models import Customer
customer_name_map = dict(
Customer.objects.filter(id__in=customer_ids).values_list("id", "name")
)
printing_order_map = {}
if printing_job_ids:
from printing.models import PrintingJob
printing_order_map = dict(
PrintingJob.objects.filter(id__in=printing_job_ids).values_list(
"id", "printing_order_id"
)
)
return {
"customer_name_map": customer_name_map,
"printing_order_map": printing_order_map,
}
class ShipmentListCreateView(ListModelMixin, GenericAPIView):
"""
出货单:查询列表 / 创建
@@ -259,6 +288,50 @@ class ShipmentExternalCreateView(APIView):
return Response(response_serializer.data, status=status.HTTP_201_CREATED)
class ShipmentSalesItemCustomerListView(GenericAPIView):
"""
从未出货销售品反推出“当前可出货客户”列表。
GET /api/v1/shipment/sales-items/customers/
"""
permission_classes = [IsAuthenticated]
serializer_class = ShipmentSalesItemCustomerSerializer
pagination_class = LimitedLimitOffsetPagination
def get_queryset(self):
user = self.request.user
if getattr(user, "is_superuser", False):
from basic_info.models import Customer, Merchant
merchant_id = self.request.query_params.get("merchant")
if not merchant_id:
return Customer.objects.none()
try:
merchant = Merchant.objects.get(id=merchant_id)
except Merchant.DoesNotExist:
return Customer.objects.none()
else:
from basic_info.models import Customer
emp = getattr(user, "employee", None)
merchant = getattr(emp, "merchant", None) if emp else None
if not merchant:
return Customer.objects.none()
from shipment.services import get_customers_with_unshipped_sales_items
return get_customers_with_unshipped_sales_items(merchant=merchant)
def get(self, request):
queryset = self.get_queryset()
page = self.paginate_queryset(queryset)
serializer = self.get_serializer(page if page is not None else queryset, many=True)
if page is not None:
return self.get_paginated_response(serializer.data)
return Response(serializer.data)
class SalesItemByPrintingOrderView(APIView):
"""
通过生产订单查询销售品
@@ -324,11 +397,73 @@ class SalesItemByPrintingOrderView(APIView):
)
# 序列化返回
serializer = SalesItemSerializer(sales_items, many=True)
items = list(sales_items)
serializer = SalesItemSerializer(
items,
many=True,
context=_build_sales_item_serializer_context(items),
)
return Response({"count": len(serializer.data), "results": serializer.data})
class SalesItemByCustomerView(GenericAPIView):
"""
通过客户查询销售品。
GET /api/v1/shipment/sales-items/by-customer/<customer_id>/
"""
permission_classes = [IsAuthenticated]
serializer_class = SalesItemSerializer
pagination_class = LimitedLimitOffsetPagination
def get(self, request, customer_id: int):
from basic_info.models import Customer
user = request.user
if getattr(user, "is_superuser", False):
customer = Customer.objects.filter(id=customer_id).first()
else:
emp = getattr(user, "employee", None)
merchant = getattr(emp, "merchant", None) if emp else None
if not merchant:
return Response(
{"detail": f"客户 {customer_id} 不存在"},
status=status.HTTP_404_NOT_FOUND,
)
customer = Customer.objects.filter(id=customer_id, merchant=merchant).first()
if customer is None:
return Response(
{"detail": f"客户 {customer_id} 不存在"},
status=status.HTTP_404_NOT_FOUND,
)
include_already_has_shipment = (
request.query_params.get("include_already_has_shipment", "false").lower()
== "true"
)
from shipment.services import get_sales_items_by_customer
queryset = get_sales_items_by_customer(
merchant=customer.merchant,
customer_id=customer.id,
include_already_has_shipment=include_already_has_shipment,
)
page = self.paginate_queryset(queryset)
items = list(page) if page is not None else list(queryset)
serializer = self.get_serializer(
items,
many=True,
context=_build_sales_item_serializer_context(items),
)
if page is not None:
return self.get_paginated_response(serializer.data)
return Response(serializer.data)
class SalesItemCreateView(APIView):
"""
手动创建销售品

View File

@@ -6,6 +6,8 @@
- [查询出货单](#查询出货单)
- [创建出货单](#创建出货单)
- [查询有待出货销售品的客户](#查询有待出货销售品的客户)
- [按客户查询销售品](#按客户查询销售品)
- [通过生产订单查询销售品](#通过生产订单查询销售品)
---
@@ -260,6 +262,137 @@
---
## 查询有待出货销售品的客户
查询当前商户下“存在未出货销售品”的客户列表。
### 接口信息
- **URL**: `/api/v1/shipment/sales-items/customers/`
- **Method**: `GET`
- **认证**: 需要登录JWT Token
### 查询参数
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| limit | int | 否 | 分页大小LimitOffsetPagination |
| offset | int | 否 | 偏移量 |
### 业务逻辑
1. 仅统计当前商户下的销售品
2. 只统计 `shipment` 为空的销售品(未出货)
3. 只返回至少拥有 1 条未出货销售品的客户
4. 返回客户基础信息及未出货销售品数量
### 响应格式
```json
{
"count": 2,
"next": null,
"previous": null,
"results": [
{
"customer_id": 12,
"customer_name": "客户A",
"mobile": "13800000000",
"area": "杭州",
"unshipped_sales_items_count": 18
}
]
}
```
### 响应字段说明
| 字段 | 类型 | 说明 |
|------|------|------|
| count | int | 结果总数 |
| next | string/null | 下一页链接 |
| previous | string/null | 上一页链接 |
| results[].customer_id | int | 客户ID |
| results[].customer_name | string | 客户名称 |
| results[].mobile | string/null | 客户手机号 |
| results[].area | string/null | 客户地区 |
| results[].unshipped_sales_items_count | int | 该客户未出货销售品数量 |
---
## 按客户查询销售品
查询指定客户下的销售品列表,默认仅返回未出货销售品。
### 接口信息
- **URL**: `/api/v1/shipment/sales-items/by-customer/<customer_id>/`
- **Method**: `GET`
- **认证**: 需要登录JWT Token
### 路径参数
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| customer_id | int | 是 | 客户ID |
### 查询参数
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| include_already_has_shipment | bool | 否 | false | 是否包含已关联出货单的销售品 |
| limit | int | 否 | 800 | 分页大小 |
| offset | int | 否 | 0 | 偏移量 |
### 业务逻辑
1. 仅允许查询当前商户下的客户
2. 仅查询当前商户下、`customer_id` 匹配的销售品
3. 默认只返回 `shipment` 为空的销售品(待出货)
4. 可通过 `include_already_has_shipment=true` 包含已出货销售品
### 响应格式
```json
{
"count": 2,
"next": null,
"previous": null,
"results": [
{
"id": 101,
"name": "赛扬 190g",
"quantity": "1200.00",
"unit": 1,
"unit_display": "米",
"position": "A1-01",
"remark": "",
"printing_job_id": 88,
"printing_order_id": 23,
"customer_id": 12,
"customer_name": "客户A",
"shipment_id": null,
"shipment_date": null,
"created_at": "2026-03-28T10:00:00Z",
"created_by_id": 1,
"created_by_name": "张三"
}
]
}
```
### 错误响应
#### 404 Not Found - 客户不存在或无权限
```json
{
"detail": "客户 999 不存在"
}
```
---
## 通过生产订单查询销售品
查询与指定生产订单PrintingOrder关联的所有销售品SalesItem
@@ -305,7 +438,9 @@
"position": "A1-01",
"remark": "加急处理",
"printing_job_id": 123,
"printing_order_id": 456,
"customer_id": null,
"customer_name": null,
"shipment_id": null,
"shipment_date": null,
"created_at": "2026-01-14T10:00:00Z",
@@ -321,7 +456,9 @@
"position": "",
"remark": "",
"printing_job_id": 124,
"printing_order_id": 456,
"customer_id": 10,
"customer_name": "客户A",
"shipment_id": 5,
"shipment_date": "2026-01-13",
"created_at": "2026-01-13T15:30:00Z",
@@ -346,7 +483,9 @@
| results[].position | string | 货位(可能为空) |
| results[].remark | string | 备注(可能为空) |
| results[].printing_job_id | int/null | 关联的生产任务ID |
| results[].printing_order_id | int/null | 关联的生产订单ID |
| results[].customer_id | int/null | 销售品级别的客户ID |
| results[].customer_name | string/null | 销售品关联客户名称 |
| results[].shipment_id | int/null | 关联的出货单IDnull 表示未出货 |
| results[].shipment_date | string/null | 出货日期YYYY-MM-DDnull 表示未出货 |
| results[].created_at | string | 创建时间ISO 8601 |

View File

@@ -7,11 +7,46 @@ from __future__ import annotations
from typing import List
from django.db import transaction
from django.db.models import QuerySet
from django.db.models import Count, Exists, IntegerField, OuterRef, QuerySet, Subquery
from django.db.models.functions import Coalesce
from shipment.models import ExternalFinishedProduct, SalesItem, Shipment
def get_customers_with_unshipped_sales_items(*, merchant) -> QuerySet:
"""
查询当前商户下“存在未出货销售品”的客户列表。
返回 Customer 查询集,并附带:
- unshipped_sales_items_count: 该客户未出货销售品数量
"""
from basic_info.models import Customer
base_sales_items = SalesItem.objects.filter(
merchant=merchant,
shipment__isnull=True,
customer_id=OuterRef("pk"),
)
count_subquery = (
base_sales_items.values("customer_id")
.annotate(total=Count("id"))
.values("total")[:1]
)
return (
Customer.objects.filter(merchant=merchant)
.annotate(has_unshipped_sales_items=Exists(base_sales_items))
.filter(has_unshipped_sales_items=True)
.annotate(
unshipped_sales_items_count=Coalesce(
Subquery(count_subquery, output_field=IntegerField()),
0,
)
)
.order_by("id")
)
def get_sales_items_by_printing_order(
printing_order_id: int,
include_already_has_shipment: bool = False,
@@ -43,6 +78,28 @@ def get_sales_items_by_printing_order(
return queryset.select_related("shipment").order_by("id")
def get_sales_items_by_customer(
*,
merchant,
customer_id: int,
include_already_has_shipment: bool = False,
) -> QuerySet[SalesItem]:
"""
通过客户ID查询对应销售品。
仅返回当前商户下、customer_id 匹配的销售品。
"""
queryset = SalesItem.objects.filter(
merchant=merchant,
customer_id=customer_id,
)
if not include_already_has_shipment:
queryset = queryset.filter(shipment__isnull=True)
return queryset.select_related("shipment").order_by("id")
@transaction.atomic
def create_shipment(
customer_id: int,