forked from erp-dev/erp
feat: added filter param for stock record list api. Support purchase order bind stock change record that is already exists
This commit is contained in:
@@ -17,6 +17,8 @@ from basic_info.models import (
|
||||
EmployeeStatusEnum,
|
||||
Merchant,
|
||||
MerchantTypeEnum,
|
||||
MerchantSetting,
|
||||
MerchantSettingKeyEnum,
|
||||
Product,
|
||||
ProductCategory,
|
||||
ProductUnitEnum,
|
||||
@@ -228,6 +230,30 @@ class PurchaseOrderAPITestCase(TestCase):
|
||||
self.assertEqual(response.data['human_id'], expected_human_id)
|
||||
mock_delay.assert_not_called()
|
||||
|
||||
def test_bind_purchase_order_stock_change_record(self):
|
||||
MerchantSetting.objects.filter(
|
||||
merchant=self.merchant,
|
||||
key=MerchantSettingKeyEnum.AUTO_CREATE_STOCK_CHANGE_TASKS,
|
||||
).update(val_bool=False)
|
||||
|
||||
order_id = self._create_purchase_order(self.strict_payload)
|
||||
record = stock_models.StockChangeRecord.objects.create(
|
||||
merchant=self.merchant,
|
||||
type=stock_models.StockChangeTypeEnum.ADD,
|
||||
warehouse=self.warehouse_strict,
|
||||
source_type=stock_models.StockChangeSourceEnum.PURCHASE,
|
||||
created_by=self.user,
|
||||
)
|
||||
|
||||
resp = self.client.post(
|
||||
f'/api/v1/purchase-orders/{order_id}/bind-stock-change/',
|
||||
{'stock_change_record_id': record.id},
|
||||
format='json',
|
||||
)
|
||||
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||
record.refresh_from_db()
|
||||
self.assertEqual(record.source_id, order_id)
|
||||
|
||||
def test_create_purchase_order_invalid_supplier(self):
|
||||
payload = {**self.strict_payload, 'supplier': 999}
|
||||
response = self.client.post('/api/v1/purchase-orders/', payload, format='json')
|
||||
|
||||
@@ -80,6 +80,11 @@ urlpatterns = [
|
||||
path('purchase-orders/', purchase_views.PurchaseOrderView.as_view(), name='purchase_orders'),
|
||||
path('purchase-orders/<int:pk>/', purchase_views.PurchaseOrderDetailView.as_view(), name='purchase_order_detail'),
|
||||
path('purchase-orders/<int:pk>/review/', purchase_views.PurchaseOrderReviewView.as_view(), name='purchase_order_review'),
|
||||
path(
|
||||
'purchase-orders/<int:pk>/bind-stock-change/',
|
||||
purchase_views.PurchaseOrderBindStockChangeView.as_view(),
|
||||
name='purchase_order_bind_stock_change',
|
||||
),
|
||||
path('purchase-return-orders/', purchase_return_views.PurchaseReturnOrderView.as_view(), name='purchase_return_orders'),
|
||||
path('purchase-return-orders/<int:pk>/', purchase_return_views.PurchaseReturnOrderDetailView.as_view(), name='purchase_return_order_detail'),
|
||||
path('purchase-return-orders/<int:pk>/review/', purchase_return_views.PurchaseReturnOrderReviewView.as_view(), name='purchase_return_order_review'),
|
||||
|
||||
@@ -253,3 +253,47 @@ class PurchaseOrderReviewView(StockChangeViewMixin, views.APIView):
|
||||
).prefetch_related('items').get(id=purchase_order.id)
|
||||
return Response(PurchaseOrderSerializer(refreshed_order).data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class PurchaseOrderBindStockChangeSerializer(serializers.Serializer):
|
||||
stock_change_record_id = serializers.IntegerField(min_value=1)
|
||||
|
||||
|
||||
class PurchaseOrderBindStockChangeView(StockChangeViewMixin, views.APIView):
|
||||
"""采购单绑定库存记录"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request, pk: int):
|
||||
if not self.check_employee_permission(request):
|
||||
return self.permission_error_response('无权限访问')
|
||||
|
||||
merchant = request.user.employee.merchant
|
||||
try:
|
||||
purchase_order = business_models.PurchaseOrder.objects.select_related(
|
||||
'supplier', 'operator', 'warehouse'
|
||||
).get(id=pk, merchant=merchant)
|
||||
except business_models.PurchaseOrder.DoesNotExist:
|
||||
return self.not_found_response('采购单不存在')
|
||||
|
||||
serializer = PurchaseOrderBindStockChangeSerializer(data=request.data or {})
|
||||
serializer.is_valid(raise_exception=True)
|
||||
stock_change_record_id = serializer.validated_data['stock_change_record_id']
|
||||
|
||||
try:
|
||||
record = business_services.bind_purchase_order_stock_change_record(
|
||||
purchase_order_id=purchase_order.id,
|
||||
stock_change_record_id=stock_change_record_id,
|
||||
operator=request.user,
|
||||
)
|
||||
except ValueError as exc:
|
||||
return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
return Response(
|
||||
{
|
||||
'purchase_order_id': purchase_order.id,
|
||||
'stock_change_record_id': record.id,
|
||||
'message': '采购单已绑定库存记录',
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
|
||||
@@ -28,6 +28,9 @@ class ListStockChangesView(StockChangeViewMixin, views.APIView):
|
||||
OpenApiParameter(name='start', type=OpenApiTypes.DATE, description='开始日期 (YYYY-MM-DD),默认为昨天'),
|
||||
OpenApiParameter(name='end', type=OpenApiTypes.DATE, description='结束日期 (YYYY-MM-DD),默认为昨天'),
|
||||
OpenApiParameter(name='type', type=OpenApiTypes.INT, description='变动类型 (1=入库, 2=出库)'),
|
||||
OpenApiParameter(name='source_type', type=OpenApiTypes.INT, description='来源类型 (如采购=1)'),
|
||||
OpenApiParameter(name='source_id', type=OpenApiTypes.INT, description='来源业务单据ID'),
|
||||
OpenApiParameter(name='source_id_isnull', type=OpenApiTypes.BOOL, description='来源业务单据ID是否为空'),
|
||||
OpenApiParameter(name='warehouse', type=OpenApiTypes.INT, description='仓库ID'),
|
||||
OpenApiParameter(name='product', type=OpenApiTypes.INT, description='产品ID'),
|
||||
OpenApiParameter(name='is_finished', type=OpenApiTypes.BOOL, description='是否已完成'),
|
||||
@@ -45,6 +48,9 @@ class ListStockChangesView(StockChangeViewMixin, views.APIView):
|
||||
- start: 开始日期 (YYYY-MM-DD),可选,默认为昨天
|
||||
- end: 结束日期 (YYYY-MM-DD),可选,默认为昨天
|
||||
- type: 变动类型 (1=入库, 2=出库),可选
|
||||
- source_type: 来源类型(可选)
|
||||
- source_id: 来源业务单据ID(可选)
|
||||
- source_id_isnull: 来源业务单据ID是否为空(可选,true/false)
|
||||
- warehouse: 仓库ID,可选
|
||||
- product: 产品ID,可选
|
||||
- is_finished: 是否已完成 (true/false),可选
|
||||
@@ -106,6 +112,31 @@ class ListStockChangesView(StockChangeViewMixin, views.APIView):
|
||||
return Response({
|
||||
'error': 'type 参数必须为整数'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
source_type = request.GET.get('source_type')
|
||||
if source_type:
|
||||
try:
|
||||
queryset = queryset.filter(source_type=int(source_type))
|
||||
except ValueError:
|
||||
return Response({
|
||||
'error': 'source_type 参数必须为整数'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
source_id = request.GET.get('source_id')
|
||||
if source_id:
|
||||
try:
|
||||
queryset = queryset.filter(source_id=int(source_id))
|
||||
except ValueError:
|
||||
return Response({
|
||||
'error': 'source_id 参数必须为整数'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
else:
|
||||
source_id_isnull = request.GET.get('source_id_isnull')
|
||||
if source_id_isnull is not None:
|
||||
if source_id_isnull.lower() == 'true':
|
||||
queryset = queryset.filter(source_id__isnull=True)
|
||||
elif source_id_isnull.lower() == 'false':
|
||||
queryset = queryset.filter(source_id__isnull=False)
|
||||
|
||||
warehouse_id = request.GET.get('warehouse')
|
||||
if warehouse_id:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from decimal import Decimal
|
||||
import datetime
|
||||
import logging
|
||||
import unittest
|
||||
|
||||
@@ -541,7 +542,7 @@ class ListStockChangeDetailsAPITestCase(TestCase):
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertIn('缺少必要参数', response.data['error'])
|
||||
self.assertIn('warehouse_id', response.data['message'])
|
||||
|
||||
|
||||
def test_list_stock_change_details_missing_product_id(self):
|
||||
"""测试缺少product_id参数"""
|
||||
@@ -665,6 +666,90 @@ class ListStockChangeDetailsAPITestCase(TestCase):
|
||||
self.assertEqual(result['record_type'], stock_models.StockChangeTypeEnum.REMOVE)
|
||||
self.assertEqual(result['record_type_display'], '出库')
|
||||
|
||||
|
||||
class ListStockChangeRecordsAPITestCase(TestCase):
|
||||
"""测试库存变动记录查询 API 的新增筛选参数"""
|
||||
|
||||
def setUp(self):
|
||||
self.client = APIClient()
|
||||
|
||||
self.merchant = basic_models.Merchant.objects.create(
|
||||
name='记录商户',
|
||||
type=basic_models.MerchantTypeEnum.FACTORY,
|
||||
)
|
||||
self.category = basic_models.ProductCategory.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='布料',
|
||||
product_prefix='FAB',
|
||||
)
|
||||
self.product = basic_models.Product.objects.create(
|
||||
merchant=self.merchant,
|
||||
category=self.category,
|
||||
name='测试布料',
|
||||
human_id='FAB-100',
|
||||
unit=basic_models.ProductUnitEnum.METER,
|
||||
)
|
||||
self.warehouse = basic_models.WareHouse.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='记录仓库',
|
||||
mode=basic_models.WareHouseModeEnum.RESTRICT_IN,
|
||||
)
|
||||
self.user = User.objects.create_user(username='record_user', password='pass123')
|
||||
self.employee = basic_models.Employee.objects.create(
|
||||
merchant=self.merchant,
|
||||
sys_user=self.user,
|
||||
name='仓管员',
|
||||
mobile='13800138009',
|
||||
status=basic_models.EmployeeStatusEnum.ACTIVE,
|
||||
)
|
||||
self.client.force_authenticate(user=self.user)
|
||||
|
||||
self.record_unbound = stock_models.StockChangeRecord.objects.create(
|
||||
merchant=self.merchant,
|
||||
type=stock_models.StockChangeTypeEnum.ADD,
|
||||
warehouse=self.warehouse,
|
||||
source_type=stock_models.StockChangeSourceEnum.PURCHASE,
|
||||
source_id=None,
|
||||
created_by=self.user,
|
||||
)
|
||||
stock_models.StockChangeDetail.objects.create(
|
||||
merchant=self.merchant,
|
||||
stock_change_record=self.record_unbound,
|
||||
product=self.product,
|
||||
quantity='10',
|
||||
unit=self.product.unit,
|
||||
)
|
||||
|
||||
self.record_bound = stock_models.StockChangeRecord.objects.create(
|
||||
merchant=self.merchant,
|
||||
type=stock_models.StockChangeTypeEnum.ADD,
|
||||
warehouse=self.warehouse,
|
||||
source_type=stock_models.StockChangeSourceEnum.PURCHASE,
|
||||
source_id=99,
|
||||
created_by=self.user,
|
||||
)
|
||||
stock_models.StockChangeDetail.objects.create(
|
||||
merchant=self.merchant,
|
||||
stock_change_record=self.record_bound,
|
||||
product=self.product,
|
||||
quantity='5',
|
||||
unit=self.product.unit,
|
||||
)
|
||||
|
||||
def test_list_stock_change_records_filter_source_type_and_source_id_isnull(self):
|
||||
today = datetime.date.today().strftime('%Y-%m-%d')
|
||||
response = self.client.get(
|
||||
f'/api/v1/stock-changes/?start={today}&end={today}'
|
||||
f'&type={stock_models.StockChangeTypeEnum.ADD}'
|
||||
f'&source_type={stock_models.StockChangeSourceEnum.PURCHASE}'
|
||||
f'&source_id_isnull=true'
|
||||
f'&warehouse={self.warehouse.id}'
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
result_ids = [item['id'] for item in response.data['results']]
|
||||
self.assertIn(self.record_unbound.id, result_ids)
|
||||
self.assertNotIn(self.record_bound.id, result_ids)
|
||||
|
||||
def test_list_stock_change_details_with_invalid_direction(self):
|
||||
"""测试无效的direction参数"""
|
||||
response = self.client.get(
|
||||
|
||||
Reference in New Issue
Block a user