forked from erp-dev/erp
fix: added some fields to business.statements api
This commit is contained in:
@@ -1283,9 +1283,13 @@ class StatementRecordAPITestCase(TestCase):
|
|||||||
sales_order=self.sales_order,
|
sales_order=self.sales_order,
|
||||||
product=self.product,
|
product=self.product,
|
||||||
price=Decimal('12.50'),
|
price=Decimal('12.50'),
|
||||||
|
color='红',
|
||||||
quantity=Decimal('5'),
|
quantity=Decimal('5'),
|
||||||
unit='米',
|
unit='米',
|
||||||
empty_diff_percent=Decimal('0'),
|
empty_diff_percent=Decimal('0'),
|
||||||
|
quantity_of_rolls='2,3',
|
||||||
|
num_of_rolls=2,
|
||||||
|
spec='32S',
|
||||||
)
|
)
|
||||||
|
|
||||||
self.purchase_order = business_models.PurchaseOrder.objects.create(
|
self.purchase_order = business_models.PurchaseOrder.objects.create(
|
||||||
@@ -1300,9 +1304,13 @@ class StatementRecordAPITestCase(TestCase):
|
|||||||
purchase_order=self.purchase_order,
|
purchase_order=self.purchase_order,
|
||||||
product=self.product,
|
product=self.product,
|
||||||
price=Decimal('8.30'),
|
price=Decimal('8.30'),
|
||||||
|
color='蓝',
|
||||||
quantity=Decimal('10'),
|
quantity=Decimal('10'),
|
||||||
unit='米',
|
unit='米',
|
||||||
empty_diff_percent=Decimal('0'),
|
empty_diff_percent=Decimal('0'),
|
||||||
|
quantity_of_rolls='6,4',
|
||||||
|
num_of_rolls=2,
|
||||||
|
spec='40S',
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_get_customer_sales_order_record(self):
|
def test_get_customer_sales_order_record(self):
|
||||||
@@ -1323,6 +1331,14 @@ class StatementRecordAPITestCase(TestCase):
|
|||||||
self.assertEqual(record['source_type'], 'sales_order')
|
self.assertEqual(record['source_type'], 'sales_order')
|
||||||
self.assertEqual(record['source_id'], self.sales_order.id)
|
self.assertEqual(record['source_id'], self.sales_order.id)
|
||||||
self.assertEqual(record['counterparty'], self.customer.id)
|
self.assertEqual(record['counterparty'], self.customer.id)
|
||||||
|
self.assertIsNotNone(record.get('warehouse'))
|
||||||
|
self.assertEqual(record['warehouse']['id'], self.warehouse.id)
|
||||||
|
self.assertEqual(record['warehouse']['name'], self.warehouse.name)
|
||||||
|
self.assertTrue(record['items'])
|
||||||
|
self.assertEqual(record['items'][0]['color'], '红')
|
||||||
|
self.assertEqual(record['items'][0]['spec'], '32S')
|
||||||
|
self.assertEqual(record['items'][0]['quantity_of_rolls'], [2, 3])
|
||||||
|
self.assertEqual(record['items'][0]['num_of_rolls'], 2)
|
||||||
|
|
||||||
def test_get_supplier_purchase_order_record(self):
|
def test_get_supplier_purchase_order_record(self):
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
@@ -1339,6 +1355,14 @@ class StatementRecordAPITestCase(TestCase):
|
|||||||
self.assertEqual(record['source_type'], 'purchase_order')
|
self.assertEqual(record['source_type'], 'purchase_order')
|
||||||
self.assertEqual(record['source_id'], self.purchase_order.id)
|
self.assertEqual(record['source_id'], self.purchase_order.id)
|
||||||
self.assertEqual(record['counterparty'], self.supplier.id)
|
self.assertEqual(record['counterparty'], self.supplier.id)
|
||||||
|
self.assertIsNotNone(record.get('warehouse'))
|
||||||
|
self.assertEqual(record['warehouse']['id'], self.warehouse.id)
|
||||||
|
self.assertEqual(record['warehouse']['name'], self.warehouse.name)
|
||||||
|
self.assertTrue(record['items'])
|
||||||
|
self.assertEqual(record['items'][0]['color'], '蓝')
|
||||||
|
self.assertEqual(record['items'][0]['spec'], '40S')
|
||||||
|
self.assertEqual(record['items'][0]['quantity_of_rolls'], [6, 4])
|
||||||
|
self.assertEqual(record['items'][0]['num_of_rolls'], 2)
|
||||||
|
|
||||||
def test_statement_record_not_found(self):
|
def test_statement_record_not_found(self):
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
|
|||||||
@@ -1,8 +1,20 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from basic_info import models as basic_models
|
||||||
from business import services as business_services
|
from business import services as business_services
|
||||||
|
|
||||||
|
|
||||||
|
class StatementWarehouseSerializer(serializers.ModelSerializer):
|
||||||
|
"""对账单中使用的仓库序列化器(输出完整仓库数据)"""
|
||||||
|
|
||||||
|
type_label = serializers.CharField(source='get_type_display', read_only=True)
|
||||||
|
mode_label = serializers.CharField(source='get_mode_display', read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = basic_models.WareHouse
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
class StatementRecordSerializer(serializers.Serializer):
|
class StatementRecordSerializer(serializers.Serializer):
|
||||||
source_type = serializers.CharField()
|
source_type = serializers.CharField()
|
||||||
source_label = serializers.CharField()
|
source_label = serializers.CharField()
|
||||||
@@ -13,6 +25,7 @@ class StatementRecordSerializer(serializers.Serializer):
|
|||||||
status_label = serializers.CharField()
|
status_label = serializers.CharField()
|
||||||
counterparty = serializers.IntegerField()
|
counterparty = serializers.IntegerField()
|
||||||
counterparty_name = serializers.CharField()
|
counterparty_name = serializers.CharField()
|
||||||
|
warehouse = StatementWarehouseSerializer(allow_null=True)
|
||||||
positive_amount = serializers.DecimalField(max_digits=15, decimal_places=2)
|
positive_amount = serializers.DecimalField(max_digits=15, decimal_places=2)
|
||||||
negative_amount = serializers.DecimalField(max_digits=15, decimal_places=2)
|
negative_amount = serializers.DecimalField(max_digits=15, decimal_places=2)
|
||||||
cumulative_amount = serializers.CharField()
|
cumulative_amount = serializers.CharField()
|
||||||
|
|||||||
@@ -1869,6 +1869,7 @@ class _StatementBuilder:
|
|||||||
recorded_at,
|
recorded_at,
|
||||||
status: int,
|
status: int,
|
||||||
status_label: str,
|
status_label: str,
|
||||||
|
warehouse: basic_info_models.WareHouse | None = None,
|
||||||
positive_amount,
|
positive_amount,
|
||||||
negative_amount,
|
negative_amount,
|
||||||
items: List[dict] | None = None,
|
items: List[dict] | None = None,
|
||||||
@@ -1885,6 +1886,8 @@ class _StatementBuilder:
|
|||||||
'recorded_at': recorded_at,
|
'recorded_at': recorded_at,
|
||||||
'status': status,
|
'status': status,
|
||||||
'status_label': status_label,
|
'status_label': status_label,
|
||||||
|
# 注意:收款单/付款单等资金类单据无仓库概念,warehouse 将为 None
|
||||||
|
'warehouse': warehouse,
|
||||||
'positive_amount': _normalize_statement_amount(positive_amount),
|
'positive_amount': _normalize_statement_amount(positive_amount),
|
||||||
'negative_amount': _normalize_statement_amount(negative_amount),
|
'negative_amount': _normalize_statement_amount(negative_amount),
|
||||||
'items': items,
|
'items': items,
|
||||||
@@ -1901,7 +1904,11 @@ class _StatementBuilder:
|
|||||||
product_name = getattr(product, 'name', '')
|
product_name = getattr(product, 'name', '')
|
||||||
unit = getattr(item, 'unit', '')
|
unit = getattr(item, 'unit', '')
|
||||||
price = getattr(item, 'price', Decimal('0'))
|
price = getattr(item, 'price', Decimal('0'))
|
||||||
key = (product_id, product_name, unit, price)
|
# 这些字段均来自订单明细(*OrderItem),用于在对账单中对 items 进行合理聚合:
|
||||||
|
# - 同一产品不同颜色/规格应拆分展示,避免合并后信息丢失
|
||||||
|
color = getattr(item, 'color', None) or ''
|
||||||
|
spec = getattr(item, 'spec', None) or ''
|
||||||
|
key = (product_id, product_name, unit, price, color, spec)
|
||||||
if key not in aggregated:
|
if key not in aggregated:
|
||||||
aggregated[key] = {
|
aggregated[key] = {
|
||||||
'product_id': product_id,
|
'product_id': product_id,
|
||||||
@@ -1909,9 +1916,36 @@ class _StatementBuilder:
|
|||||||
'quantity': Decimal('0'),
|
'quantity': Decimal('0'),
|
||||||
'price': price,
|
'price': price,
|
||||||
'unit': unit,
|
'unit': unit,
|
||||||
|
'color': color,
|
||||||
|
'spec': spec,
|
||||||
|
# 统一输出各条数数量(严格模式下为每条数量列表;非严格模式/未填则为空列表)
|
||||||
|
# 来源于各 *OrderItem 模型新增的 split_quantity_of_rolls() 方法
|
||||||
|
'quantity_of_rolls': [],
|
||||||
|
# 条数:来自各 *OrderItem.num_of_rolls。聚合时对相同产品/单价的多行明细累加。
|
||||||
|
'num_of_rolls': 0,
|
||||||
}
|
}
|
||||||
quantity_value = getattr(item, 'quantity', 0) or 0
|
quantity_value = getattr(item, 'quantity', 0) or 0
|
||||||
aggregated[key]['quantity'] += Decimal(str(quantity_value))
|
aggregated[key]['quantity'] += Decimal(str(quantity_value))
|
||||||
|
|
||||||
|
num_of_rolls_value = getattr(item, 'num_of_rolls', None)
|
||||||
|
try:
|
||||||
|
num_of_rolls_int = int(num_of_rolls_value) if num_of_rolls_value is not None else 0
|
||||||
|
except (TypeError, ValueError): # pragma: no cover - defensive
|
||||||
|
num_of_rolls_int = 0
|
||||||
|
|
||||||
|
split_quantity_of_rolls = getattr(item, 'split_quantity_of_rolls', None)
|
||||||
|
if callable(split_quantity_of_rolls):
|
||||||
|
try:
|
||||||
|
rolls = split_quantity_of_rolls() or []
|
||||||
|
except Exception: # pragma: no cover - defensive
|
||||||
|
rolls = []
|
||||||
|
if rolls:
|
||||||
|
aggregated[key]['quantity_of_rolls'].extend(list(rolls))
|
||||||
|
# 若无 num_of_rolls(或为 0),兜底使用 rolls 长度
|
||||||
|
if not num_of_rolls_int:
|
||||||
|
num_of_rolls_int = len(rolls)
|
||||||
|
|
||||||
|
aggregated[key]['num_of_rolls'] += num_of_rolls_int
|
||||||
return list(aggregated.values())
|
return list(aggregated.values())
|
||||||
|
|
||||||
def _sort_records(self, records: Iterable[dict]) -> List[dict]:
|
def _sort_records(self, records: Iterable[dict]) -> List[dict]:
|
||||||
@@ -1951,7 +1985,7 @@ class _CustomerStatementBuilder(_StatementBuilder):
|
|||||||
customer=customer,
|
customer=customer,
|
||||||
status=models.SalesOrderStatusEnum.APPROVED,
|
status=models.SalesOrderStatusEnum.APPROVED,
|
||||||
)
|
)
|
||||||
.select_related('customer')
|
.select_related('customer', 'warehouse')
|
||||||
.prefetch_related('items__product')
|
.prefetch_related('items__product')
|
||||||
)
|
)
|
||||||
records = []
|
records = []
|
||||||
@@ -1968,6 +2002,7 @@ class _CustomerStatementBuilder(_StatementBuilder):
|
|||||||
recorded_at=order.created_at,
|
recorded_at=order.created_at,
|
||||||
status=order.status,
|
status=order.status,
|
||||||
status_label=order.get_status_display(),
|
status_label=order.get_status_display(),
|
||||||
|
warehouse=order.warehouse,
|
||||||
positive_amount=order.get_total_amount(),
|
positive_amount=order.get_total_amount(),
|
||||||
negative_amount=_STATEMENT_ZERO,
|
negative_amount=_STATEMENT_ZERO,
|
||||||
items=items,
|
items=items,
|
||||||
@@ -1982,7 +2017,7 @@ class _CustomerStatementBuilder(_StatementBuilder):
|
|||||||
customer=customer,
|
customer=customer,
|
||||||
status=models.SalesReturnStatusEnum.APPROVED,
|
status=models.SalesReturnStatusEnum.APPROVED,
|
||||||
)
|
)
|
||||||
.select_related('customer')
|
.select_related('customer', 'warehouse')
|
||||||
.prefetch_related('items__product')
|
.prefetch_related('items__product')
|
||||||
)
|
)
|
||||||
records = []
|
records = []
|
||||||
@@ -1999,6 +2034,7 @@ class _CustomerStatementBuilder(_StatementBuilder):
|
|||||||
recorded_at=order.created_at,
|
recorded_at=order.created_at,
|
||||||
status=order.status,
|
status=order.status,
|
||||||
status_label=order.get_status_display(),
|
status_label=order.get_status_display(),
|
||||||
|
warehouse=order.warehouse,
|
||||||
positive_amount=_STATEMENT_ZERO,
|
positive_amount=_STATEMENT_ZERO,
|
||||||
negative_amount=order.get_total_amount(),
|
negative_amount=order.get_total_amount(),
|
||||||
items=items,
|
items=items,
|
||||||
@@ -2050,7 +2086,7 @@ class _SupplierStatementBuilder(_StatementBuilder):
|
|||||||
supplier=supplier,
|
supplier=supplier,
|
||||||
status=models.PurchaseOrderStatusEnum.APPROVED,
|
status=models.PurchaseOrderStatusEnum.APPROVED,
|
||||||
)
|
)
|
||||||
.select_related('supplier')
|
.select_related('supplier', 'warehouse')
|
||||||
.prefetch_related('items__product')
|
.prefetch_related('items__product')
|
||||||
)
|
)
|
||||||
records = []
|
records = []
|
||||||
@@ -2067,6 +2103,7 @@ class _SupplierStatementBuilder(_StatementBuilder):
|
|||||||
recorded_at=order.created_at,
|
recorded_at=order.created_at,
|
||||||
status=order.status,
|
status=order.status,
|
||||||
status_label=order.get_status_display(),
|
status_label=order.get_status_display(),
|
||||||
|
warehouse=order.warehouse,
|
||||||
positive_amount=order.get_total_amount(),
|
positive_amount=order.get_total_amount(),
|
||||||
negative_amount=_STATEMENT_ZERO,
|
negative_amount=_STATEMENT_ZERO,
|
||||||
items=items,
|
items=items,
|
||||||
@@ -2081,7 +2118,7 @@ class _SupplierStatementBuilder(_StatementBuilder):
|
|||||||
supplier=supplier,
|
supplier=supplier,
|
||||||
status=models.PurchaseReturnStatusEnum.APPROVED,
|
status=models.PurchaseReturnStatusEnum.APPROVED,
|
||||||
)
|
)
|
||||||
.select_related('supplier')
|
.select_related('supplier', 'warehouse')
|
||||||
.prefetch_related('items__product')
|
.prefetch_related('items__product')
|
||||||
)
|
)
|
||||||
records = []
|
records = []
|
||||||
@@ -2098,6 +2135,7 @@ class _SupplierStatementBuilder(_StatementBuilder):
|
|||||||
recorded_at=order.created_at,
|
recorded_at=order.created_at,
|
||||||
status=order.status,
|
status=order.status,
|
||||||
status_label=order.get_status_display(),
|
status_label=order.get_status_display(),
|
||||||
|
warehouse=order.warehouse,
|
||||||
positive_amount=_STATEMENT_ZERO,
|
positive_amount=_STATEMENT_ZERO,
|
||||||
negative_amount=order.get_total_amount(),
|
negative_amount=order.get_total_amount(),
|
||||||
items=items,
|
items=items,
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -60,3 +60,4 @@ PlateOrder 模型当前 **没有 `work_state` 字段**。主要字段如下(
|
|||||||
|
|
||||||
由于 PlateOrder 未包含 `work_state`,前端无需提交该字段;若后续业务需要,可在模型与序列化器中新增字段并同步更新本文档。
|
由于 PlateOrder 未包含 `work_state`,前端无需提交该字段;若后续业务需要,可在模型与序列化器中新增字段并同步更新本文档。
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
231
stateflow/tests/test_business_object_crud_and_filters_api.py
Normal file
231
stateflow/tests/test_business_object_crud_and_filters_api.py
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
"""
|
||||||
|
BusinessObject CRUD + 过滤器 API 测试
|
||||||
|
|
||||||
|
现有测试主要覆盖 BusinessObject 的 action(advance/reset/timeline/state-logs/...),
|
||||||
|
这里补齐 list/retrieve/create/update/delete 以及 FilterSet 的关键分支。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.test import TestCase
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
|
from stateflow import models, services
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
class BusinessObjectCRUDAndFilterAPITestCase(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.user = User.objects.create_user(username="testuser", password="testpass")
|
||||||
|
self.client.force_authenticate(user=self.user)
|
||||||
|
|
||||||
|
# 通用状态/流程(3个节点)
|
||||||
|
self.state1 = models.State.objects.create(name="状态1")
|
||||||
|
self.state2 = models.State.objects.create(name="状态2")
|
||||||
|
self.state3 = models.State.objects.create(name="状态3")
|
||||||
|
|
||||||
|
self.process_a = models.Process.objects.create(name="流程A", description="用于测试过滤器A")
|
||||||
|
models.ProcessNode.objects.create(process=self.process_a, state=self.state1, order=0)
|
||||||
|
models.ProcessNode.objects.create(process=self.process_a, state=self.state2, order=1)
|
||||||
|
models.ProcessNode.objects.create(process=self.process_a, state=self.state3, order=2)
|
||||||
|
|
||||||
|
self.process_b = models.Process.objects.create(name="流程B", description="用于测试过滤器B")
|
||||||
|
models.ProcessNode.objects.create(process=self.process_b, state=self.state1, order=0)
|
||||||
|
|
||||||
|
def test_business_object_crud(self):
|
||||||
|
"""覆盖 create/list/retrieve/patch/delete 的基本 happy path"""
|
||||||
|
# create(注意:create serializer 不包含 id,需要从 DB 获取)
|
||||||
|
create_payload = {
|
||||||
|
"name": "BO-CRUD-1",
|
||||||
|
"process": self.process_a.id,
|
||||||
|
"description": "测试 CRUD",
|
||||||
|
}
|
||||||
|
resp = self.client.post("/api/v1/stateflow/business-objects/", create_payload, format="json")
|
||||||
|
self.assertEqual(resp.status_code, status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
bo = models.BusinessObject.objects.get(name="BO-CRUD-1")
|
||||||
|
self.assertEqual(bo.process_id, self.process_a.id)
|
||||||
|
|
||||||
|
# list
|
||||||
|
resp = self.client.get("/api/v1/stateflow/business-objects/?limit=10&offset=0")
|
||||||
|
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertGreaterEqual(resp.data["count"], 1)
|
||||||
|
|
||||||
|
# list 返回字段校验(挑关键字段)
|
||||||
|
results = resp.data["results"]
|
||||||
|
item = next(x for x in results if x["id"] == bo.id)
|
||||||
|
self.assertEqual(item["name"], "BO-CRUD-1")
|
||||||
|
self.assertEqual(item["process"], self.process_a.id)
|
||||||
|
self.assertEqual(item["process_name"], "流程A")
|
||||||
|
self.assertIn("current_state_name", item)
|
||||||
|
self.assertIn("overall_status", item)
|
||||||
|
self.assertIn("progress_percentage", item)
|
||||||
|
|
||||||
|
# retrieve(详情含 timeline/state_logs 等)
|
||||||
|
resp = self.client.get(f"/api/v1/stateflow/business-objects/{bo.id}/")
|
||||||
|
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(resp.data["id"], bo.id)
|
||||||
|
self.assertEqual(resp.data["name"], "BO-CRUD-1")
|
||||||
|
self.assertIn("process_detail", resp.data)
|
||||||
|
self.assertIn("timeline", resp.data)
|
||||||
|
self.assertIn("state_logs", resp.data)
|
||||||
|
self.assertEqual(len(resp.data["timeline"]), 3)
|
||||||
|
|
||||||
|
# patch 更新 name
|
||||||
|
resp = self.client.patch(
|
||||||
|
f"/api/v1/stateflow/business-objects/{bo.id}/",
|
||||||
|
{"name": "BO-CRUD-1-RENAMED"},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||||
|
bo.refresh_from_db()
|
||||||
|
self.assertEqual(bo.name, "BO-CRUD-1-RENAMED")
|
||||||
|
|
||||||
|
# delete
|
||||||
|
resp = self.client.delete(f"/api/v1/stateflow/business-objects/{bo.id}/")
|
||||||
|
self.assertEqual(resp.status_code, status.HTTP_204_NO_CONTENT)
|
||||||
|
self.assertFalse(models.BusinessObject.objects.filter(id=bo.id).exists())
|
||||||
|
|
||||||
|
def test_create_business_object_with_content_type_str(self):
|
||||||
|
"""覆盖 create serializer 的 content_type_str 解析与一致性校验"""
|
||||||
|
# 正确:提供 content_type_str + object_id
|
||||||
|
resp = self.client.post(
|
||||||
|
"/api/v1/stateflow/business-objects/",
|
||||||
|
{
|
||||||
|
"name": "BO-Linked",
|
||||||
|
"process": self.process_a.id,
|
||||||
|
"content_type_str": "stateflow.process",
|
||||||
|
"object_id": self.process_a.id,
|
||||||
|
},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(resp.status_code, status.HTTP_201_CREATED)
|
||||||
|
bo = models.BusinessObject.objects.get(name="BO-Linked")
|
||||||
|
self.assertIsNotNone(bo.content_type)
|
||||||
|
self.assertEqual(bo.content_type.app_label, "stateflow")
|
||||||
|
self.assertEqual(bo.content_type.model, "process")
|
||||||
|
self.assertEqual(bo.object_id, self.process_a.id)
|
||||||
|
|
||||||
|
# 错误:content_type_str 格式不合法
|
||||||
|
resp = self.client.post(
|
||||||
|
"/api/v1/stateflow/business-objects/",
|
||||||
|
{
|
||||||
|
"name": "BO-Bad-CT",
|
||||||
|
"process": self.process_a.id,
|
||||||
|
"content_type_str": "badformat",
|
||||||
|
"object_id": self.process_a.id,
|
||||||
|
},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertIn("content_type_str", resp.data)
|
||||||
|
|
||||||
|
# 错误:提供 content_type_str 但缺 object_id
|
||||||
|
resp = self.client.post(
|
||||||
|
"/api/v1/stateflow/business-objects/",
|
||||||
|
{
|
||||||
|
"name": "BO-Missing-ObjectId",
|
||||||
|
"process": self.process_a.id,
|
||||||
|
"content_type_str": "stateflow.process",
|
||||||
|
},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertIn("object_id", resp.data)
|
||||||
|
|
||||||
|
def test_business_object_filters(self):
|
||||||
|
"""覆盖 BusinessObjectFilterSet 的关键过滤条件"""
|
||||||
|
bo_in_progress = models.BusinessObject.objects.create(
|
||||||
|
name="BO-InProgress-Alpha",
|
||||||
|
process=self.process_a,
|
||||||
|
description="alpha desc",
|
||||||
|
)
|
||||||
|
bo_completed = models.BusinessObject.objects.create(
|
||||||
|
name="BO-Completed-Beta",
|
||||||
|
process=self.process_a,
|
||||||
|
description="beta desc",
|
||||||
|
)
|
||||||
|
bo_other_process = models.BusinessObject.objects.create(
|
||||||
|
name="BO-OtherProcess-Gamma",
|
||||||
|
process=self.process_b,
|
||||||
|
description="gamma desc",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 让 bo_completed 完成(该流程无必填参数,直接推进即可)
|
||||||
|
services.advance_to_next_state(bo_completed, self.user)
|
||||||
|
services.advance_to_next_state(bo_completed, self.user)
|
||||||
|
services.advance_to_next_state(bo_completed, self.user)
|
||||||
|
self.assertEqual(services.get_overall_status(bo_completed), "completed")
|
||||||
|
|
||||||
|
# name contains
|
||||||
|
resp = self.client.get("/api/v1/stateflow/business-objects/?name=Alpha&limit=10&offset=0")
|
||||||
|
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(resp.data["count"], 1)
|
||||||
|
self.assertEqual(resp.data["results"][0]["id"], bo_in_progress.id)
|
||||||
|
|
||||||
|
# process id
|
||||||
|
resp = self.client.get(f"/api/v1/stateflow/business-objects/?process={self.process_b.id}&limit=10&offset=0")
|
||||||
|
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(resp.data["count"], 1)
|
||||||
|
self.assertEqual(resp.data["results"][0]["id"], bo_other_process.id)
|
||||||
|
|
||||||
|
# process_name contains
|
||||||
|
resp = self.client.get("/api/v1/stateflow/business-objects/?process_name=流程A&limit=10&offset=0")
|
||||||
|
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(resp.data["count"], 2)
|
||||||
|
returned_ids = {x["id"] for x in resp.data["results"]}
|
||||||
|
self.assertSetEqual(returned_ids, {bo_in_progress.id, bo_completed.id})
|
||||||
|
|
||||||
|
# overall_status=completed
|
||||||
|
resp = self.client.get("/api/v1/stateflow/business-objects/?overall_status=completed&limit=10&offset=0")
|
||||||
|
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(resp.data["count"], 1)
|
||||||
|
self.assertEqual(resp.data["results"][0]["id"], bo_completed.id)
|
||||||
|
|
||||||
|
# overall_status=in_progress
|
||||||
|
resp = self.client.get("/api/v1/stateflow/business-objects/?overall_status=in_progress&limit=10&offset=0")
|
||||||
|
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||||
|
returned_ids = {x["id"] for x in resp.data["results"]}
|
||||||
|
self.assertSetEqual(returned_ids, {bo_in_progress.id, bo_other_process.id})
|
||||||
|
|
||||||
|
# content_type_str + has_content_object
|
||||||
|
ct = ContentType.objects.get_for_model(models.Process)
|
||||||
|
bo_linked = models.BusinessObject.objects.create(
|
||||||
|
name="BO-Linked-Process",
|
||||||
|
process=self.process_a,
|
||||||
|
content_type=ct,
|
||||||
|
object_id=self.process_a.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
resp = self.client.get("/api/v1/stateflow/business-objects/?has_content_object=true&limit=10&offset=0")
|
||||||
|
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||||
|
returned_ids = {x["id"] for x in resp.data["results"]}
|
||||||
|
self.assertIn(bo_linked.id, returned_ids)
|
||||||
|
|
||||||
|
resp = self.client.get("/api/v1/stateflow/business-objects/?has_content_object=false&limit=10&offset=0")
|
||||||
|
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||||
|
returned_ids = {x["id"] for x in resp.data["results"]}
|
||||||
|
self.assertNotIn(bo_linked.id, returned_ids)
|
||||||
|
|
||||||
|
resp = self.client.get("/api/v1/stateflow/business-objects/?content_type_str=stateflow.process&limit=10&offset=0")
|
||||||
|
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||||
|
returned_ids = {x["id"] for x in resp.data["results"]}
|
||||||
|
self.assertSetEqual(returned_ids, {bo_linked.id})
|
||||||
|
|
||||||
|
# 无效 content_type_str:返回空
|
||||||
|
resp = self.client.get("/api/v1/stateflow/business-objects/?content_type_str=badformat&limit=10&offset=0")
|
||||||
|
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(resp.data["count"], 0)
|
||||||
|
|
||||||
|
|
||||||
|
class AuthenticationGuardAPITestCase(TestCase):
|
||||||
|
def test_stateflow_endpoints_require_authentication(self):
|
||||||
|
"""默认权限 IsAuthenticated:未登录应 401"""
|
||||||
|
client = APIClient()
|
||||||
|
resp = client.get("/api/v1/stateflow/states/?limit=1&offset=0")
|
||||||
|
self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
|
||||||
39
stateflow/tests/test_process_list_node_count_api.py
Normal file
39
stateflow/tests/test_process_list_node_count_api.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
"""
|
||||||
|
补充 Process 列表接口的 node_count 字段覆盖
|
||||||
|
|
||||||
|
ProcessViewSet.list() 会对 queryset annotate(node_count),对应 ProcessListSerializer 的只读字段。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.test import TestCase
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
|
from stateflow import models
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
class ProcessListNodeCountAPITestCase(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.user = User.objects.create_user(username="testuser", password="testpass")
|
||||||
|
self.client.force_authenticate(user=self.user)
|
||||||
|
|
||||||
|
self.state1 = models.State.objects.create(name="状态1")
|
||||||
|
self.state2 = models.State.objects.create(name="状态2")
|
||||||
|
|
||||||
|
def test_process_list_contains_node_count(self):
|
||||||
|
process = models.Process.objects.create(name="带节点计数的流程")
|
||||||
|
models.ProcessNode.objects.create(process=process, state=self.state1, order=0)
|
||||||
|
models.ProcessNode.objects.create(process=process, state=self.state2, order=1)
|
||||||
|
|
||||||
|
resp = self.client.get("/api/v1/stateflow/processes/?limit=10&offset=0")
|
||||||
|
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(resp.data["count"], 1)
|
||||||
|
|
||||||
|
item = resp.data["results"][0]
|
||||||
|
self.assertIn("node_count", item)
|
||||||
|
self.assertEqual(item["node_count"], 2)
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user