forked from erp-dev/erp
feat: new module: 'shipment', new signal when process was finished
This commit is contained in:
1
shipment/__init__.py
Normal file
1
shipment/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Shipment module
|
||||
49
shipment/admin.py
Normal file
49
shipment/admin.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from django.contrib import admin
|
||||
from .models import Shipment, SalesItem
|
||||
|
||||
|
||||
class SalesItemInline(admin.TabularInline):
|
||||
model = SalesItem
|
||||
extra = 1
|
||||
fields = ['name', 'quantity', 'unit', 'printing_job_id', 'customer_id', 'created_by']
|
||||
readonly_fields = ['created_by']
|
||||
|
||||
|
||||
@admin.register(Shipment)
|
||||
class ShipmentAdmin(admin.ModelAdmin):
|
||||
list_display = ['id', 'customer', 'shipment_date', 'items_count', 'created_by', 'created_at']
|
||||
list_filter = ['shipment_date', 'created_at']
|
||||
search_fields = ['customer__name', 'remark']
|
||||
readonly_fields = ['created_at', 'updated_at', 'created_by']
|
||||
date_hierarchy = 'shipment_date'
|
||||
inlines = [SalesItemInline]
|
||||
|
||||
def items_count(self, obj):
|
||||
return obj.items.count()
|
||||
items_count.short_description = '销售品数量'
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
if not change:
|
||||
obj.created_by = request.user
|
||||
super().save_model(request, obj, form, change)
|
||||
|
||||
def save_formset(self, request, form, formset, change):
|
||||
instances = formset.save(commit=False)
|
||||
for instance in instances:
|
||||
if isinstance(instance, SalesItem) and not instance.pk:
|
||||
instance.created_by = request.user
|
||||
instance.save()
|
||||
formset.save_m2m()
|
||||
|
||||
|
||||
@admin.register(SalesItem)
|
||||
class SalesItemAdmin(admin.ModelAdmin):
|
||||
list_display = ['id', 'name', 'quantity', 'unit', 'shipment', 'printing_job_id', 'customer_id', 'created_by', 'created_at']
|
||||
list_filter = ['unit', 'created_at']
|
||||
search_fields = ['name', 'shipment__customer__name']
|
||||
readonly_fields = ['created_at', 'updated_at', 'created_by']
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
if not change:
|
||||
obj.created_by = request.user
|
||||
super().save_model(request, obj, form, change)
|
||||
7
shipment/apps.py
Normal file
7
shipment/apps.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ShipmentConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'shipment'
|
||||
verbose_name = '出货管理'
|
||||
57
shipment/migrations/0001_initial.py
Normal file
57
shipment/migrations/0001_initial.py
Normal file
@@ -0,0 +1,57 @@
|
||||
# Generated by Django 5.2.8 on 2026-01-13 09:45
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('basic_info', '0024_frontend_page_and_visible_pages'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Shipment',
|
||||
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='更新时间')),
|
||||
('shipment_date', models.DateField(help_text='实际出货日期', verbose_name='出货日期')),
|
||||
('remark', models.TextField(blank=True, default='', verbose_name='备注')),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_shipments', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
|
||||
('customer', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='shipments', to='basic_info.customer', verbose_name='客户')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '出货单',
|
||||
'verbose_name_plural': '出货单',
|
||||
'db_table': 'shipment',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SalesItem',
|
||||
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='更新时间')),
|
||||
('name', models.CharField(max_length=200, verbose_name='名称')),
|
||||
('quantity', models.DecimalField(decimal_places=2, max_digits=12, verbose_name='数量')),
|
||||
('unit', models.IntegerField(choices=[(1, '米'), (2, '件'), (3, '码'), (4, '个')], default=2, verbose_name='单位')),
|
||||
('printing_job_id', models.PositiveIntegerField(blank=True, help_text='关联的 PrintingJob ID(可空)', null=True, verbose_name='关联生产任务ID')),
|
||||
('customer_id', models.PositiveIntegerField(blank=True, help_text='销售品级别的客户ID(可空)', null=True, verbose_name='客户ID')),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_sales_items', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
|
||||
('shipment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='shipment.shipment', verbose_name='出货单')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '销售品',
|
||||
'verbose_name_plural': '销售品',
|
||||
'db_table': 'sales_item',
|
||||
'ordering': ['id'],
|
||||
},
|
||||
),
|
||||
]
|
||||
19
shipment/migrations/0002_salesitem_shipment_nullable.py
Normal file
19
shipment/migrations/0002_salesitem_shipment_nullable.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.2.8 on 2026-01-13 10:02
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('shipment', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='salesitem',
|
||||
name='shipment',
|
||||
field=models.ForeignKey(blank=True, help_text='为空表示待分配', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='items', to='shipment.shipment', verbose_name='出货单'),
|
||||
),
|
||||
]
|
||||
0
shipment/migrations/__init__.py
Normal file
0
shipment/migrations/__init__.py
Normal file
154
shipment/models.py
Normal file
154
shipment/models.py
Normal file
@@ -0,0 +1,154 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth import get_user_model
|
||||
from flower.common import ModelBase
|
||||
from basic_info import models as basic_models
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class UnitChoices(models.IntegerChoices):
|
||||
"""销售品单位"""
|
||||
METER = 1, '米'
|
||||
PIECE = 2, '件'
|
||||
YARD = 3, '码'
|
||||
UNIT = 4, '个'
|
||||
|
||||
|
||||
class Shipment(ModelBase):
|
||||
"""
|
||||
出货单
|
||||
|
||||
记录一次出货操作,包含多个销售品。
|
||||
"""
|
||||
customer = models.ForeignKey(
|
||||
basic_models.Customer,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='shipments',
|
||||
verbose_name='客户'
|
||||
)
|
||||
|
||||
shipment_date = models.DateField(
|
||||
verbose_name='出货日期',
|
||||
help_text='实际出货日期'
|
||||
)
|
||||
|
||||
remark = models.TextField(
|
||||
blank=True,
|
||||
default='',
|
||||
verbose_name='备注'
|
||||
)
|
||||
|
||||
created_by = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
related_name='created_shipments',
|
||||
verbose_name='创建人'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = 'shipment'
|
||||
verbose_name = '出货单'
|
||||
verbose_name_plural = '出货单'
|
||||
ordering = ['-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f'出货单 #{self.id} - {self.customer.name}'
|
||||
|
||||
|
||||
class SalesItem(ModelBase):
|
||||
"""
|
||||
销售品
|
||||
|
||||
出货单中的具体商品明细。
|
||||
销售品可以先创建(待分配状态),后续再关联到出货单。
|
||||
"""
|
||||
shipment = models.ForeignKey(
|
||||
Shipment,
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='items',
|
||||
verbose_name='出货单',
|
||||
help_text='为空表示待分配'
|
||||
)
|
||||
|
||||
name = models.CharField(
|
||||
max_length=200,
|
||||
verbose_name='名称'
|
||||
)
|
||||
|
||||
quantity = models.DecimalField(
|
||||
max_digits=12,
|
||||
decimal_places=2,
|
||||
verbose_name='数量'
|
||||
)
|
||||
|
||||
unit = models.IntegerField(
|
||||
choices=UnitChoices.choices,
|
||||
default=UnitChoices.PIECE,
|
||||
verbose_name='单位'
|
||||
)
|
||||
|
||||
created_by = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
related_name='created_sales_items',
|
||||
verbose_name='创建人'
|
||||
)
|
||||
|
||||
# 关联的生产明细订单(可空)
|
||||
# 使用 PositiveIntegerField 而非 ForeignKey,避免模块间强依赖
|
||||
printing_job_id = models.PositiveIntegerField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name='关联生产任务ID',
|
||||
help_text='关联的 PrintingJob ID(可空)'
|
||||
)
|
||||
|
||||
# 客户ID(可空,用于销售品级别的客户关联)
|
||||
customer_id = models.PositiveIntegerField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name='客户ID',
|
||||
help_text='销售品级别的客户ID(可空)'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = 'sales_item'
|
||||
verbose_name = '销售品'
|
||||
verbose_name_plural = '销售品'
|
||||
ordering = ['id']
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.name} x {self.quantity} {self.get_unit_display()}'
|
||||
|
||||
def get_printing_job(self):
|
||||
"""
|
||||
获取关联的 PrintingJob 实例
|
||||
|
||||
Returns:
|
||||
PrintingJob 实例,如果不存在则返回 None
|
||||
"""
|
||||
if not self.printing_job_id:
|
||||
return None
|
||||
try:
|
||||
from printing.models import PrintingJob
|
||||
return PrintingJob.objects.get(id=self.printing_job_id)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_customer(self):
|
||||
"""
|
||||
获取关联的 Customer 实例
|
||||
|
||||
Returns:
|
||||
Customer 实例,如果不存在则返回 None
|
||||
"""
|
||||
if not self.customer_id:
|
||||
return None
|
||||
try:
|
||||
return basic_models.Customer.objects.get(id=self.customer_id)
|
||||
except basic_models.Customer.DoesNotExist:
|
||||
return None
|
||||
Reference in New Issue
Block a user