From 8846446ecf47abb983036ec33c1aeb40b7d6e59e Mon Sep 17 00:00:00 2001 From: colaftc Date: Wed, 14 Jan 2026 19:13:20 +0800 Subject: [PATCH] feat: added fields into shipment and support external order system --- docs/2026-01-14_summary.md | 24 +++++ docs/shipment_api.md | 11 +++ ...al_finished_product_and_shipment_status.py | 58 ++++++++++++ ...ment_external_finished_product_relation.py | 23 +++++ shipment/models.py | 89 +++++++++++++++++++ 5 files changed, 205 insertions(+) create mode 100644 shipment/migrations/0004_add_external_finished_product_and_shipment_status.py create mode 100644 shipment/migrations/0005_fix_shipment_external_finished_product_relation.py diff --git a/docs/2026-01-14_summary.md b/docs/2026-01-14_summary.md index b40b6d9..8fe20a1 100644 --- a/docs/2026-01-14_summary.md +++ b/docs/2026-01-14_summary.md @@ -132,6 +132,30 @@ python manage.py backfill_merchant --- +### 8. Shipment 模型兼容外部遗留系统字段 + +为出货单与外部系统对接预留字段与模型(暂未接入业务逻辑/API)。 + +#### Shipment 新增字段/方法 +- `status`:状态(待送货 / 已交付 / 已取消),默认「待送货」 +- `cancelled_at`:取消时间(可空) +- `cancelled_by`:取消人(可空,绑定用户) +- `external_id`:外部订单号(可空,长度 120) +- `external_finished_products`:关联多个“外部成品表”(一对多,外键在外部成品表侧) +- `cancel(operator)`:取消出货单时自动记录取消时间与操作者 + +#### 新增模型:ExternalFinishedProduct(外部成品表) +- 字段:`style_name`、`num_of_rolls`、`created_at`(来自 ModelBase)、`created_by` + +#### Migration +- `shipment/migrations/0004_add_external_finished_product_and_shipment_status.py` +- `shipment/migrations/0005_fix_shipment_external_finished_product_relation.py`(修正关系方向:Shipment 1→N ExternalFinishedProduct) + +#### 测试 +- 运行 `api_v1.views.shipment.test_api`:13 个测试通过 + +--- + ## 待办 --- diff --git a/docs/shipment_api.md b/docs/shipment_api.md index fe692dd..f0fdd1b 100644 --- a/docs/shipment_api.md +++ b/docs/shipment_api.md @@ -246,3 +246,14 @@ curl -X GET \ - `shipment/services.py`: 业务逻辑层 - `api_v1/views/shipment/`: API 视图层 - `shipment/models.py`: 数据模型(SalesItem, Shipment) + +--- + +## 外部系统兼容(数据模型说明) + +为兼容外部遗留系统,Shipment 模块新增了“外部成品表”模型: + +- **关系**:`Shipment` 1 → N `ExternalFinishedProduct` + - 外键在 `ExternalFinishedProduct.shipment`(可空) +- **Shipment 外部字段**: + - `external_id`:外部订单号(可空) diff --git a/shipment/migrations/0004_add_external_finished_product_and_shipment_status.py b/shipment/migrations/0004_add_external_finished_product_and_shipment_status.py new file mode 100644 index 0000000..de6c08c --- /dev/null +++ b/shipment/migrations/0004_add_external_finished_product_and_shipment_status.py @@ -0,0 +1,58 @@ +# Generated by Django 5.2.8 on 2026-01-14 10:59 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('shipment', '0003_salesitem_position_remark'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='shipment', + name='cancelled_at', + field=models.DateTimeField(blank=True, null=True, verbose_name='取消时间'), + ), + migrations.AddField( + model_name='shipment', + name='cancelled_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='cancelled_shipments', to=settings.AUTH_USER_MODEL, verbose_name='取消人'), + ), + migrations.AddField( + model_name='shipment', + name='external_id', + field=models.CharField(blank=True, help_text='用于兼容外部遗留系统的订单号(可空)', max_length=120, null=True, verbose_name='外部订单号'), + ), + migrations.AddField( + model_name='shipment', + name='status', + field=models.IntegerField(choices=[(1, '待送货'), (2, '已交付'), (3, '已取消')], default=1, verbose_name='状态'), + ), + migrations.CreateModel( + name='ExternalFinishedProduct', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('style_name', models.CharField(max_length=200, verbose_name='款式名称')), + ('num_of_rolls', models.PositiveIntegerField(verbose_name='卷数')), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_external_finished_products', to=settings.AUTH_USER_MODEL, verbose_name='创建人')), + ], + options={ + 'verbose_name': '外部成品表', + 'verbose_name_plural': '外部成品表', + 'db_table': 'external_finished_product', + 'ordering': ['-created_at'], + }, + ), + migrations.AddField( + model_name='shipment', + name='external_finished_product', + field=models.ForeignKey(blank=True, help_text='用于兼容外部遗留系统(可空)', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='shipments', to='shipment.externalfinishedproduct', verbose_name='外部成品表'), + ), + ] diff --git a/shipment/migrations/0005_fix_shipment_external_finished_product_relation.py b/shipment/migrations/0005_fix_shipment_external_finished_product_relation.py new file mode 100644 index 0000000..b6a18bd --- /dev/null +++ b/shipment/migrations/0005_fix_shipment_external_finished_product_relation.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.8 on 2026-01-14 11:05 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('shipment', '0004_add_external_finished_product_and_shipment_status'), + ] + + operations = [ + migrations.RemoveField( + model_name='shipment', + name='external_finished_product', + ), + migrations.AddField( + model_name='externalfinishedproduct', + name='shipment', + field=models.ForeignKey(blank=True, help_text='关联的出货单(可空,兼容历史数据/逐步迁移)', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='external_finished_products', to='shipment.shipment', verbose_name='出货单'), + ), + ] diff --git a/shipment/models.py b/shipment/models.py index 18a75d4..24f18b8 100644 --- a/shipment/models.py +++ b/shipment/models.py @@ -3,6 +3,7 @@ from typing import TYPE_CHECKING from django.db import models from django.contrib.auth import get_user_model +from django.utils import timezone from flower.common import ModelBase from basic_info import models as basic_models @@ -20,12 +21,88 @@ class UnitChoices(models.IntegerChoices): UNIT = 4, '个' +class ShipmentStatus(models.IntegerChoices): + """出货单状态""" + PENDING_DELIVERY = 1, '待送货' + DELIVERED = 2, '已交付' + CANCELLED = 3, '已取消' + + +class ExternalFinishedProduct(ModelBase): + """ + 外部成品表 + + 用于兼容外部遗留系统的数据映射(可被多个出货单引用)。 + """ + style_name = models.CharField( + max_length=200, + verbose_name='款式名称', + ) + num_of_rolls = models.PositiveIntegerField( + verbose_name='卷数', + ) + created_by = models.ForeignKey( + User, + on_delete=models.SET_NULL, + null=True, + related_name='created_external_finished_products', + verbose_name='创建人', + ) + shipment = models.ForeignKey( + 'Shipment', + on_delete=models.CASCADE, + null=True, + blank=True, + related_name='external_finished_products', + verbose_name='出货单', + help_text='关联的出货单(可空,兼容历史数据/逐步迁移)', + ) + + class Meta: + db_table = 'external_finished_product' + verbose_name = '外部成品表' + verbose_name_plural = '外部成品表' + ordering = ['-created_at'] + + def __str__(self) -> str: + return f'{self.style_name} ({self.num_of_rolls}卷)' + + class Shipment(ModelBase): """ 出货单 记录一次出货操作,包含多个销售品。 """ + status = models.IntegerField( + choices=ShipmentStatus.choices, + default=ShipmentStatus.PENDING_DELIVERY, + verbose_name='状态', + ) + + cancelled_at = models.DateTimeField( + null=True, + blank=True, + verbose_name='取消时间', + ) + + cancelled_by = models.ForeignKey( + User, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name='cancelled_shipments', + verbose_name='取消人', + ) + + external_id = models.CharField( + max_length=120, + null=True, + blank=True, + verbose_name='外部订单号', + help_text='用于兼容外部遗留系统的订单号(可空)', + ) + customer = models.ForeignKey( basic_models.Customer, on_delete=models.PROTECT, @@ -61,6 +138,18 @@ class Shipment(ModelBase): def __str__(self): return f'出货单 #{self.id} - {self.customer.name}' + def cancel(self, operator: User | None) -> None: + """ + 取消出货单(记录取消时间与操作者) + + Args: + operator: 操作者(通常为 request.user,可为空) + """ + self.status = ShipmentStatus.CANCELLED + self.cancelled_at = timezone.now() + self.cancelled_by = operator + self.save(update_fields=['status', 'cancelled_at', 'cancelled_by', 'updated_at']) + class SalesItem(ModelBase): """