diff --git a/api_v1/tests.py b/api_v1/tests.py index 136b314..3698291 100644 --- a/api_v1/tests.py +++ b/api_v1/tests.py @@ -220,8 +220,12 @@ class PurchaseOrderAPITestCase(TestCase): self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertIn('id', response.data) + self.assertIn('human_id', response.data) self.assertEqual(response.data['status'], 1) self.assertIn('等待审批', response.data['message']) + order = business_models.PurchaseOrder.objects.get(id=response.data['id']) + expected_human_id = f"{order.created_at.strftime('%Y%m%d')}{order.id:06d}" + self.assertEqual(response.data['human_id'], expected_human_id) mock_delay.assert_not_called() def test_create_purchase_order_invalid_supplier(self): @@ -239,7 +243,11 @@ class PurchaseOrderAPITestCase(TestCase): with patch('business.services.create_purchase_order_stock_entries.delay') as mock_delay: response = self.client.post('/api/v1/purchase-orders/', self.relaxed_payload, format='json') self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertIn('human_id', response.data) self.assertEqual(response.data['status'], 1) + order = business_models.PurchaseOrder.objects.get(id=response.data['id']) + expected_human_id = f"{order.created_at.strftime('%Y%m%d')}{order.id:06d}" + self.assertEqual(response.data['human_id'], expected_human_id) mock_delay.assert_not_called() def test_mode_mismatch_raises(self): @@ -652,7 +660,11 @@ class PurchaseReturnOrderAPITestCase(TestCase): response = self.client.post('/api/v1/purchase-return-orders/', self.strict_payload, format='json') self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertIn('id', response.data) + self.assertIn('human_id', response.data) self.assertEqual(response.data['status'], business_models.PurchaseReturnStatusEnum.PENDING) + order = business_models.PurchaseReturnOrder.objects.get(id=response.data['id']) + expected_human_id = f"{order.created_at.strftime('%Y%m%d')}{order.id:06d}" + self.assertEqual(response.data['human_id'], expected_human_id) def test_create_purchase_return_requires_consume_ids_in_strict_out(self): payload = copy.deepcopy(self.strict_out_payload) @@ -802,7 +814,11 @@ class SalesReturnOrderAPITestCase(TestCase): def test_create_sales_return_order_success(self): response = self.client.post('/api/v1/sales-return-orders/', self.strict_payload, format='json') self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertIn('human_id', response.data) self.assertEqual(response.data['status'], business_models.SalesReturnStatusEnum.PENDING) + order = business_models.SalesReturnOrder.objects.get(id=response.data['id']) + expected_human_id = f"{order.created_at.strftime('%Y%m%d')}{order.id:06d}" + self.assertEqual(response.data['human_id'], expected_human_id) def test_create_sales_return_order_invalid_customer(self): payload = {**self.strict_payload, 'customer': 999} diff --git a/api_v1/views/business/purchase/views.py b/api_v1/views/business/purchase/views.py index c431974..08bf93b 100644 --- a/api_v1/views/business/purchase/views.py +++ b/api_v1/views/business/purchase/views.py @@ -28,6 +28,7 @@ class PurchaseOrderSerializer(serializers.ModelSerializer): diff_quantity = serializers.SerializerMethodField(read_only=True) total_quantity = serializers.SerializerMethodField(read_only=True) quantity_of_rolls = serializers.SerializerMethodField(read_only=True) + human_id = serializers.CharField(read_only=True) def get_total_amount(self, obj: business_models.PurchaseOrder): return obj.get_total_amount() @@ -49,7 +50,7 @@ class PurchaseOrderSerializer(serializers.ModelSerializer): class Meta: model = business_models.PurchaseOrder fields = [ - 'id', 'supplier', 'supplier_name', 'purchase_date', 'kind', + 'id', 'human_id', 'supplier', 'supplier_name', 'purchase_date', 'kind', 'total_amount', 'diff_quantity', 'total_quantity', 'operator', 'operator_name', 'warehouse', 'warehouse_name', 'status', 'remarks', 'created_at', 'updated_at', 'items', @@ -133,6 +134,7 @@ class PurchaseOrderView(StockChangeViewMixin, views.APIView): return Response( { 'id': purchase_order.id, + 'human_id': purchase_order.human_id, 'status': purchase_order.status, 'message': '采购单创建成功,等待审批', }, diff --git a/api_v1/views/business/purchase_return/views.py b/api_v1/views/business/purchase_return/views.py index d693518..81b0aed 100644 --- a/api_v1/views/business/purchase_return/views.py +++ b/api_v1/views/business/purchase_return/views.py @@ -33,6 +33,7 @@ class PurchaseReturnOrderSerializer(serializers.ModelSerializer): diff_quantity = serializers.SerializerMethodField(read_only=True) total_quantity = serializers.SerializerMethodField(read_only=True) quantity_of_rolls = serializers.SerializerMethodField(read_only=True) + human_id = serializers.CharField(read_only=True) supplier_name = serializers.CharField(source='supplier.name', read_only=True) operator_name = serializers.CharField(source='operator.name', read_only=True) warehouse_name = serializers.CharField(source='warehouse.name', read_only=True) @@ -53,7 +54,7 @@ class PurchaseReturnOrderSerializer(serializers.ModelSerializer): class Meta: model = business_models.PurchaseReturnOrder fields = [ - 'id', 'supplier', 'supplier_name', 'purchase_order', 'return_date', + 'id', 'human_id', 'supplier', 'supplier_name', 'purchase_order', 'return_date', 'total_amount', 'diff_quantity', 'total_quantity', 'operator', 'operator_name', 'warehouse', 'warehouse_name', 'status', 'remarks', 'created_at', 'updated_at', 'items', @@ -140,6 +141,7 @@ class PurchaseReturnOrderView(StockChangeViewMixin, views.APIView): return Response( { 'id': purchase_return.id, + 'human_id': purchase_return.human_id, 'status': purchase_return.status, 'message': '采购退货单创建成功,等待审批', }, diff --git a/api_v1/views/business/sales/views.py b/api_v1/views/business/sales/views.py index 740fec6..98d3f89 100644 --- a/api_v1/views/business/sales/views.py +++ b/api_v1/views/business/sales/views.py @@ -53,7 +53,7 @@ class SalesOrderSerializer(serializers.ModelSerializer): 'id', 'human_id', 'customer', 'customer_name', 'sales_date', 'kind', 'total_amount', 'diff_quantity', 'total_quantity', 'operator', 'operator_name', 'warehouse', 'warehouse_name', - 'status', 'remarks', 'created_at', 'updated_at', 'items', + 'status', 'order_quantity', 'remarks', 'created_at', 'updated_at', 'items', 'quantity_of_rolls', ] read_only_fields = ['id', 'created_at', 'updated_at', 'items', 'customer_name', 'operator_name', 'warehouse_name'] diff --git a/api_v1/views/business/sales_return/views.py b/api_v1/views/business/sales_return/views.py index 281c0dd..8939182 100644 --- a/api_v1/views/business/sales_return/views.py +++ b/api_v1/views/business/sales_return/views.py @@ -32,6 +32,7 @@ class SalesReturnOrderSerializer(serializers.ModelSerializer): diff_quantity = serializers.SerializerMethodField(read_only=True) total_quantity = serializers.SerializerMethodField(read_only=True) quantity_of_rolls = serializers.SerializerMethodField(read_only=True) + human_id = serializers.CharField(read_only=True) customer_name = serializers.CharField(source='customer.name', read_only=True) operator_name = serializers.CharField(source='operator.name', read_only=True) warehouse_name = serializers.CharField(source='warehouse.name', read_only=True) @@ -52,7 +53,7 @@ class SalesReturnOrderSerializer(serializers.ModelSerializer): class Meta: model = business_models.SalesReturnOrder fields = [ - 'id', 'customer', 'customer_name', 'sales_order', 'return_date', + 'id', 'human_id', 'customer', 'customer_name', 'sales_order', 'return_date', 'total_amount', 'diff_quantity', 'total_quantity', 'quantity_of_rolls', 'operator', 'operator_name', 'warehouse', 'warehouse_name', 'status', 'remarks', 'created_at', 'updated_at', 'items', @@ -138,6 +139,7 @@ class SalesReturnOrderView(StockChangeViewMixin, views.APIView): return Response( { 'id': sales_return.id, + 'human_id': sales_return.human_id, 'status': sales_return.status, 'message': '销售退货单创建成功,等待审批', }, diff --git a/business/admin.py b/business/admin.py index 8ad4066..c0e90d0 100644 --- a/business/admin.py +++ b/business/admin.py @@ -127,7 +127,7 @@ class SalesReturnOrderItemAdmin(admin.ModelAdmin): @admin.register(models.SalesOrder) class SalesOrderAdmin(admin.ModelAdmin): - list_display = ('id', 'human_id', 'customer', 'sales_date', 'operator', 'status', '_total_amount', '_diff_quantity', '_total_quantity', 'created_at') + list_display = ('id', 'human_id', 'customer', 'sales_date', 'operator', 'status', 'order_quantity', '_total_amount', '_diff_quantity', '_total_quantity', 'created_at') search_fields = ('customer__name',) list_filter = ('operator', 'warehouse', 'status', 'created_at') ordering = ('-created_at',) diff --git a/business/migrations/0020_salesorder_order_quantity.py b/business/migrations/0020_salesorder_order_quantity.py new file mode 100644 index 0000000..af75852 --- /dev/null +++ b/business/migrations/0020_salesorder_order_quantity.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2026-01-30 10:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('business', '0019_alter_salesorderitem_quantity_of_rolls'), + ] + + operations = [ + migrations.AddField( + model_name='salesorder', + name='order_quantity', + field=models.IntegerField(blank=True, null=True, verbose_name='下单数量'), + ), + ] diff --git a/business/models.py b/business/models.py index f31ba49..309efc7 100644 --- a/business/models.py +++ b/business/models.py @@ -133,6 +133,17 @@ class PurchaseOrder(OrderItemsAggregationMixin, OrderDirectionMixin, OrderCounte def __str__(self): return f'采购订单 {self.id} - {self.supplier.name}' + @property + def human_id(self) -> str: + """ + 人类可读编号(虚拟编号,不落库)。 + + 规则与 printing.PrintingOrder.human_id / SalesOrder.human_id 保持一致:YYYYMMDD{6位id} + """ + if self.id is None or self.created_at is None: + return "" + return f"{self.created_at.year}{self.created_at.month:02d}{self.created_at.day:02d}{self.id:06d}" + class Meta: verbose_name = '采购单' verbose_name_plural = '采购单' @@ -252,6 +263,7 @@ class SalesOrder(OrderItemsAggregationMixin, OrderDirectionMixin, OrderCounterpa default=SalesOrderStatusEnum.PENDING, verbose_name='状态', ) + order_quantity = models.IntegerField(null=True, blank=True, verbose_name='下单数量') remarks = models.TextField(blank=True, null=True, verbose_name='备注') def __str__(self): @@ -405,6 +417,17 @@ class PurchaseReturnOrder(OrderItemsAggregationMixin, OrderDirectionMixin, Order def __str__(self): return f'采购退货 {self.id} - {self.supplier.name}' + @property + def human_id(self) -> str: + """ + 人类可读编号(虚拟编号,不落库)。 + + 规则与 printing.PrintingOrder.human_id / SalesOrder.human_id 保持一致:YYYYMMDD{6位id} + """ + if self.id is None or self.created_at is None: + return "" + return f"{self.created_at.year}{self.created_at.month:02d}{self.created_at.day:02d}{self.id:06d}" + def get_direction(self) -> int: return -1 @@ -519,6 +542,17 @@ class SalesReturnOrder(OrderItemsAggregationMixin, OrderDirectionMixin, OrderCou def __str__(self): return f'销售退货 {self.id} - {self.customer.name}' + @property + def human_id(self) -> str: + """ + 人类可读编号(虚拟编号,不落库)。 + + 规则与 printing.PrintingOrder.human_id / SalesOrder.human_id 保持一致:YYYYMMDD{6位id} + """ + if self.id is None or self.created_at is None: + return "" + return f"{self.created_at.year}{self.created_at.month:02d}{self.created_at.day:02d}{self.id:06d}" + def get_direction(self) -> int: return 1 diff --git a/business/tests.py b/business/tests.py index 3c85d2d..ed369e9 100644 --- a/business/tests.py +++ b/business/tests.py @@ -150,6 +150,22 @@ class PurchaseOrderServiceTestCase(TestCase): self.assertEqual(item.split_quantity_of_rolls(), [10, 5]) mock_delay.assert_not_called() + def test_purchase_order_human_id_generation(self): + purchase_order = services.create_purchase_order( + merchant=self.merchant, + supplier=self.supplier, + order_date=timezone.now().date(), + warehouse=self.warehouse_strict, + operator=self.operator, + items=self.strict_items, + created_by=self.user, + ) + self.assertIsNotNone(purchase_order.human_id) + self.assertEqual( + purchase_order.human_id, + f"{purchase_order.created_at.strftime('%Y%m%d')}{purchase_order.id:06d}", + ) + def test_review_purchase_order_approval_triggers_task(self): purchase_order = services.create_purchase_order( merchant=self.merchant, @@ -605,6 +621,22 @@ class PurchaseReturnServiceTestCase(TestCase): self.assertEqual(item.quantity_of_rolls, '4,2') self.assertEqual(item.split_quantity_of_rolls(), [4, 2]) + def test_purchase_return_order_human_id_generation(self): + order = services.create_purchase_return_order( + merchant=self.merchant, + supplier=self.supplier, + return_date=timezone.now().date(), + warehouse=self.warehouse_strict, + operator=self.operator, + items=self.strict_items, + created_by=self.user, + ) + self.assertIsNotNone(order.human_id) + self.assertEqual( + order.human_id, + f"{order.created_at.strftime('%Y%m%d')}{order.id:06d}", + ) + def test_review_purchase_return_order_triggers_task_and_balance(self): order = services.create_purchase_return_order( merchant=self.merchant, @@ -744,6 +776,22 @@ class SalesReturnServiceTestCase(TestCase): item = order.items.first() self.assertEqual(item.split_quantity_of_rolls(), []) + def test_sales_return_order_human_id_generation(self): + order = services.create_sales_return_order( + merchant=self.merchant, + customer=self.customer, + return_date=timezone.now().date(), + warehouse=self.warehouse_relaxed, + operator=self.operator, + items=[{'product_id': self.product.id, 'quantity': 50, 'num_of_rolls': 2, 'price': '19.5'}], + created_by=self.user, + ) + self.assertIsNotNone(order.human_id) + self.assertEqual( + order.human_id, + f"{order.created_at.strftime('%Y%m%d')}{order.id:06d}", + ) + def test_review_sales_return_order_updates_balance(self): order = services.create_sales_return_order( merchant=self.merchant,