feat:mpr-物资管理模块(申购单、入库单、物料库存、领用记录)
- 新增 mpr app,包含物资申购单、仓库入库单、物料库存、物资领用单四个业务模型 - 物资申购单/入库单支持 TicketMixin 审批工作流,明细随主表一次性提交 - 入库单审批通过后自动生成物料库存记录 - 物料库存增加状态字段(闲置/领用中/已领用/已领完),数量为0时显示已领完 - 物资领用单提交时自动扣减库存并标记领用中,审批通过改为已领用,拒绝时恢复库存 - 包含 models、serializers、views、filters、urls 完整后端代码 Made-with: Cursor
This commit is contained in:
parent
7e052b7b71
commit
8541905a5c
|
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class MprConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.mpr'
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
from django_filters import rest_framework as filters
|
||||
from apps.mpr.models import PurchaseRequisition, WarehouseEntry, WarehouseStock, MaterialRequisition
|
||||
|
||||
|
||||
class PurchaseRequisitionFilter(filters.FilterSet):
|
||||
req_date_after = filters.DateFilter(field_name='req_date', lookup_expr='gte')
|
||||
req_date_before = filters.DateFilter(field_name='req_date', lookup_expr='lte')
|
||||
|
||||
class Meta:
|
||||
model = PurchaseRequisition
|
||||
fields = ['belong_dept', 'req_date_after', 'req_date_before']
|
||||
|
||||
|
||||
class WarehouseEntryFilter(filters.FilterSet):
|
||||
entry_date_after = filters.DateFilter(field_name='entry_date', lookup_expr='gte')
|
||||
entry_date_before = filters.DateFilter(field_name='entry_date', lookup_expr='lte')
|
||||
|
||||
class Meta:
|
||||
model = WarehouseEntry
|
||||
fields = ['warehouse', 'entry_type', 'entry_method', 'entry_date_after', 'entry_date_before']
|
||||
|
||||
|
||||
class WarehouseStockFilter(filters.FilterSet):
|
||||
entry_date_after = filters.DateFilter(field_name='entry_date', lookup_expr='gte')
|
||||
entry_date_before = filters.DateFilter(field_name='entry_date', lookup_expr='lte')
|
||||
|
||||
class Meta:
|
||||
model = WarehouseStock
|
||||
fields = ['warehouse', 'entry_type', 'entry_method', 'supplier_name',
|
||||
'invoice_received', 'status', 'entry_date_after', 'entry_date_before']
|
||||
|
||||
|
||||
class MaterialRequisitionFilter(filters.FilterSet):
|
||||
req_date_after = filters.DateFilter(field_name='req_date', lookup_expr='gte')
|
||||
req_date_before = filters.DateFilter(field_name='req_date', lookup_expr='lte')
|
||||
|
||||
class Meta:
|
||||
model = MaterialRequisition
|
||||
fields = ['belong_dept', 'req_date_after', 'req_date_before']
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
# Generated by Django 3.2.12 on 2026-03-12 03:26
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('system', '0007_alter_dept_create_by_alter_dept_third_info_and_more'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('wf', '0006_auto_20251215_1645'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PurchaseRequisition',
|
||||
fields=[
|
||||
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
|
||||
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||
('number', models.CharField(max_length=20, unique=True, verbose_name='编号')),
|
||||
('phone', models.CharField(blank=True, max_length=20, null=True, verbose_name='联系电话')),
|
||||
('req_date', models.DateField(blank=True, null=True, verbose_name='申购日期')),
|
||||
('total_amount', models.DecimalField(decimal_places=2, default=0, max_digits=14, verbose_name='合计金额')),
|
||||
('note', models.TextField(blank=True, null=True, verbose_name='备注')),
|
||||
('belong_dept', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='purchaserequisition_belong_dept', to='system.dept', verbose_name='所属部门')),
|
||||
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='purchaserequisition_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
|
||||
('ticket', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='mpr_ticket', to='wf.ticket', verbose_name='关联工单')),
|
||||
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='purchaserequisition_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PurchaseRequisitionItem',
|
||||
fields=[
|
||||
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
|
||||
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||
('item_name', models.CharField(max_length=100, verbose_name='物品名称')),
|
||||
('spec', models.CharField(blank=True, max_length=200, null=True, verbose_name='规格及型号')),
|
||||
('unit', models.CharField(blank=True, max_length=20, null=True, verbose_name='单位')),
|
||||
('req_quantity', models.DecimalField(decimal_places=3, default=0, max_digits=12, verbose_name='申购数量')),
|
||||
('current_stock', models.DecimalField(decimal_places=3, default=0, max_digits=12, verbose_name='现库存量')),
|
||||
('need_date', models.DateField(blank=True, null=True, verbose_name='需用日期')),
|
||||
('purchase_quantity', models.DecimalField(decimal_places=3, default=0, max_digits=12, verbose_name='需采购数量')),
|
||||
('unit_price', models.DecimalField(decimal_places=2, default=0, max_digits=14, verbose_name='单价')),
|
||||
('total_price', models.DecimalField(decimal_places=2, default=0, max_digits=14, verbose_name='总价')),
|
||||
('note', models.TextField(blank=True, null=True, verbose_name='备注')),
|
||||
('requisition', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='mpr.purchaserequisition', verbose_name='关联申购单')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
# Generated by Django 3.2.12 on 2026-03-12 06:33
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('system', '0007_alter_dept_create_by_alter_dept_third_info_and_more'),
|
||||
('wf', '0006_auto_20251215_1645'),
|
||||
('inm', '0038_mioitem_count_send'),
|
||||
('mpr', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='WarehouseEntry',
|
||||
fields=[
|
||||
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
|
||||
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||
('number', models.CharField(max_length=20, unique=True, verbose_name='编号')),
|
||||
('entry_date', models.DateField(blank=True, null=True, verbose_name='入库日期')),
|
||||
('entry_type', models.CharField(choices=[('raw_normal', '原材料正常入库'), ('raw_estimated', '原材料暂估入库'), ('product', '产品入库'), ('other', '其他')], default='raw_normal', max_length=20, verbose_name='入库类型')),
|
||||
('entry_method', models.CharField(choices=[('purchase', '采购'), ('self_made', '自制'), ('other', '其他')], default='purchase', max_length=20, verbose_name='入库方式')),
|
||||
('total_amount', models.DecimalField(decimal_places=2, default=0, max_digits=14, verbose_name='合计金额')),
|
||||
('note', models.TextField(blank=True, null=True, verbose_name='备注')),
|
||||
('belong_dept', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='warehouseentry_belong_dept', to='system.dept', verbose_name='所属部门')),
|
||||
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='warehouseentry_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
|
||||
('ticket', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='warehouse_entry_ticket', to='wf.ticket', verbose_name='关联工单')),
|
||||
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='warehouseentry_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
|
||||
('warehouse', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='entries', to='inm.warehouse', verbose_name='仓库')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='WarehouseEntryItem',
|
||||
fields=[
|
||||
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
|
||||
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||
('name', models.CharField(max_length=100, verbose_name='名称')),
|
||||
('spec', models.CharField(blank=True, max_length=200, null=True, verbose_name='规格')),
|
||||
('unit', models.CharField(blank=True, max_length=20, null=True, verbose_name='单位')),
|
||||
('quantity', models.DecimalField(decimal_places=3, default=0, max_digits=12, verbose_name='数量')),
|
||||
('unit_price', models.DecimalField(decimal_places=2, default=0, max_digits=14, verbose_name='单价')),
|
||||
('amount', models.DecimalField(decimal_places=2, default=0, max_digits=14, verbose_name='金额')),
|
||||
('supplier_name', models.CharField(blank=True, max_length=100, null=True, verbose_name='供应商名称')),
|
||||
('invoice_received', models.BooleanField(default=False, verbose_name='账单是否收到')),
|
||||
('note', models.TextField(blank=True, null=True, verbose_name='备注')),
|
||||
('entry', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='mpr.warehouseentry', verbose_name='关联入库单')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
# Generated by Django 3.2.12 on 2026-03-12 07:26
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inm', '0038_mioitem_count_send'),
|
||||
('mpr', '0002_warehouseentry_warehouseentryitem'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='WarehouseStock',
|
||||
fields=[
|
||||
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
|
||||
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||
('entry_number', models.CharField(max_length=20, verbose_name='入库单号')),
|
||||
('entry_date', models.DateField(blank=True, null=True, verbose_name='入库日期')),
|
||||
('entry_type', models.CharField(choices=[('raw_normal', '原材料正常入库'), ('raw_estimated', '原材料暂估入库'), ('product', '产品入库'), ('other', '其他')], max_length=20, verbose_name='入库类型')),
|
||||
('entry_method', models.CharField(choices=[('purchase', '采购'), ('self_made', '自制'), ('other', '其他')], max_length=20, verbose_name='入库方式')),
|
||||
('name', models.CharField(max_length=100, verbose_name='名称')),
|
||||
('spec', models.CharField(blank=True, max_length=200, null=True, verbose_name='规格')),
|
||||
('unit', models.CharField(blank=True, max_length=20, null=True, verbose_name='单位')),
|
||||
('quantity', models.DecimalField(decimal_places=3, default=0, max_digits=12, verbose_name='数量')),
|
||||
('unit_price', models.DecimalField(decimal_places=2, default=0, max_digits=14, verbose_name='单价')),
|
||||
('amount', models.DecimalField(decimal_places=2, default=0, max_digits=14, verbose_name='金额')),
|
||||
('supplier_name', models.CharField(blank=True, max_length=100, null=True, verbose_name='供应商名称')),
|
||||
('invoice_received', models.BooleanField(default=False, verbose_name='账单是否收到')),
|
||||
('entry', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='stocks', to='mpr.warehouseentry', verbose_name='来源入库单')),
|
||||
('warehouse', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='mpr_stocks', to='inm.warehouse', verbose_name='仓库')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
# Generated by Django 3.2.12 on 2026-03-12 08:06
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('system', '0007_alter_dept_create_by_alter_dept_third_info_and_more'),
|
||||
('wf', '0006_auto_20251215_1645'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('mpr', '0003_warehousestock'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='MaterialRequisition',
|
||||
fields=[
|
||||
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
|
||||
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||
('number', models.CharField(max_length=20, unique=True, verbose_name='编号')),
|
||||
('req_date', models.DateField(blank=True, null=True, verbose_name='填报时间')),
|
||||
('collector', models.CharField(blank=True, max_length=50, null=True, verbose_name='领取人')),
|
||||
('note', models.TextField(blank=True, null=True, verbose_name='备注')),
|
||||
('belong_dept', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='materialrequisition_belong_dept', to='system.dept', verbose_name='所属部门')),
|
||||
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='materialrequisition_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
|
||||
('ticket', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='material_requisition_ticket', to='wf.ticket', verbose_name='关联工单')),
|
||||
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='materialrequisition_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='MaterialRequisitionItem',
|
||||
fields=[
|
||||
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
|
||||
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||
('is_stock_item', models.BooleanField(default=True, verbose_name='是否库存物品')),
|
||||
('req_type', models.CharField(blank=True, max_length=50, null=True, verbose_name='领用类型')),
|
||||
('name', models.CharField(max_length=100, verbose_name='物资名称')),
|
||||
('spec', models.CharField(blank=True, max_length=200, null=True, verbose_name='规格型号')),
|
||||
('unit', models.CharField(blank=True, max_length=20, null=True, verbose_name='单位')),
|
||||
('quantity', models.DecimalField(decimal_places=3, default=0, max_digits=12, verbose_name='领用量')),
|
||||
('note', models.TextField(blank=True, null=True, verbose_name='备注')),
|
||||
('requisition', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='mpr.materialrequisition', verbose_name='关联领用单')),
|
||||
('stock', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='requisition_items', to='mpr.warehousestock', verbose_name='关联库存')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 3.2.12 on 2026-03-12 08:41
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mpr', '0004_materialrequisition_materialrequisitionitem'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='warehousestock',
|
||||
name='status',
|
||||
field=models.CharField(choices=[('idle', '闲置'), ('in_requisition', '领用中'), ('requisitioned', '已领用')], default='idle', max_length=20, verbose_name='状态'),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
from django.db import models
|
||||
from apps.utils.models import BaseModel, CommonBDModel
|
||||
from datetime import datetime
|
||||
from django.db.models import Max, Sum
|
||||
|
||||
|
||||
def _get_number(model_cls):
|
||||
today_str = datetime.now().strftime('%Y%m%d')
|
||||
prefix = model_cls.PREFIX
|
||||
last_record = model_cls.objects.filter(
|
||||
number__startswith=f"{prefix}-{today_str}"
|
||||
).aggregate(Max('number'))['number__max']
|
||||
if last_record:
|
||||
last_number = int(last_record.split('-')[-1]) + 1
|
||||
else:
|
||||
last_number = 1
|
||||
return f"{prefix}-{today_str}-{last_number:04d}"
|
||||
|
||||
|
||||
class PurchaseRequisition(CommonBDModel):
|
||||
"""
|
||||
TN:物资申购单
|
||||
"""
|
||||
PREFIX = 'WZSG'
|
||||
|
||||
number = models.CharField('编号', max_length=20, unique=True)
|
||||
phone = models.CharField('联系电话', max_length=20, null=True, blank=True)
|
||||
req_date = models.DateField('申购日期', null=True, blank=True)
|
||||
total_amount = models.DecimalField('合计金额', max_digits=14, decimal_places=2, default=0)
|
||||
note = models.TextField('备注', null=True, blank=True)
|
||||
ticket = models.OneToOneField(
|
||||
'wf.ticket', verbose_name='关联工单',
|
||||
on_delete=models.SET_NULL, null=True, blank=True,
|
||||
related_name='mpr_ticket')
|
||||
|
||||
@classmethod
|
||||
def get_a_number(cls):
|
||||
return _get_number(cls)
|
||||
|
||||
|
||||
class PurchaseRequisitionItem(BaseModel):
|
||||
"""
|
||||
TN:物资申购明细
|
||||
"""
|
||||
requisition = models.ForeignKey(
|
||||
PurchaseRequisition, verbose_name='关联申购单',
|
||||
on_delete=models.CASCADE, related_name='items')
|
||||
item_name = models.CharField('物品名称', max_length=100)
|
||||
spec = models.CharField('规格及型号', max_length=200, null=True, blank=True)
|
||||
unit = models.CharField('单位', max_length=20, null=True, blank=True)
|
||||
req_quantity = models.DecimalField('申购数量', max_digits=12, decimal_places=3, default=0)
|
||||
current_stock = models.DecimalField('现库存量', max_digits=12, decimal_places=3, default=0)
|
||||
need_date = models.DateField('需用日期', null=True, blank=True)
|
||||
purchase_quantity = models.DecimalField('需采购数量', max_digits=12, decimal_places=3, default=0)
|
||||
unit_price = models.DecimalField('单价', max_digits=14, decimal_places=2, default=0)
|
||||
total_price = models.DecimalField('总价', max_digits=14, decimal_places=2, default=0)
|
||||
note = models.TextField('备注', null=True, blank=True)
|
||||
|
||||
|
||||
class WarehouseEntry(CommonBDModel):
|
||||
"""
|
||||
TN:仓库入库单
|
||||
"""
|
||||
PREFIX = 'CKRK'
|
||||
|
||||
ENTRY_TYPE_CHOICES = (
|
||||
('raw_normal', '原材料正常入库'),
|
||||
('raw_estimated', '原材料暂估入库'),
|
||||
('product', '产品入库'),
|
||||
('other', '其他'),
|
||||
)
|
||||
ENTRY_METHOD_CHOICES = (
|
||||
('purchase', '采购'),
|
||||
('self_made', '自制'),
|
||||
('other', '其他'),
|
||||
)
|
||||
|
||||
number = models.CharField('编号', max_length=20, unique=True)
|
||||
warehouse = models.ForeignKey(
|
||||
'inm.WareHouse', verbose_name='仓库',
|
||||
on_delete=models.CASCADE, related_name='entries')
|
||||
entry_date = models.DateField('入库日期', null=True, blank=True)
|
||||
entry_type = models.CharField('入库类型', max_length=20, choices=ENTRY_TYPE_CHOICES, default='raw_normal')
|
||||
entry_method = models.CharField('入库方式', max_length=20, choices=ENTRY_METHOD_CHOICES, default='purchase')
|
||||
total_amount = models.DecimalField('合计金额', max_digits=14, decimal_places=2, default=0)
|
||||
note = models.TextField('备注', null=True, blank=True)
|
||||
ticket = models.OneToOneField(
|
||||
'wf.ticket', verbose_name='关联工单',
|
||||
on_delete=models.SET_NULL, null=True, blank=True,
|
||||
related_name='warehouse_entry_ticket')
|
||||
|
||||
@classmethod
|
||||
def get_a_number(cls):
|
||||
return _get_number(cls)
|
||||
|
||||
|
||||
class WarehouseEntryItem(BaseModel):
|
||||
"""
|
||||
TN:入库明细
|
||||
"""
|
||||
entry = models.ForeignKey(
|
||||
WarehouseEntry, verbose_name='关联入库单',
|
||||
on_delete=models.CASCADE, related_name='items')
|
||||
name = models.CharField('名称', max_length=100)
|
||||
spec = models.CharField('规格', max_length=200, null=True, blank=True)
|
||||
unit = models.CharField('单位', max_length=20, null=True, blank=True)
|
||||
quantity = models.DecimalField('数量', max_digits=12, decimal_places=3, default=0)
|
||||
unit_price = models.DecimalField('单价', max_digits=14, decimal_places=2, default=0)
|
||||
amount = models.DecimalField('金额', max_digits=14, decimal_places=2, default=0)
|
||||
supplier_name = models.CharField('供应商名称', max_length=100, null=True, blank=True)
|
||||
invoice_received = models.BooleanField('账单是否收到', default=False)
|
||||
note = models.TextField('备注', null=True, blank=True)
|
||||
|
||||
|
||||
class WarehouseStock(BaseModel):
|
||||
"""
|
||||
TN:物料库存(审批通过后入库)
|
||||
"""
|
||||
STATUS_CHOICES = (
|
||||
('idle', '闲置'),
|
||||
('in_requisition', '领用中'),
|
||||
('requisitioned', '已领用'),
|
||||
)
|
||||
|
||||
warehouse = models.ForeignKey(
|
||||
'inm.WareHouse', verbose_name='仓库',
|
||||
on_delete=models.CASCADE, related_name='mpr_stocks')
|
||||
entry = models.ForeignKey(
|
||||
WarehouseEntry, verbose_name='来源入库单',
|
||||
on_delete=models.SET_NULL, null=True, blank=True, related_name='stocks')
|
||||
entry_number = models.CharField('入库单号', max_length=20)
|
||||
entry_date = models.DateField('入库日期', null=True, blank=True)
|
||||
entry_type = models.CharField('入库类型', max_length=20, choices=WarehouseEntry.ENTRY_TYPE_CHOICES)
|
||||
entry_method = models.CharField('入库方式', max_length=20, choices=WarehouseEntry.ENTRY_METHOD_CHOICES)
|
||||
name = models.CharField('名称', max_length=100)
|
||||
spec = models.CharField('规格', max_length=200, null=True, blank=True)
|
||||
unit = models.CharField('单位', max_length=20, null=True, blank=True)
|
||||
quantity = models.DecimalField('数量', max_digits=12, decimal_places=3, default=0)
|
||||
unit_price = models.DecimalField('单价', max_digits=14, decimal_places=2, default=0)
|
||||
amount = models.DecimalField('金额', max_digits=14, decimal_places=2, default=0)
|
||||
supplier_name = models.CharField('供应商名称', max_length=100, null=True, blank=True)
|
||||
invoice_received = models.BooleanField('账单是否收到', default=False)
|
||||
status = models.CharField('状态', max_length=20, choices=STATUS_CHOICES, default='idle')
|
||||
|
||||
|
||||
class MaterialRequisition(CommonBDModel):
|
||||
"""
|
||||
TN:物资领用单
|
||||
"""
|
||||
PREFIX = 'WZLY'
|
||||
|
||||
number = models.CharField('编号', max_length=20, unique=True)
|
||||
req_date = models.DateField('填报时间', null=True, blank=True)
|
||||
collector = models.CharField('领取人', max_length=50, null=True, blank=True)
|
||||
note = models.TextField('备注', null=True, blank=True)
|
||||
ticket = models.OneToOneField(
|
||||
'wf.ticket', verbose_name='关联工单',
|
||||
on_delete=models.SET_NULL, null=True, blank=True,
|
||||
related_name='material_requisition_ticket')
|
||||
|
||||
@classmethod
|
||||
def get_a_number(cls):
|
||||
return _get_number(cls)
|
||||
|
||||
|
||||
class MaterialRequisitionItem(BaseModel):
|
||||
"""
|
||||
TN:物资领用明细
|
||||
"""
|
||||
requisition = models.ForeignKey(
|
||||
MaterialRequisition, verbose_name='关联领用单',
|
||||
on_delete=models.CASCADE, related_name='items')
|
||||
is_stock_item = models.BooleanField('是否库存物品', default=True)
|
||||
stock = models.ForeignKey(
|
||||
WarehouseStock, verbose_name='关联库存',
|
||||
on_delete=models.SET_NULL, null=True, blank=True, related_name='requisition_items')
|
||||
req_type = models.CharField('领用类型', max_length=50, null=True, blank=True)
|
||||
name = models.CharField('物资名称', max_length=100)
|
||||
spec = models.CharField('规格型号', max_length=200, null=True, blank=True)
|
||||
unit = models.CharField('单位', max_length=20, null=True, blank=True)
|
||||
quantity = models.DecimalField('领用量', max_digits=12, decimal_places=3, default=0)
|
||||
note = models.TextField('备注', null=True, blank=True)
|
||||
|
|
@ -0,0 +1,271 @@
|
|||
from decimal import Decimal
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ParseError
|
||||
from apps.utils.serializers import CustomModelSerializer
|
||||
from apps.utils.constants import EXCLUDE_FIELDS
|
||||
from apps.mpr.models import (
|
||||
PurchaseRequisition, PurchaseRequisitionItem,
|
||||
WarehouseEntry, WarehouseEntryItem, WarehouseStock,
|
||||
MaterialRequisition, MaterialRequisitionItem,
|
||||
)
|
||||
from apps.wf.serializers import TicketSimpleSerializer
|
||||
|
||||
|
||||
# ========== 物资申购单 ==========
|
||||
|
||||
class PurchaseRequisitionItemSerializer(CustomModelSerializer):
|
||||
class Meta:
|
||||
model = PurchaseRequisitionItem
|
||||
fields = '__all__'
|
||||
read_only_fields = ['create_time', 'update_time', 'is_deleted']
|
||||
|
||||
|
||||
class PurchaseRequisitionListSerializer(CustomModelSerializer):
|
||||
create_by_name = serializers.CharField(source='create_by.name', read_only=True)
|
||||
belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True)
|
||||
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = PurchaseRequisition
|
||||
fields = '__all__'
|
||||
read_only_fields = EXCLUDE_FIELDS
|
||||
|
||||
|
||||
class PurchaseRequisitionDetailSerializer(CustomModelSerializer):
|
||||
create_by_name = serializers.CharField(source='create_by.name', read_only=True)
|
||||
belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True)
|
||||
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
|
||||
items_ = PurchaseRequisitionItemSerializer(source='items', many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = PurchaseRequisition
|
||||
fields = '__all__'
|
||||
read_only_fields = EXCLUDE_FIELDS
|
||||
|
||||
|
||||
class PurchaseRequisitionCreateSerializer(CustomModelSerializer):
|
||||
items = serializers.ListField(child=serializers.DictField(), write_only=True, required=False, default=[])
|
||||
|
||||
class Meta:
|
||||
model = PurchaseRequisition
|
||||
fields = '__all__'
|
||||
read_only_fields = EXCLUDE_FIELDS + ['belong_dept', 'number', 'total_amount']
|
||||
|
||||
def create(self, validated_data):
|
||||
items_data = validated_data.pop('items', [])
|
||||
validated_data['number'] = PurchaseRequisition.get_a_number()
|
||||
instance = super().create(validated_data)
|
||||
self._save_items(instance, items_data)
|
||||
return instance
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
items_data = validated_data.pop('items', None)
|
||||
instance = super().update(instance, validated_data)
|
||||
if items_data is not None:
|
||||
instance.items.all().delete()
|
||||
self._save_items(instance, items_data)
|
||||
return instance
|
||||
|
||||
def _save_items(self, instance, items_data):
|
||||
total = 0
|
||||
for item in items_data:
|
||||
item.pop('id', None)
|
||||
unit_price = float(item.get('unit_price', 0) or 0)
|
||||
purchase_quantity = float(item.get('purchase_quantity', 0) or 0)
|
||||
item['total_price'] = round(unit_price * purchase_quantity, 2)
|
||||
total += item['total_price']
|
||||
PurchaseRequisitionItem.objects.create(requisition=instance, **item)
|
||||
instance.total_amount = total
|
||||
instance.save(update_fields=['total_amount'])
|
||||
|
||||
|
||||
# ========== 仓库入库单 ==========
|
||||
|
||||
class WarehouseEntryItemSerializer(CustomModelSerializer):
|
||||
class Meta:
|
||||
model = WarehouseEntryItem
|
||||
fields = '__all__'
|
||||
read_only_fields = ['create_time', 'update_time', 'is_deleted']
|
||||
|
||||
|
||||
class WarehouseEntryListSerializer(CustomModelSerializer):
|
||||
create_by_name = serializers.CharField(source='create_by.name', read_only=True)
|
||||
belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True)
|
||||
warehouse_name = serializers.CharField(source='warehouse.name', read_only=True)
|
||||
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
|
||||
entry_type_display = serializers.CharField(source='get_entry_type_display', read_only=True)
|
||||
entry_method_display = serializers.CharField(source='get_entry_method_display', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = WarehouseEntry
|
||||
fields = '__all__'
|
||||
read_only_fields = EXCLUDE_FIELDS
|
||||
|
||||
|
||||
class WarehouseEntryDetailSerializer(CustomModelSerializer):
|
||||
create_by_name = serializers.CharField(source='create_by.name', read_only=True)
|
||||
belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True)
|
||||
warehouse_name = serializers.CharField(source='warehouse.name', read_only=True)
|
||||
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
|
||||
items_ = WarehouseEntryItemSerializer(source='items', many=True, read_only=True)
|
||||
entry_type_display = serializers.CharField(source='get_entry_type_display', read_only=True)
|
||||
entry_method_display = serializers.CharField(source='get_entry_method_display', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = WarehouseEntry
|
||||
fields = '__all__'
|
||||
read_only_fields = EXCLUDE_FIELDS
|
||||
|
||||
|
||||
class WarehouseEntryCreateSerializer(CustomModelSerializer):
|
||||
items = serializers.ListField(child=serializers.DictField(), write_only=True, required=False, default=[])
|
||||
|
||||
class Meta:
|
||||
model = WarehouseEntry
|
||||
fields = '__all__'
|
||||
read_only_fields = EXCLUDE_FIELDS + ['belong_dept', 'number', 'total_amount']
|
||||
|
||||
def create(self, validated_data):
|
||||
items_data = validated_data.pop('items', [])
|
||||
validated_data['number'] = WarehouseEntry.get_a_number()
|
||||
instance = super().create(validated_data)
|
||||
self._save_items(instance, items_data)
|
||||
return instance
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
items_data = validated_data.pop('items', None)
|
||||
instance = super().update(instance, validated_data)
|
||||
if items_data is not None:
|
||||
instance.items.all().delete()
|
||||
self._save_items(instance, items_data)
|
||||
return instance
|
||||
|
||||
def _save_items(self, instance, items_data):
|
||||
total = 0
|
||||
for item in items_data:
|
||||
item.pop('id', None)
|
||||
unit_price = float(item.get('unit_price', 0) or 0)
|
||||
quantity = float(item.get('quantity', 0) or 0)
|
||||
item['amount'] = round(unit_price * quantity, 2)
|
||||
total += item['amount']
|
||||
WarehouseEntryItem.objects.create(entry=instance, **item)
|
||||
instance.total_amount = total
|
||||
instance.save(update_fields=['total_amount'])
|
||||
|
||||
|
||||
# ========== 物料库存 ==========
|
||||
|
||||
class WarehouseStockSerializer(CustomModelSerializer):
|
||||
warehouse_name = serializers.CharField(source='warehouse.name', read_only=True)
|
||||
entry_type_display = serializers.CharField(source='get_entry_type_display', read_only=True)
|
||||
entry_method_display = serializers.CharField(source='get_entry_method_display', read_only=True)
|
||||
status_display = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = WarehouseStock
|
||||
fields = '__all__'
|
||||
read_only_fields = ['create_time', 'update_time', 'is_deleted']
|
||||
|
||||
def get_status_display(self, obj):
|
||||
if obj.quantity <= 0:
|
||||
return '已领完'
|
||||
return obj.get_status_display()
|
||||
|
||||
|
||||
# ========== 物资领用单 ==========
|
||||
|
||||
class MaterialRequisitionItemSerializer(CustomModelSerializer):
|
||||
stock_name = serializers.CharField(source='stock.name', read_only=True, default='')
|
||||
|
||||
class Meta:
|
||||
model = MaterialRequisitionItem
|
||||
fields = '__all__'
|
||||
read_only_fields = ['create_time', 'update_time', 'is_deleted']
|
||||
|
||||
|
||||
class MaterialRequisitionListSerializer(CustomModelSerializer):
|
||||
create_by_name = serializers.CharField(source='create_by.name', read_only=True)
|
||||
belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True)
|
||||
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = MaterialRequisition
|
||||
fields = '__all__'
|
||||
read_only_fields = EXCLUDE_FIELDS
|
||||
|
||||
|
||||
class MaterialRequisitionDetailSerializer(CustomModelSerializer):
|
||||
create_by_name = serializers.CharField(source='create_by.name', read_only=True)
|
||||
belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True)
|
||||
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
|
||||
items_ = MaterialRequisitionItemSerializer(source='items', many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = MaterialRequisition
|
||||
fields = '__all__'
|
||||
read_only_fields = EXCLUDE_FIELDS
|
||||
|
||||
|
||||
class MaterialRequisitionCreateSerializer(CustomModelSerializer):
|
||||
items = serializers.ListField(child=serializers.DictField(), write_only=True, required=False, default=[])
|
||||
|
||||
class Meta:
|
||||
model = MaterialRequisition
|
||||
fields = '__all__'
|
||||
read_only_fields = EXCLUDE_FIELDS + ['belong_dept', 'number']
|
||||
|
||||
def create(self, validated_data):
|
||||
items_data = validated_data.pop('items', [])
|
||||
validated_data['number'] = MaterialRequisition.get_a_number()
|
||||
instance = super().create(validated_data)
|
||||
self._save_items(instance, items_data)
|
||||
return instance
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
items_data = validated_data.pop('items', None)
|
||||
instance = super().update(instance, validated_data)
|
||||
if items_data is not None:
|
||||
self._restore_stock(instance)
|
||||
instance.items.all().delete()
|
||||
self._save_items(instance, items_data)
|
||||
return instance
|
||||
|
||||
def _save_items(self, instance, items_data):
|
||||
for item in items_data:
|
||||
item.pop('id', None)
|
||||
is_stock_item = item.get('is_stock_item', True)
|
||||
stock_id = item.pop('stock', None) or item.pop('stock_id', None)
|
||||
quantity = Decimal(str(item.get('quantity', 0) or 0))
|
||||
|
||||
stock_obj = None
|
||||
if is_stock_item and stock_id:
|
||||
try:
|
||||
stock_obj = WarehouseStock.objects.select_for_update().get(id=stock_id)
|
||||
except WarehouseStock.DoesNotExist:
|
||||
raise ParseError(f"库存记录不存在: {stock_id}")
|
||||
if stock_obj.quantity < quantity:
|
||||
raise ParseError(f"库存不足: {stock_obj.name} 库存{stock_obj.quantity} < 领用{quantity}")
|
||||
stock_obj.quantity -= quantity
|
||||
stock_obj.status = 'in_requisition'
|
||||
stock_obj.save(update_fields=['quantity', 'status'])
|
||||
|
||||
MaterialRequisitionItem.objects.create(
|
||||
requisition=instance,
|
||||
is_stock_item=is_stock_item,
|
||||
stock=stock_obj,
|
||||
req_type=item.get('req_type', ''),
|
||||
name=item.get('name', ''),
|
||||
spec=item.get('spec', ''),
|
||||
unit=item.get('unit', ''),
|
||||
quantity=quantity,
|
||||
note=item.get('note', ''),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _restore_stock(instance):
|
||||
"""恢复库存(用于编辑或拒绝时)"""
|
||||
for item in instance.items.filter(is_stock_item=True, stock__isnull=False):
|
||||
stock = WarehouseStock.objects.select_for_update().get(id=item.stock_id)
|
||||
stock.quantity += item.quantity
|
||||
stock.status = 'idle'
|
||||
stock.save(update_fields=['quantity', 'status'])
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from apps.mpr.views import (
|
||||
PurchaseRequisitionViewSet, PurchaseRequisitionItemViewSet,
|
||||
WarehouseEntryViewSet, WarehouseEntryItemViewSet,
|
||||
WarehouseStockViewSet,
|
||||
MaterialRequisitionViewSet, MaterialRequisitionItemViewSet,
|
||||
)
|
||||
|
||||
API_BASE_URL = 'api/mpr/'
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register('requisition', PurchaseRequisitionViewSet, basename='requisition')
|
||||
router.register('requisition_item', PurchaseRequisitionItemViewSet, basename='requisition_item')
|
||||
router.register('warehouse_entry', WarehouseEntryViewSet, basename='warehouse_entry')
|
||||
router.register('warehouse_entry_item', WarehouseEntryItemViewSet, basename='warehouse_entry_item')
|
||||
router.register('warehouse_stock', WarehouseStockViewSet, basename='warehouse_stock')
|
||||
router.register('material_requisition', MaterialRequisitionViewSet, basename='material_requisition')
|
||||
router.register('material_requisition_item', MaterialRequisitionItemViewSet, basename='material_requisition_item')
|
||||
|
||||
urlpatterns = [
|
||||
path(API_BASE_URL, include(router.urls)),
|
||||
]
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
from rest_framework.exceptions import ParseError
|
||||
from django.db import transaction
|
||||
|
||||
from apps.utils.viewsets import CustomModelViewSet
|
||||
from apps.wf.mixins import TicketMixin
|
||||
from apps.wf.models import Ticket
|
||||
from apps.mpr.models import (
|
||||
PurchaseRequisition, PurchaseRequisitionItem,
|
||||
WarehouseEntry, WarehouseEntryItem, WarehouseStock,
|
||||
MaterialRequisition, MaterialRequisitionItem,
|
||||
)
|
||||
from apps.mpr.serializers import (
|
||||
PurchaseRequisitionListSerializer,
|
||||
PurchaseRequisitionDetailSerializer,
|
||||
PurchaseRequisitionCreateSerializer,
|
||||
PurchaseRequisitionItemSerializer,
|
||||
WarehouseEntryListSerializer,
|
||||
WarehouseEntryDetailSerializer,
|
||||
WarehouseEntryCreateSerializer,
|
||||
WarehouseEntryItemSerializer,
|
||||
WarehouseStockSerializer,
|
||||
MaterialRequisitionListSerializer,
|
||||
MaterialRequisitionDetailSerializer,
|
||||
MaterialRequisitionCreateSerializer,
|
||||
MaterialRequisitionItemSerializer,
|
||||
)
|
||||
from apps.mpr.filters import (
|
||||
PurchaseRequisitionFilter, WarehouseEntryFilter,
|
||||
WarehouseStockFilter, MaterialRequisitionFilter,
|
||||
)
|
||||
|
||||
|
||||
class PurchaseRequisitionViewSet(TicketMixin, CustomModelViewSet):
|
||||
"""
|
||||
物资申购单
|
||||
"""
|
||||
queryset = PurchaseRequisition.objects.all()
|
||||
serializer_class = PurchaseRequisitionListSerializer
|
||||
retrieve_serializer_class = PurchaseRequisitionDetailSerializer
|
||||
create_serializer_class = PurchaseRequisitionCreateSerializer
|
||||
update_serializer_class = PurchaseRequisitionCreateSerializer
|
||||
select_related_fields = ['create_by', 'belong_dept', 'ticket', 'ticket__state']
|
||||
search_fields = ['number', 'create_by__name']
|
||||
filterset_class = PurchaseRequisitionFilter
|
||||
ordering = '-create_time'
|
||||
workflow_key = 'wf_mpr'
|
||||
|
||||
def gen_other_ticket_data(self, instance):
|
||||
dept_name = instance.belong_dept.name if instance.belong_dept else ''
|
||||
return {"dept_name": dept_name}
|
||||
|
||||
|
||||
class PurchaseRequisitionItemViewSet(CustomModelViewSet):
|
||||
"""
|
||||
物资申购明细
|
||||
"""
|
||||
queryset = PurchaseRequisitionItem.objects.all()
|
||||
serializer_class = PurchaseRequisitionItemSerializer
|
||||
filterset_fields = ['requisition']
|
||||
ordering = 'create_time'
|
||||
|
||||
|
||||
class WarehouseEntryViewSet(TicketMixin, CustomModelViewSet):
|
||||
"""
|
||||
仓库入库单
|
||||
"""
|
||||
queryset = WarehouseEntry.objects.all()
|
||||
serializer_class = WarehouseEntryListSerializer
|
||||
retrieve_serializer_class = WarehouseEntryDetailSerializer
|
||||
create_serializer_class = WarehouseEntryCreateSerializer
|
||||
update_serializer_class = WarehouseEntryCreateSerializer
|
||||
select_related_fields = ['create_by', 'belong_dept', 'warehouse', 'ticket', 'ticket__state']
|
||||
search_fields = ['number', 'create_by__name', 'warehouse__name']
|
||||
filterset_class = WarehouseEntryFilter
|
||||
ordering = '-create_time'
|
||||
workflow_key = 'wf_warehouse_entry'
|
||||
|
||||
def gen_other_ticket_data(self, instance):
|
||||
return {"warehouse_name": instance.warehouse.name if instance.warehouse else ''}
|
||||
|
||||
@staticmethod
|
||||
def approve_entry(ticket: Ticket, transition, new_ticket_data: dict):
|
||||
"""审批通过后,将入库明细写入物料库存"""
|
||||
entry: WarehouseEntry = WarehouseEntry.objects.get(ticket=ticket)
|
||||
if WarehouseStock.objects.filter(entry=entry).exists():
|
||||
raise ParseError('该入库单已入库,不可重复操作')
|
||||
for item in entry.items.all():
|
||||
WarehouseStock.objects.create(
|
||||
warehouse=entry.warehouse,
|
||||
entry=entry,
|
||||
entry_number=entry.number,
|
||||
entry_date=entry.entry_date,
|
||||
entry_type=entry.entry_type,
|
||||
entry_method=entry.entry_method,
|
||||
name=item.name,
|
||||
spec=item.spec,
|
||||
unit=item.unit,
|
||||
quantity=item.quantity,
|
||||
unit_price=item.unit_price,
|
||||
amount=item.amount,
|
||||
supplier_name=item.supplier_name,
|
||||
invoice_received=item.invoice_received,
|
||||
)
|
||||
|
||||
|
||||
class WarehouseEntryItemViewSet(CustomModelViewSet):
|
||||
"""
|
||||
入库明细
|
||||
"""
|
||||
queryset = WarehouseEntryItem.objects.all()
|
||||
serializer_class = WarehouseEntryItemSerializer
|
||||
filterset_fields = ['entry']
|
||||
ordering = 'create_time'
|
||||
|
||||
|
||||
class WarehouseStockViewSet(CustomModelViewSet):
|
||||
"""
|
||||
物料库存
|
||||
"""
|
||||
queryset = WarehouseStock.objects.all()
|
||||
serializer_class = WarehouseStockSerializer
|
||||
select_related_fields = ['warehouse', 'entry']
|
||||
search_fields = ['name', 'spec', 'supplier_name', 'entry_number']
|
||||
filterset_class = WarehouseStockFilter
|
||||
ordering = '-create_time'
|
||||
perms_map = {'get': '*', 'post': 'warehouse_stock.create',
|
||||
'put': 'warehouse_stock.update', 'delete': 'warehouse_stock.delete'}
|
||||
|
||||
|
||||
class MaterialRequisitionViewSet(TicketMixin, CustomModelViewSet):
|
||||
"""
|
||||
物资领用单
|
||||
"""
|
||||
queryset = MaterialRequisition.objects.all()
|
||||
serializer_class = MaterialRequisitionListSerializer
|
||||
retrieve_serializer_class = MaterialRequisitionDetailSerializer
|
||||
create_serializer_class = MaterialRequisitionCreateSerializer
|
||||
update_serializer_class = MaterialRequisitionCreateSerializer
|
||||
select_related_fields = ['create_by', 'belong_dept', 'ticket', 'ticket__state']
|
||||
search_fields = ['number', 'create_by__name', 'collector']
|
||||
filterset_class = MaterialRequisitionFilter
|
||||
ordering = '-create_time'
|
||||
workflow_key = 'wf_material_requis'
|
||||
|
||||
def gen_other_ticket_data(self, instance):
|
||||
dept_name = instance.belong_dept.name if instance.belong_dept else ''
|
||||
return {"dept_name": dept_name, "collector": instance.collector or ''}
|
||||
|
||||
@staticmethod
|
||||
def approve_requisition(ticket: Ticket, transition, new_ticket_data: dict):
|
||||
"""审批通过后,将库存物品状态改为已领用"""
|
||||
req = MaterialRequisition.objects.get(ticket=ticket)
|
||||
for item in req.items.filter(is_stock_item=True, stock__isnull=False):
|
||||
stock = WarehouseStock.objects.select_for_update().get(id=item.stock_id)
|
||||
stock.status = 'requisitioned'
|
||||
stock.save(update_fields=['status'])
|
||||
|
||||
@staticmethod
|
||||
def reject_requisition(ticket: Ticket, transition, new_ticket_data: dict):
|
||||
"""审批拒绝后,恢复库存数量和状态"""
|
||||
from apps.mpr.serializers import MaterialRequisitionCreateSerializer
|
||||
req = MaterialRequisition.objects.get(ticket=ticket)
|
||||
MaterialRequisitionCreateSerializer._restore_stock(req)
|
||||
|
||||
|
||||
class MaterialRequisitionItemViewSet(CustomModelViewSet):
|
||||
"""
|
||||
物资领用明细
|
||||
"""
|
||||
queryset = MaterialRequisitionItem.objects.all()
|
||||
serializer_class = MaterialRequisitionItemSerializer
|
||||
filterset_fields = ['requisition']
|
||||
ordering = 'create_time'
|
||||
|
|
@ -88,7 +88,8 @@ INSTALLED_APPS = [
|
|||
'apps.ofm',
|
||||
'apps.srm',
|
||||
'apps.asm',
|
||||
'apps.rem'
|
||||
'apps.rem',
|
||||
'apps.mpr'
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ urlpatterns = [
|
|||
path('', include('apps.srm.urls')),
|
||||
path('', include('apps.asm.urls')),
|
||||
path('', include('apps.rem.urls')),
|
||||
path('', include('apps.mpr.urls')),
|
||||
|
||||
# 前端页面入口
|
||||
path('', TemplateView.as_view(template_name="index.html")),
|
||||
|
|
|
|||
Loading…
Reference in New Issue