diff --git a/api_v1/test_pre_sales_order_api.py b/api_v1/test_pre_sales_order_api.py index 3e56f79..5e62f91 100644 --- a/api_v1/test_pre_sales_order_api.py +++ b/api_v1/test_pre_sales_order_api.py @@ -229,3 +229,180 @@ class PreSalesOrderAPITestCase(TestCase): self.assertEqual(convert_resp.data['status'], business_models.SalesOrderStatusEnum.PENDING) sales_order = business_models.SalesOrder.objects.get(id=convert_resp.data['id']) self.assertEqual(sales_order.from_pre_sales_order_id, order_id) + + def test_detail_filters_cancelled_allocation_records(self): + """单体查询应过滤掉已撤销的配货记录""" + resp = self.client.post('/api/v1/pre-sales-orders/', self.payload, format='json') + self.assertEqual(resp.status_code, status.HTTP_201_CREATED) + order_id = resp.data['id'] + item_id = resp.data['items'][0]['id'] + + # 创建库存明细 + stock_record = stock_models.StockChangeRecord.objects.create( + merchant=self.merchant, + type=stock_models.StockChangeTypeEnum.ADD, + warehouse=self.warehouse, + source_type=stock_models.StockChangeSourceEnum.PURCHASE, + created_by=self.user, + ) + stock_detail1 = stock_models.StockChangeDetail.objects.create( + merchant=self.merchant, + product=self.product, + unit=ProductUnitEnum.METER, + stock_change_record=stock_record, + quantity='10', + ) + stock_detail2 = stock_models.StockChangeDetail.objects.create( + merchant=self.merchant, + product=self.product, + unit=ProductUnitEnum.METER, + stock_change_record=stock_record, + quantity='5', + ) + + # 创建两条配货记录 + item = business_models.PreSalesOrderItem.objects.get(id=item_id) + allocation1 = allocation_services.create_allocation_record( + item=item, + stock_ids=[stock_detail1.id], + quantity='10', + unit='米', + scanned_by=self.employee, + ) + allocation2 = allocation_services.create_allocation_record( + item=item, + stock_ids=[stock_detail2.id], + quantity='5', + unit='米', + scanned_by=self.employee, + ) + + # 撤销第一条记录 + allocation_services.cancel_allocation_record_with_unfreeze(record=allocation1) + + # 查询单体 + detail_resp = self.client.get(f'/api/v1/pre-sales-orders/{order_id}/') + self.assertEqual(detail_resp.status_code, status.HTTP_200_OK) + records = detail_resp.data['items'][0]['allocation_records'] + + # 应该只包含未撤销的记录 + self.assertEqual(len(records), 1) + self.assertEqual(records[0]['id'], allocation2.id) + self.assertEqual(records[0]['status'], business_models.AllocationRecordStatusEnum.ACTIVE) + + def test_list_includes_progress_fields(self): + """列表查询应包含allocated_quantity和progress字段""" + # 创建订单 + resp = self.client.post('/api/v1/pre-sales-orders/', self.payload, format='json') + self.assertEqual(resp.status_code, status.HTTP_201_CREATED) + order_id = resp.data['id'] + item_id = resp.data['items'][0]['id'] + + # 创建库存并配货 + stock_record = stock_models.StockChangeRecord.objects.create( + merchant=self.merchant, + type=stock_models.StockChangeTypeEnum.ADD, + warehouse=self.warehouse, + source_type=stock_models.StockChangeSourceEnum.PURCHASE, + created_by=self.user, + ) + stock_detail = stock_models.StockChangeDetail.objects.create( + merchant=self.merchant, + product=self.product, + unit=ProductUnitEnum.METER, + stock_change_record=stock_record, + quantity='12.5', + ) + item = business_models.PreSalesOrderItem.objects.get(id=item_id) + allocation_services.create_allocation_record( + item=item, + stock_ids=[stock_detail.id], + quantity='5.0', + unit='米', + scanned_by=self.employee, + ) + + # 查询列表 + list_resp = self.client.get('/api/v1/pre-sales-orders/') + self.assertEqual(list_resp.status_code, status.HTTP_200_OK) + self.assertIn('results', list_resp.data) + + # 查找当前订单 + order_data = None + for order in list_resp.data['results']: + if order['id'] == order_id: + order_data = order + break + + self.assertIsNotNone(order_data) + # 验证进度字段存在 + self.assertIn('allocated_quantity', order_data) + self.assertIn('progress', order_data) + # 需求量12.5,已配货5.0 + self.assertEqual(order_data['allocated_quantity'], '5.00') + # 进度 = 5.0 / 12.5 = 0.4 + self.assertEqual(order_data['progress'], '0.4000') + + def test_list_progress_excludes_cancelled_allocations(self): + """列表查询的进度字段应排除已撤销的配货记录""" + resp = self.client.post('/api/v1/pre-sales-orders/', self.payload, format='json') + self.assertEqual(resp.status_code, status.HTTP_201_CREATED) + order_id = resp.data['id'] + item_id = resp.data['items'][0]['id'] + + # 创建库存 + stock_record = stock_models.StockChangeRecord.objects.create( + merchant=self.merchant, + type=stock_models.StockChangeTypeEnum.ADD, + warehouse=self.warehouse, + source_type=stock_models.StockChangeSourceEnum.PURCHASE, + created_by=self.user, + ) + stock_detail1 = stock_models.StockChangeDetail.objects.create( + merchant=self.merchant, + product=self.product, + unit=ProductUnitEnum.METER, + stock_change_record=stock_record, + quantity='10', + ) + stock_detail2 = stock_models.StockChangeDetail.objects.create( + merchant=self.merchant, + product=self.product, + unit=ProductUnitEnum.METER, + stock_change_record=stock_record, + quantity='5', + ) + + # 创建两条配货,然后撤销一条 + item = business_models.PreSalesOrderItem.objects.get(id=item_id) + allocation1 = allocation_services.create_allocation_record( + item=item, + stock_ids=[stock_detail1.id], + quantity='10', + unit='米', + scanned_by=self.employee, + ) + allocation2 = allocation_services.create_allocation_record( + item=item, + stock_ids=[stock_detail2.id], + quantity='2.5', + unit='米', + scanned_by=self.employee, + ) + allocation_services.cancel_allocation_record_with_unfreeze(record=allocation1) + + # 查询列表 + list_resp = self.client.get('/api/v1/pre-sales-orders/') + self.assertEqual(list_resp.status_code, status.HTTP_200_OK) + + order_data = None + for order in list_resp.data['results']: + if order['id'] == order_id: + order_data = order + break + + self.assertIsNotNone(order_data) + # 应该只统计未撤销的配货(2.5),而不是10+2.5 + self.assertEqual(order_data['allocated_quantity'], '2.50') + # 进度 = 2.5 / 12.5 = 0.2 + self.assertEqual(order_data['progress'], '0.2000') diff --git a/api_v1/views/business/pre_sales/views.py b/api_v1/views/business/pre_sales/views.py index 451a66d..dc9dce6 100644 --- a/api_v1/views/business/pre_sales/views.py +++ b/api_v1/views/business/pre_sales/views.py @@ -122,13 +122,20 @@ class PreSalesOrderItemSerializer(serializers.ModelSerializer): class PreSalesOrderItemDetailSerializer(PreSalesOrderItemSerializer): - allocation_records = AllocationRecordSerializer(many=True, read_only=True) + allocation_records = serializers.SerializerMethodField(read_only=True) class Meta(PreSalesOrderItemSerializer.Meta): fields = PreSalesOrderItemSerializer.Meta.fields + [ 'allocation_records', ] + def get_allocation_records(self, obj: business_models.PreSalesOrderItem): + """返回非已撤销的配货记录""" + active_records = obj.allocation_records.exclude( + status=business_models.AllocationRecordStatusEnum.CANCELLED + ) + return AllocationRecordSerializer(active_records, many=True).data + class PreSalesOrderSerializer(serializers.ModelSerializer): human_id = serializers.CharField(read_only=True) @@ -136,6 +143,8 @@ class PreSalesOrderSerializer(serializers.ModelSerializer): warehouse_name = serializers.CharField(source='warehouse.name', read_only=True) operator_name = serializers.CharField(source='operator.name', read_only=True) items = PreSalesOrderItemSerializer(many=True, read_only=True) + allocated_quantity = serializers.SerializerMethodField(read_only=True) + progress = serializers.SerializerMethodField(read_only=True) class Meta: model = business_models.PreSalesOrder @@ -153,6 +162,8 @@ class PreSalesOrderSerializer(serializers.ModelSerializer): 'remarks', 'created_at', 'items', + 'allocated_quantity', + 'progress', ] read_only_fields = [ 'id', @@ -162,8 +173,34 @@ class PreSalesOrderSerializer(serializers.ModelSerializer): 'warehouse_name', 'operator_name', 'items', + 'allocated_quantity', + 'progress', ] + def get_allocated_quantity(self, obj: business_models.PreSalesOrder) -> str: + """计算整单已配货总量""" + total = Decimal('0') + for item in obj.items.all(): + result = item.allocation_records.filter( + status=business_models.AllocationRecordStatusEnum.ACTIVE + ).aggregate(total=Sum('quantity')) + total += result.get('total') or Decimal('0') + return str(total) + + def get_progress(self, obj: business_models.PreSalesOrder) -> str: + """计算整单配货进度(已配货总量/总需求量)""" + required_total = Decimal('0') + allocated_total = Decimal('0') + for item in obj.items.all(): + required_total += item.quantity or Decimal('0') + result = item.allocation_records.filter( + status=business_models.AllocationRecordStatusEnum.ACTIVE + ).aggregate(total=Sum('quantity')) + allocated_total += result.get('total') or Decimal('0') + if required_total <= 0: + return '0' + return str((allocated_total / required_total).quantize(Decimal('0.0001'))) + class PreSalesOrderDetailSerializer(PreSalesOrderSerializer): items = PreSalesOrderItemDetailSerializer(many=True, read_only=True)