From 4c9b8e6a4234e245900c419d09328313258e346c Mon Sep 17 00:00:00 2001 From: colaftc Date: Fri, 12 Dec 2025 17:06:17 +0800 Subject: [PATCH] fix: added some fields to business.statements api --- api_v1/tests.py | 24 ++ .../views/business/statements/serializers.py | 13 + business/services.py | 48 +++- celerybeat-schedule-shm | Bin 32768 -> 32768 bytes celerybeat-schedule-wal | Bin 4120032 -> 4120032 bytes docs/printing_fields_20251212.md | 1 + ...st_business_object_crud_and_filters_api.py | 231 ++++++++++++++++++ .../tests/test_process_list_node_count_api.py | 39 +++ 8 files changed, 351 insertions(+), 5 deletions(-) create mode 100644 stateflow/tests/test_business_object_crud_and_filters_api.py create mode 100644 stateflow/tests/test_process_list_node_count_api.py diff --git a/api_v1/tests.py b/api_v1/tests.py index 243e00e..e4211a6 100644 --- a/api_v1/tests.py +++ b/api_v1/tests.py @@ -1283,9 +1283,13 @@ class StatementRecordAPITestCase(TestCase): sales_order=self.sales_order, product=self.product, price=Decimal('12.50'), + color='红', quantity=Decimal('5'), unit='米', empty_diff_percent=Decimal('0'), + quantity_of_rolls='2,3', + num_of_rolls=2, + spec='32S', ) self.purchase_order = business_models.PurchaseOrder.objects.create( @@ -1300,9 +1304,13 @@ class StatementRecordAPITestCase(TestCase): purchase_order=self.purchase_order, product=self.product, price=Decimal('8.30'), + color='蓝', quantity=Decimal('10'), unit='米', empty_diff_percent=Decimal('0'), + quantity_of_rolls='6,4', + num_of_rolls=2, + spec='40S', ) 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_id'], self.sales_order.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): response = self.client.get( @@ -1339,6 +1355,14 @@ class StatementRecordAPITestCase(TestCase): self.assertEqual(record['source_type'], 'purchase_order') self.assertEqual(record['source_id'], self.purchase_order.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): response = self.client.get( diff --git a/api_v1/views/business/statements/serializers.py b/api_v1/views/business/statements/serializers.py index 0029d7d..59fab22 100644 --- a/api_v1/views/business/statements/serializers.py +++ b/api_v1/views/business/statements/serializers.py @@ -1,8 +1,20 @@ from rest_framework import serializers +from basic_info import models as basic_models 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): source_type = serializers.CharField() source_label = serializers.CharField() @@ -13,6 +25,7 @@ class StatementRecordSerializer(serializers.Serializer): status_label = serializers.CharField() counterparty = serializers.IntegerField() counterparty_name = serializers.CharField() + warehouse = StatementWarehouseSerializer(allow_null=True) positive_amount = serializers.DecimalField(max_digits=15, decimal_places=2) negative_amount = serializers.DecimalField(max_digits=15, decimal_places=2) cumulative_amount = serializers.CharField() diff --git a/business/services.py b/business/services.py index d9669d0..f3c24da 100644 --- a/business/services.py +++ b/business/services.py @@ -1869,6 +1869,7 @@ class _StatementBuilder: recorded_at, status: int, status_label: str, + warehouse: basic_info_models.WareHouse | None = None, positive_amount, negative_amount, items: List[dict] | None = None, @@ -1885,6 +1886,8 @@ class _StatementBuilder: 'recorded_at': recorded_at, 'status': status, 'status_label': status_label, + # 注意:收款单/付款单等资金类单据无仓库概念,warehouse 将为 None + 'warehouse': warehouse, 'positive_amount': _normalize_statement_amount(positive_amount), 'negative_amount': _normalize_statement_amount(negative_amount), 'items': items, @@ -1901,7 +1904,11 @@ class _StatementBuilder: product_name = getattr(product, 'name', '') unit = getattr(item, 'unit', '') 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: aggregated[key] = { 'product_id': product_id, @@ -1909,9 +1916,36 @@ class _StatementBuilder: 'quantity': Decimal('0'), 'price': price, '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 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()) def _sort_records(self, records: Iterable[dict]) -> List[dict]: @@ -1951,7 +1985,7 @@ class _CustomerStatementBuilder(_StatementBuilder): customer=customer, status=models.SalesOrderStatusEnum.APPROVED, ) - .select_related('customer') + .select_related('customer', 'warehouse') .prefetch_related('items__product') ) records = [] @@ -1968,6 +2002,7 @@ class _CustomerStatementBuilder(_StatementBuilder): recorded_at=order.created_at, status=order.status, status_label=order.get_status_display(), + warehouse=order.warehouse, positive_amount=order.get_total_amount(), negative_amount=_STATEMENT_ZERO, items=items, @@ -1982,7 +2017,7 @@ class _CustomerStatementBuilder(_StatementBuilder): customer=customer, status=models.SalesReturnStatusEnum.APPROVED, ) - .select_related('customer') + .select_related('customer', 'warehouse') .prefetch_related('items__product') ) records = [] @@ -1999,6 +2034,7 @@ class _CustomerStatementBuilder(_StatementBuilder): recorded_at=order.created_at, status=order.status, status_label=order.get_status_display(), + warehouse=order.warehouse, positive_amount=_STATEMENT_ZERO, negative_amount=order.get_total_amount(), items=items, @@ -2050,7 +2086,7 @@ class _SupplierStatementBuilder(_StatementBuilder): supplier=supplier, status=models.PurchaseOrderStatusEnum.APPROVED, ) - .select_related('supplier') + .select_related('supplier', 'warehouse') .prefetch_related('items__product') ) records = [] @@ -2067,6 +2103,7 @@ class _SupplierStatementBuilder(_StatementBuilder): recorded_at=order.created_at, status=order.status, status_label=order.get_status_display(), + warehouse=order.warehouse, positive_amount=order.get_total_amount(), negative_amount=_STATEMENT_ZERO, items=items, @@ -2081,7 +2118,7 @@ class _SupplierStatementBuilder(_StatementBuilder): supplier=supplier, status=models.PurchaseReturnStatusEnum.APPROVED, ) - .select_related('supplier') + .select_related('supplier', 'warehouse') .prefetch_related('items__product') ) records = [] @@ -2098,6 +2135,7 @@ class _SupplierStatementBuilder(_StatementBuilder): recorded_at=order.created_at, status=order.status, status_label=order.get_status_display(), + warehouse=order.warehouse, positive_amount=_STATEMENT_ZERO, negative_amount=order.get_total_amount(), items=items, diff --git a/celerybeat-schedule-shm b/celerybeat-schedule-shm index db8ea77f5d247f417636e07fa4eafae29058bc22..dbac8c11e83a941b431ce97d554558c04a2b6419 100644 GIT binary patch delta 275 zcmb7+yH3Jj0EK@)7D*H0Ch-NB+#o3rVBjHK*;$=vTr_ba=>r&M7xPcu@KzKBM8yjr z)Czb36tF@~NEC>JtKV>Pa&q!%8k$DJZ!e1}agPvlLM*rlF{xthE2tQO(2{PbLpv2& zonP$jY;48W|NEcGCo%PY|KWGvO?Z{$wk03v> z>o{~tRSz~CNPx`!j|8CN8|yi_H)rrnv7Fq%^I-EI*8@zGKY812{^fCiY4ayXdxV)h37yA z^C1EvAqw;m4GX{k&w~+SAQl#a>Fgp?nr!^`iPc+Aoz1Rsne>;vKUK{ikn-}q^m3i(6nGtj@&^juaE&c8N<5i)=!#r!2*b4wTw8urD-E{3mWONqLAB#|Miy_$ z8e4&D3vjLL#E@&M?)n6-eTZs@vNXR6hX`{rFOfzy!O@FwTJ2N zn$;)KA^#+Im|Npf)QcY!5Bw=of54&tBJ_0O7xy;zxVQ$Kr0D%`$GXm)-bdq3M-ci$ zTZZDxzHtxkw1^bFXY{KXSLTwN8 zD;l%vxI5iTiry&r-L%6l&|vftZHwmXsr11HPl=R!`Yuvby{0cV!QHzDqmNOi_H=i4 z;nz;?PD@D9yH$Of(dl&@I!O-~MeC`wYg>Nx{Ka)zN{Vi@*A@TqMhrLp6s?em^wj45 zS()9>-RVA3)P0-(WBaP+O?dn#X#AMujNT7*1KiUuBSiyE()U7hzp-F+8ljDG9amQm zcUbVt@5ZQ-S$%#px8_ys?#~?tE#?k)pxrM!l^SU3>{*G?8H8SMxhSA7&lF(v9a8l5 z{oSe$lm@w|%+sDAp7an6$22^)dC4FZr$5tH-b@qT_y(GhgSQMg&J-?9u delta 2365 zcmb8w`%hbC9LMoqIc;fa>E$5cxK1qtq}@vCSc|xk$u>m8)`&5LmI4t^v^QV*DL<he?DmG5*W6$DESKeieRck8Cc}BY zQpe-*gj!EI+vF+nY}x4@GaI^=4tcrDhmU#da|~^g0~nRKD*rdAu@Zkbp0ZR;WG0;(5F6K+px z?v(l^dc&lg&!c9KNji$ge`)UL*f}Mihw2uG?e7is?+b~_r&c$e5?%WEWkn{cd{pCl zoBtYYO*Io$K&=WaVtr0Q%Xg>>P_0eLubMk;@ex%>tyX8T{p*6(t*8o7ooMZzvBsP- z5miL3&S-~4w)lm;sESb4`_(sHalg8WDyCKo&DHN!_zuRRDn_+^ch%j{=gQ}a8bMT+ zyRy8s%eV6dyt0djf+Nd^`FLex9lqIfgC{dkjX?E}nOhS>%qlxk8EW-xo5N6*IsGrH z462*b&t+YRDq2ZY3AOsOyfXe$XGJcm5>%hMy5lF$H^%)$l~SudcVE>-YvmbKrKrAh zx^^EkaAGr2Wz?#zY4}D!rqrVd{_zs5m?_ zK~y=lnmID{%%8_ycko%|s5VN2>pNQuVu-4sQf;QA%=XJ;d9&j`AgVz0+m7xQ@3n${ zf<{rHi+`BkOHD7nlJ{d5s!>ZSV`?AxsOQMEpV1qBlk=6-=nebqbrG~@A*%FlPcx=4 zMVp_1 z=p`s4s(Mj13MC8K7dqH`2NSVXW4ld16yS-5dq}GxZ6m81``~`tacni%O7=FJ(iG!w zkXB3D+r