1
0
forked from erp-dev/erp

feat: transfer api for stock module

This commit is contained in:
2025-12-02 14:14:17 +08:00
parent 9dbb2f9c9b
commit 0b8146d972
9 changed files with 616 additions and 0 deletions

View File

@@ -15,6 +15,7 @@ from .detail import GetStockChangeView
from .finish import FinishStockChangeView
from .settings import SetMerchantAutoCompleteView
from .offset import OffsetStockChangeView
from .transfer import StockTransferView
# 向后兼容:保持原有的函数式接口
create_full_stock_change = CreateStockChangeView.as_view()
@@ -26,6 +27,7 @@ get_stock_change = GetStockChangeView.as_view()
finish_stock_change = FinishStockChangeView.as_view()
set_merchant_auto_complete_stock_change = SetMerchantAutoCompleteView.as_view()
offset_stock_change = OffsetStockChangeView.as_view()
transfer_stock_change = StockTransferView.as_view()
__all__ = [
'StockChangeViewMixin',
@@ -38,6 +40,7 @@ __all__ = [
'FinishStockChangeView',
'SetMerchantAutoCompleteView',
'OffsetStockChangeView',
'StockTransferView',
'create_full_stock_change',
'create_relaxed_stock_change',
'create_restrict_stock_change',
@@ -47,4 +50,5 @@ __all__ = [
'finish_stock_change',
'set_merchant_auto_complete_stock_change',
'offset_stock_change',
'transfer_stock_change',
]

View File

@@ -856,3 +856,89 @@ class OffsetStockChangeAPITestCase(TestCase):
response = self.client.post(self.offset_url, {}, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn('reason', response.data)
class TransferStockChangeAPITestCase(TestCase):
def setUp(self):
self.client = APIClient()
self.merchant = basic_models.Merchant.objects.create(
name='调拨商户',
type=basic_models.MerchantTypeEnum.FACTORY,
)
category = basic_models.ProductCategory.objects.create(
merchant=self.merchant,
name='调拨布料',
product_prefix='TRN',
)
self.product = basic_models.Product.objects.create(
merchant=self.merchant,
category=category,
name='调拨面料',
human_id='TRN-001',
unit=basic_models.ProductUnitEnum.METER,
)
self.from_warehouse = basic_models.WareHouse.objects.create(
merchant=self.merchant,
name='调出仓',
mode=basic_models.WareHouseModeEnum.RESTRICT_IN,
)
self.to_warehouse = basic_models.WareHouse.objects.create(
merchant=self.merchant,
name='调入仓',
mode=basic_models.WareHouseModeEnum.RESTRICT_IN,
type=basic_models.WarehouseTypeEnum.SCATTERED,
)
self.relaxed_warehouse = basic_models.WareHouse.objects.create(
merchant=self.merchant,
name='宽松仓',
mode=basic_models.WareHouseModeEnum.UNRESTRICTED,
)
self.user = User.objects.create_user(username='transfer_user', password='pass123')
self.employee = basic_models.Employee.objects.create(
merchant=self.merchant,
sys_user=self.user,
name='调拨员',
mobile='13800138007',
status=basic_models.EmployeeStatusEnum.ACTIVE,
)
self.client.force_authenticate(user=self.user)
def test_transfer_success(self):
payload = {
'from_warehouse': self.from_warehouse.id,
'to_warehouse': self.to_warehouse.id,
'products': [
{
'product_id': self.product.id,
'quantities': ['10.50', '5.25'],
}
],
'remarks': '整仓到散仓',
}
response = self.client.post('/api/v1/stock-change/transfer/', payload, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data['status'], stock_models.TransferOrderStatusEnum.COMPLETED)
self.assertEqual(response.data['from_warehouse'], self.from_warehouse.id)
self.assertEqual(response.data['to_warehouse'], self.to_warehouse.id)
self.assertEqual(len(response.data['items']), 1)
self.assertEqual(
Decimal(response.data['items'][0]['total_quantity']),
Decimal('15.75'),
)
def test_transfer_requires_same_mode(self):
payload = {
'from_warehouse': self.from_warehouse.id,
'to_warehouse': self.relaxed_warehouse.id,
'products': [
{
'product_id': self.product.id,
'quantities': ['8.00'],
}
],
}
response = self.client.post('/api/v1/stock-change/transfer/', payload, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn('模式一致', response.data['error'])

View File

@@ -0,0 +1,48 @@
import logging
from rest_framework import status, views
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from api_v1 import serializers
from stock import services as stock_services
from .mixins import StockChangeViewMixin
logger = logging.getLogger(__name__)
class StockTransferView(StockChangeViewMixin, views.APIView):
permission_classes = [IsAuthenticated]
def post(self, request):
if not self.check_employee_permission(request):
return self.permission_error_response('无权限访问')
serializer = serializers.StockTransferSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
data = serializer.validated_data
merchant = request.user.employee.merchant
operator = request.user.employee
try:
transfer_order = stock_services.create_transfer_order(
merchant=merchant,
created_by=request.user,
operator=operator,
from_warehouse_id=data['from_warehouse'],
to_warehouse_id=data['to_warehouse'],
items=data['products'],
transfer_date=data.get('transfer_date'),
remarks=data.get('remarks'),
request_id=data.get('request_id') or None,
)
except ValueError as exc:
return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST)
except Exception as exc:
logger.error('创建调拨单失败: %s', exc, exc_info=True)
return Response({'error': '创建调拨单失败'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
response_serializer = serializers.StockTransferResponseSerializer(transfer_order)
return Response(response_serializer.data, status=status.HTTP_201_CREATED)