1
0
forked from erp-dev/erp

feat: added shipments list api

This commit is contained in:
2026-01-15 16:47:12 +08:00
parent ea9f378de9
commit a0cd168397
6 changed files with 284 additions and 8 deletions

View File

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

View File

@@ -553,3 +553,101 @@ class ShipmentExternalCreateAPITestCase(TestCase):
}
response = self.client.post('/api/v1/shipment/shipments/external/', data, format='json')
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
class ShipmentQueryAPITestCase(TestCase):
"""测试出货单查询 API列表/详情)"""
def setUp(self):
self.client = APIClient()
self.merchant1 = basic_models.Merchant.objects.create(
name='商户1',
type=basic_models.MerchantTypeEnum.FACTORY
)
self.merchant2 = basic_models.Merchant.objects.create(
name='商户2',
type=basic_models.MerchantTypeEnum.FACTORY
)
self.user1 = User.objects.create_user(
username='u1',
password='pass123',
email='u1@example.com'
)
self.emp1 = basic_models.Employee.objects.create(
sys_user=self.user1,
merchant=self.merchant1,
name='员工1',
mobile='13800138010',
status=basic_models.EmployeeStatusEnum.ACTIVE
)
self.user2 = User.objects.create_user(
username='u2',
password='pass123',
email='u2@example.com'
)
self.emp2 = basic_models.Employee.objects.create(
sys_user=self.user2,
merchant=self.merchant2,
name='员工2',
mobile='13800138011',
status=basic_models.EmployeeStatusEnum.ACTIVE
)
self.customer1 = basic_models.Customer.objects.create(
merchant=self.merchant1,
name='客户1',
mobile='13900139010',
area='A'
)
self.customer2 = basic_models.Customer.objects.create(
merchant=self.merchant2,
name='客户2',
mobile='13900139011',
area='B'
)
self.shipment1 = shipment_models.Shipment.objects.create(
merchant=self.merchant1,
customer=self.customer1,
shipment_date='2026-01-15',
created_by=self.user1,
remark='s1',
)
self.shipment2 = shipment_models.Shipment.objects.create(
merchant=self.merchant2,
customer=self.customer2,
shipment_date='2026-01-15',
created_by=self.user2,
remark='s2',
)
self.client.force_authenticate(user=self.user1)
def test_list_shipments_only_current_merchant(self):
resp = self.client.get('/api/v1/shipment/shipments/')
self.assertEqual(resp.status_code, status.HTTP_200_OK)
data = resp.json()
# LimitOffsetPagination 结构count/next/previous/results
self.assertIn('count', data)
self.assertIn('results', data)
ids = [it['id'] for it in data['results']]
self.assertIn(self.shipment1.id, ids)
self.assertNotIn(self.shipment2.id, ids)
def test_retrieve_shipment_success(self):
resp = self.client.get(f'/api/v1/shipment/shipments/{self.shipment1.id}/')
self.assertEqual(resp.status_code, status.HTTP_200_OK)
self.assertEqual(resp.json()['id'], self.shipment1.id)
def test_retrieve_other_merchant_shipment_404(self):
resp = self.client.get(f'/api/v1/shipment/shipments/{self.shipment2.id}/')
self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND)
def test_list_shipments_unauthenticated(self):
self.client.logout()
resp = self.client.get('/api/v1/shipment/shipments/')
self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED)

View File

@@ -5,6 +5,11 @@ 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 flower.viewsets import LimitedLimitOffsetPagination
from shipment.models import Shipment
from .serializers import (
SalesItemSerializer,
@@ -14,11 +19,12 @@ from .serializers import (
)
class ShipmentCreateView(APIView):
class ShipmentListCreateView(ListModelMixin, GenericAPIView):
"""
创建出货单
出货单:查询列表 / 创建
POST /api/v1/shipment/shipments/
- GET /api/v1/shipment/shipments/
- POST /api/v1/shipment/shipments/
请求体:
{
@@ -43,6 +49,61 @@ class ShipmentCreateView(APIView):
}
"""
permission_classes = [IsAuthenticated]
serializer_class = ShipmentSerializer
pagination_class = LimitedLimitOffsetPagination
def get_queryset(self):
"""
仅返回当前用户所属商户的出货单merchant 隔离)。
支持过滤参数(可选):
- customer: 客户ID
- status: 状态1/2/3
- external_id: 外部订单号(精确匹配)
- shipment_date_from: 出货日期起始YYYY-MM-DD
- shipment_date_to: 出货日期结束YYYY-MM-DD包含整天
"""
qs = Shipment.objects.all().select_related(
'merchant', 'customer', 'created_by', 'cancelled_by'
).prefetch_related(
'items',
'external_finished_products',
)
user = self.request.user
if not getattr(user, 'is_superuser', False):
emp = getattr(user, 'employee', None)
merchant = getattr(emp, 'merchant', None) if emp else None
if not merchant:
return Shipment.objects.none()
qs = qs.filter(merchant=merchant)
# optional filters
customer_id = self.request.query_params.get('customer')
if customer_id:
qs = qs.filter(customer_id=customer_id)
status_val = self.request.query_params.get('status')
if status_val:
qs = qs.filter(status=status_val)
external_id = self.request.query_params.get('external_id')
if external_id:
qs = qs.filter(external_id=external_id)
date_from = self.request.query_params.get('shipment_date_from')
if date_from:
qs = qs.filter(shipment_date__gte=date_from)
date_to = self.request.query_params.get('shipment_date_to')
if date_to:
# 通过 <= 过滤日期
qs = qs.filter(shipment_date__lte=date_to)
return qs.order_by('-created_at', '-id')
def get(self, request):
return self.list(request)
def post(self, request):
# 验证请求数据
@@ -70,6 +131,37 @@ class ShipmentCreateView(APIView):
return Response(response_serializer.data, status=status.HTTP_201_CREATED)
class ShipmentDetailView(RetrieveModelMixin, GenericAPIView):
"""
出货单详情
GET /api/v1/shipment/shipments/<id>/
"""
permission_classes = [IsAuthenticated]
serializer_class = ShipmentSerializer
def get_queryset(self):
qs = Shipment.objects.all().select_related(
'merchant', 'customer', 'created_by', 'cancelled_by'
).prefetch_related(
'items',
'external_finished_products',
)
user = self.request.user
if getattr(user, 'is_superuser', False):
return qs
emp = getattr(user, 'employee', None)
merchant = getattr(emp, 'merchant', None) if emp else None
if not merchant:
return Shipment.objects.none()
return qs.filter(merchant=merchant)
def get(self, request, pk: int):
return self.retrieve(request, pk=pk)
class ShipmentExternalCreateView(APIView):
"""
创建出货单external 版)