diff --git a/api_v1/serializers.py b/api_v1/serializers.py index 64aa625..8ff75ba 100644 --- a/api_v1/serializers.py +++ b/api_v1/serializers.py @@ -121,7 +121,8 @@ class StockChangeRecordResponseSerializer(serializers.ModelSerializer): model = stock_models.StockChangeRecord fields = [ 'id', 'type', 'warehouse', 'source_type', 'source_id', - 'is_finished', 'finished_at', 'created_at', 'updated_at' + 'is_finished', 'finished_at', 'cancelled', 'is_offset', 'is_reversed', + 'created_at', 'updated_at' ] diff --git a/api_v1/views/stock_change_views/mixins.py b/api_v1/views/stock_change_views/mixins.py index 13ddbac..5d6ed85 100644 --- a/api_v1/views/stock_change_views/mixins.py +++ b/api_v1/views/stock_change_views/mixins.py @@ -58,6 +58,9 @@ class StockChangeViewMixin: 'source_id': record.source_id, 'is_finished': record.is_finished, 'finished_at': record.finished_at, + 'cancelled': record.cancelled, + 'is_offset': record.is_offset, + 'is_reversed': record.is_reversed, 'created_at': record.created_at, 'created_by': record.created_by.username if record.created_by else None, 'remarks': record.remarks, diff --git a/api_v1/views/stock_change_views/test_stock_change_api.py b/api_v1/views/stock_change_views/test_stock_change_api.py index 4c627e3..282d660 100644 --- a/api_v1/views/stock_change_views/test_stock_change_api.py +++ b/api_v1/views/stock_change_views/test_stock_change_api.py @@ -4,6 +4,7 @@ import logging import unittest from django.contrib.auth import get_user_model +from django.utils import timezone from django.test import TestCase from rest_framework import status from rest_framework.test import APIClient @@ -139,6 +140,7 @@ class CreateStockChangeAPITestCase(TestCase): logging.disable(logging.NOTSET) self.addCleanup(logging.disable, logging.CRITICAL) + with self.assertLogs('api_v1.views.stock_change_views.mixins', level='ERROR') as cm: response = self.client.post('/api/v1/stock-change/', payload, format='json') @@ -146,6 +148,70 @@ class CreateStockChangeAPITestCase(TestCase): self.assertEqual(response.data['error'], '无权限访问') self.assertTrue(any('无员工信息' in msg for msg in cm.output)) + def test_get_stock_change_includes_offset_status_fields(self): + 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, + ) + detail = stock_models.StockChangeDetail.objects.create( + merchant=self.merchant, + stock_change_record=record, + product=self.product, + quantity='10', + unit=self.product.unit, + ) + snapshot = stock_models.StockSnapshot.objects.create( + merchant=self.merchant, + product=self.product, + warehouse=self.warehouse, + delta='10', + quantity_before='0', + quantity_after='10', + stock_change_record=record, + unit=self.product.unit, + num_of_rolls=1, + cancelled=True, + cancelled_at=timezone.now(), + offset_id=1, + ) + + offset_record = stock_models.StockChangeRecord.objects.create( + merchant=self.merchant, + type=stock_models.StockChangeTypeEnum.ADD, + warehouse=self.warehouse, + source_type=stock_models.StockChangeSourceEnum.OFFSET, + created_by=self.user, + ) + stock_models.StockSnapshot.objects.create( + merchant=self.merchant, + product=self.product, + warehouse=self.warehouse, + delta='-10', + quantity_before='10', + quantity_after='0', + stock_change_record=offset_record, + unit=self.product.unit, + num_of_rolls=1, + offset_to=snapshot.id, + offset_at=timezone.now(), + ) + + response = self.client.get(f'/api/v1/stock-change/{record.id}/') + self.assertEqual(response.status_code, status.HTTP_200_OK) + record_data = response.data['stock_change_record'] + self.assertTrue(record_data['cancelled']) + self.assertTrue(record_data['is_reversed']) + self.assertFalse(record_data['is_offset']) + + offset_response = self.client.get(f'/api/v1/stock-change/{offset_record.id}/') + self.assertEqual(offset_response.status_code, status.HTTP_200_OK) + offset_data = offset_response.data['stock_change_record'] + self.assertTrue(offset_data['is_offset']) + self.assertFalse(offset_data['is_reversed']) + class CreateStockChangeRelaxedAPITestCase(TestCase): """测试宽松模式库存变动创建 API""" diff --git a/docs/purchase_order_approval_and_red_flush.md b/docs/purchase_order_approval_and_red_flush.md index 4e7141f..4927caa 100644 --- a/docs/purchase_order_approval_and_red_flush.md +++ b/docs/purchase_order_approval_and_red_flush.md @@ -100,3 +100,15 @@ 通过以上步骤,审批流程与红冲机制可以衔接:审批负责“正向入库”与任务分发,红冲负责在业务需要时生成配对的反向库存记录,保持库存账实一致,同时具备可回溯、可审计的业务闭环。 +--- + +## 8. 库存记录红冲标识字段 + +库存变动记录的 API 响应新增以下字段,用于前端识别红冲关系: + +- `cancelled`:是否已被红冲(等价于 `is_reversed`) +- `is_reversed`:当前记录是否已被红冲 +- `is_offset`:当前记录是否为红冲记录(用于冲抵其他记录) + +说明:红冲关系来源于库存快照的 `offset_to/offset_id/cancelled` 字段,记录本身不落库状态字段。 + diff --git a/stock/models.py b/stock/models.py index 673f067..dd73fab 100644 --- a/stock/models.py +++ b/stock/models.py @@ -133,6 +133,21 @@ class StockChangeRecord(ModelBase): self.details.count() return self.details.count() + @property + def is_offset(self) -> bool: + """是否为红冲记录(用于冲抵其他记录)""" + return self.stock_snapshots.filter(offset_to__isnull=False).exists() + + @property + def is_reversed(self) -> bool: + """是否已被红冲""" + return self.stock_snapshots.filter(cancelled=True).exists() + + @property + def cancelled(self) -> bool: + """兼容字段:是否已被红冲""" + return self.is_reversed + @property def delta_num_of_rolls(self) -> int: """计算库存变动的匹数增减"""