Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory
This commit is contained in:
commit
48305ed6fb
|
|
@ -13,6 +13,7 @@ class Migration(migrations.Migration):
|
|||
sql=[
|
||||
(
|
||||
"""
|
||||
CREATE EXTENSION IF NOT EXISTS timescaledb;
|
||||
CREATE TABLE public.enm_mplogx (
|
||||
"timex" timestamptz NOT NULL,
|
||||
"mpoint_id" text NOT NULL,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ class Migration(migrations.Migration):
|
|||
sql=[
|
||||
(
|
||||
"""
|
||||
CREATE EXTENSION IF NOT EXISTS timescaledb;
|
||||
CREATE TABLE public.enp_envdata (
|
||||
"timex" timestamptz NOT NULL,
|
||||
"equipment_id" text NOT NULL,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,164 @@
|
|||
# Generated by Django 4.2.27 on 2026-04-20 06:02
|
||||
|
||||
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'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('pum', '0010_quotationapply'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PuContract',
|
||||
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='合同名称')),
|
||||
('number', models.CharField(max_length=100, unique=True, verbose_name='合同编号')),
|
||||
('contract_amount', models.DecimalField(decimal_places=2, default=0, max_digits=14, verbose_name='合同金额')),
|
||||
('sign_date', models.DateField(verbose_name='签订日期')),
|
||||
('effective_date', models.DateField(blank=True, null=True, verbose_name='生效日期')),
|
||||
('end_date', models.DateField(blank=True, null=True, verbose_name='截止日期')),
|
||||
('status', models.PositiveSmallIntegerField(choices=[(10, '草稿'), (20, '执行中'), (30, '已完成'), (40, '已终止')], default=10, help_text="((10, '草稿'), (20, '执行中'), (30, '已完成'), (40, '已终止'))", verbose_name='合同状态')),
|
||||
('settlement_status', models.PositiveSmallIntegerField(choices=[(10, '未付款'), (20, '部分付款'), (30, '全部付款')], default=10, help_text="((10, '未付款'), (20, '部分付款'), (30, '全部付款'))", verbose_name='结算状态')),
|
||||
('paid_amount', models.DecimalField(decimal_places=2, default=0, max_digits=14, verbose_name='累计已付款')),
|
||||
('unpaid_amount', models.DecimalField(decimal_places=2, default=0, max_digits=14, verbose_name='累计未付款')),
|
||||
('pay_progress', models.DecimalField(decimal_places=2, default=0, max_digits=5, verbose_name='付款进度')),
|
||||
('description', models.CharField(blank=True, max_length=200, null=True, verbose_name='描述')),
|
||||
('belong_dept', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_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='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '采购合同',
|
||||
'verbose_name_plural': '采购合同',
|
||||
},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='puorder',
|
||||
name='belong_dept',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_belong_dept', to='system.dept', verbose_name='所属部门'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='puorder',
|
||||
name='create_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='puorder',
|
||||
name='update_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='puplan',
|
||||
name='belong_dept',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_belong_dept', to='system.dept', verbose_name='所属部门'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='puplan',
|
||||
name='create_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='puplan',
|
||||
name='update_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='puplanitem',
|
||||
name='belong_dept',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_belong_dept', to='system.dept', verbose_name='所属部门'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='puplanitem',
|
||||
name='create_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='puplanitem',
|
||||
name='update_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='quotationapply',
|
||||
name='create_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='quotationapply',
|
||||
name='update_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='supplier',
|
||||
name='belong_dept',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_belong_dept', to='system.dept', verbose_name='所属部门'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='supplier',
|
||||
name='create_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='supplier',
|
||||
name='update_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='supplieraudit',
|
||||
name='create_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='supplieraudit',
|
||||
name='update_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PuContractRecord',
|
||||
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='删除标记')),
|
||||
('record_date', models.DateField(verbose_name='付款日期')),
|
||||
('amount', models.DecimalField(decimal_places=2, max_digits=14, verbose_name='付款金额')),
|
||||
('stage_type', models.PositiveSmallIntegerField(choices=[(10, '首款'), (20, '中间款'), (30, '尾款'), (40, '其他')], default=40, help_text="((10, '首款'), (20, '中间款'), (30, '尾款'), (40, '其他'))", verbose_name='阶段类型')),
|
||||
('pay_method', models.PositiveSmallIntegerField(choices=[(10, '银行转账'), (20, '现金'), (30, '承兑'), (40, '微信'), (50, '支付宝'), (60, '其他')], default=10, help_text="((10, '银行转账'), (20, '现金'), (30, '承兑'), (40, '微信'), (50, '支付宝'), (60, '其他'))", verbose_name='付款方式')),
|
||||
('voucher_no', models.CharField(blank=True, max_length=100, null=True, verbose_name='凭证号')),
|
||||
('remark', models.CharField(blank=True, max_length=200, null=True, verbose_name='备注')),
|
||||
('belong_dept', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_belong_dept', to='system.dept', verbose_name='所属部门')),
|
||||
('contract', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='records', to='pum.pucontract', verbose_name='采购合同')),
|
||||
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
|
||||
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '采购合同付款流水',
|
||||
'verbose_name_plural': '采购合同付款流水',
|
||||
'ordering': ['-record_date', '-create_time'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='pucontract',
|
||||
name='supplier',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contracts', to='pum.supplier', verbose_name='供应商'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='pucontract',
|
||||
name='update_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='puorder',
|
||||
name='contract',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='orders', to='pum.pucontract', verbose_name='采购合同'),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,4 +1,7 @@
|
|||
from decimal import Decimal
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import Sum
|
||||
from apps.utils.models import CommonBModel, BaseModel, CommonBDModel, CommonADModel
|
||||
from apps.mtm.models import Material
|
||||
from apps.wf.models import Ticket
|
||||
|
|
@ -71,6 +74,9 @@ class PuOrder(CommonBModel):
|
|||
number = models.CharField('订单编号', max_length=20, null=True, blank=True)
|
||||
supplier = models.ForeignKey(
|
||||
Supplier, verbose_name='供应商', on_delete=models.CASCADE)
|
||||
contract = models.ForeignKey(
|
||||
'pum.PuContract', verbose_name='采购合同', on_delete=models.SET_NULL,
|
||||
null=True, blank=True, related_name='orders')
|
||||
delivery_date = models.DateField('截止到货日期', null=True, blank=True)
|
||||
submit_time = models.DateTimeField('提交时间', null=True, blank=True)
|
||||
submit_user = models.ForeignKey(
|
||||
|
|
@ -125,4 +131,152 @@ class QuotationApply(CommonADModel):
|
|||
quoter = models.CharField(max_length=50,verbose_name="报价人", null=True, blank=True)
|
||||
apply_date = models.DateField(verbose_name="申请日期",auto_now_add=True)
|
||||
ticket = models.OneToOneField('wf.ticket', verbose_name='关联工单',
|
||||
on_delete=models.CASCADE, related_name='quo_ticket', null=True, blank=True)
|
||||
on_delete=models.CASCADE, related_name='quo_ticket', null=True, blank=True)
|
||||
|
||||
|
||||
class PuContract(CommonBDModel):
|
||||
"""
|
||||
TN:采购合同
|
||||
"""
|
||||
STATUS_DRAFT = 10
|
||||
STATUS_ACTIVE = 20
|
||||
STATUS_DONE = 30
|
||||
STATUS_TERMINATED = 40
|
||||
STATUS_CHOICES = (
|
||||
(STATUS_DRAFT, '草稿'),
|
||||
(STATUS_ACTIVE, '执行中'),
|
||||
(STATUS_DONE, '已完成'),
|
||||
(STATUS_TERMINATED, '已终止'),
|
||||
)
|
||||
SETTLEMENT_UNPAID = 10
|
||||
SETTLEMENT_PARTIAL = 20
|
||||
SETTLEMENT_FULL = 30
|
||||
SETTLEMENT_CHOICES = (
|
||||
(SETTLEMENT_UNPAID, '未付款'),
|
||||
(SETTLEMENT_PARTIAL, '部分付款'),
|
||||
(SETTLEMENT_FULL, '全部付款'),
|
||||
)
|
||||
name = models.CharField('合同名称', max_length=100)
|
||||
number = models.CharField('合同编号', max_length=100, unique=True)
|
||||
supplier = models.ForeignKey(Supplier, verbose_name='供应商', on_delete=models.CASCADE, related_name='contracts')
|
||||
contract_amount = models.DecimalField('合同金额', max_digits=14, decimal_places=2, default=0)
|
||||
sign_date = models.DateField('签订日期')
|
||||
effective_date = models.DateField('生效日期', null=True, blank=True)
|
||||
end_date = models.DateField('截止日期', null=True, blank=True)
|
||||
status = models.PositiveSmallIntegerField(
|
||||
'合同状态', choices=STATUS_CHOICES, default=STATUS_DRAFT, help_text=str(STATUS_CHOICES))
|
||||
settlement_status = models.PositiveSmallIntegerField(
|
||||
'结算状态', choices=SETTLEMENT_CHOICES, default=SETTLEMENT_UNPAID, help_text=str(SETTLEMENT_CHOICES))
|
||||
paid_amount = models.DecimalField('累计已付款', max_digits=14, decimal_places=2, default=0)
|
||||
unpaid_amount = models.DecimalField('累计未付款', max_digits=14, decimal_places=2, default=0)
|
||||
pay_progress = models.DecimalField('付款进度', max_digits=5, decimal_places=2, default=0)
|
||||
description = models.CharField('描述', max_length=200, blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = '采购合同'
|
||||
verbose_name_plural = verbose_name
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
refresh_settlement = kwargs.pop('refresh_settlement', True)
|
||||
super().save(*args, **kwargs)
|
||||
if refresh_settlement:
|
||||
self.refresh_settlement()
|
||||
|
||||
def refresh_settlement(self):
|
||||
paid_amount = PuContractRecord.objects.filter(contract=self).aggregate(
|
||||
total=Sum('amount')
|
||||
)['total'] or Decimal('0.00')
|
||||
contract_amount = Decimal(str(self.contract_amount or 0)).quantize(Decimal('0.01'))
|
||||
unpaid_amount = contract_amount - paid_amount
|
||||
if unpaid_amount < Decimal('0.00'):
|
||||
unpaid_amount = Decimal('0.00')
|
||||
if contract_amount <= Decimal('0.00'):
|
||||
pay_progress = Decimal('0.00')
|
||||
else:
|
||||
pay_progress = (paid_amount * Decimal('100.00') / contract_amount).quantize(Decimal('0.01'))
|
||||
if pay_progress > Decimal('100.00'):
|
||||
pay_progress = Decimal('100.00')
|
||||
if paid_amount <= Decimal('0.00'):
|
||||
settlement_status = self.SETTLEMENT_UNPAID
|
||||
elif paid_amount >= contract_amount and contract_amount > Decimal('0.00'):
|
||||
settlement_status = self.SETTLEMENT_FULL
|
||||
else:
|
||||
settlement_status = self.SETTLEMENT_PARTIAL
|
||||
status = self.status
|
||||
if status != self.STATUS_TERMINATED:
|
||||
if paid_amount <= Decimal('0.00'):
|
||||
status = self.STATUS_DRAFT
|
||||
elif paid_amount >= contract_amount and contract_amount > Decimal('0.00'):
|
||||
status = self.STATUS_DONE
|
||||
else:
|
||||
status = self.STATUS_ACTIVE
|
||||
type(self).objects.filter(pk=self.pk).update(
|
||||
paid_amount=paid_amount,
|
||||
unpaid_amount=unpaid_amount,
|
||||
pay_progress=pay_progress,
|
||||
settlement_status=settlement_status,
|
||||
status=status,
|
||||
)
|
||||
self.paid_amount = paid_amount
|
||||
self.unpaid_amount = unpaid_amount
|
||||
self.pay_progress = pay_progress
|
||||
self.settlement_status = settlement_status
|
||||
self.status = status
|
||||
|
||||
|
||||
class PuContractRecord(CommonBDModel):
|
||||
"""
|
||||
TN:采购合同付款流水
|
||||
"""
|
||||
STAGE_FIRST = 10
|
||||
STAGE_MIDDLE = 20
|
||||
STAGE_FINAL = 30
|
||||
STAGE_OTHER = 40
|
||||
STAGE_CHOICES = (
|
||||
(STAGE_FIRST, '首款'),
|
||||
(STAGE_MIDDLE, '中间款'),
|
||||
(STAGE_FINAL, '尾款'),
|
||||
(STAGE_OTHER, '其他'),
|
||||
)
|
||||
PAY_BANK = 10
|
||||
PAY_CASH = 20
|
||||
PAY_ACCEPTANCE = 30
|
||||
PAY_WECHAT = 40
|
||||
PAY_ALIPAY = 50
|
||||
PAY_OTHER = 60
|
||||
PAY_METHOD_CHOICES = (
|
||||
(PAY_BANK, '银行转账'),
|
||||
(PAY_CASH, '现金'),
|
||||
(PAY_ACCEPTANCE, '承兑'),
|
||||
(PAY_WECHAT, '微信'),
|
||||
(PAY_ALIPAY, '支付宝'),
|
||||
(PAY_OTHER, '其他'),
|
||||
)
|
||||
contract = models.ForeignKey(
|
||||
PuContract, verbose_name='采购合同', on_delete=models.CASCADE, related_name='records')
|
||||
record_date = models.DateField('付款日期')
|
||||
amount = models.DecimalField('付款金额', max_digits=14, decimal_places=2)
|
||||
stage_type = models.PositiveSmallIntegerField(
|
||||
'阶段类型', choices=STAGE_CHOICES, default=STAGE_OTHER, help_text=str(STAGE_CHOICES))
|
||||
pay_method = models.PositiveSmallIntegerField(
|
||||
'付款方式', choices=PAY_METHOD_CHOICES, default=PAY_BANK, help_text=str(PAY_METHOD_CHOICES))
|
||||
voucher_no = models.CharField('凭证号', max_length=100, null=True, blank=True)
|
||||
remark = models.CharField('备注', max_length=200, null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = '采购合同付款流水'
|
||||
verbose_name_plural = verbose_name
|
||||
ordering = ['-record_date', '-create_time']
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
self.contract.refresh_settlement()
|
||||
|
||||
def delete(self, using=None, *args, **kwargs):
|
||||
contract = self.contract
|
||||
result = super().delete(using=using, *args, **kwargs)
|
||||
contract.refresh_settlement()
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
from decimal import Decimal
|
||||
|
||||
from rest_framework import serializers
|
||||
from apps.utils.serializers import CustomModelSerializer
|
||||
from apps.utils.constants import EXCLUDE_FIELDS_DEPT, EXCLUDE_FIELDS_BASE, EXCLUDE_FIELDS
|
||||
from rest_framework.exceptions import ValidationError, ParseError
|
||||
|
||||
from apps.pum.models import Supplier, PuPlan, PuPlanItem, PuOrder, PuOrderItem, SupplierAudit, QuotationApply
|
||||
from apps.pum.models import Supplier, PuPlan, PuPlanItem, PuOrder, PuOrderItem, SupplierAudit, QuotationApply, PuContract, PuContractRecord
|
||||
from apps.mtm.serializers import MaterialSerializer, MaterialSimpleSerializer
|
||||
from django.db import transaction
|
||||
from .services import PumService
|
||||
|
|
@ -99,6 +101,14 @@ class PuOrderSerializer(CustomModelSerializer):
|
|||
fields = '__all__'
|
||||
read_only_fields = EXCLUDE_FIELDS_DEPT + ['state', 'submit_time', 'total_price']
|
||||
|
||||
def validate(self, attrs):
|
||||
contract = attrs.get('contract', None)
|
||||
if contract:
|
||||
attrs['supplier'] = contract.supplier
|
||||
if attrs.get('supplier', None) is None:
|
||||
raise ValidationError('未选择供应商')
|
||||
return attrs
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
validated_data.pop('supplier')
|
||||
if instance.state != PuOrder.PUORDER_CREATE:
|
||||
|
|
@ -164,4 +174,40 @@ class QuotationApplySerializer(CustomModelSerializer):
|
|||
class Meta:
|
||||
model = QuotationApply
|
||||
fields = "__all__"
|
||||
read_only_fields = EXCLUDE_FIELDS
|
||||
read_only_fields = EXCLUDE_FIELDS
|
||||
|
||||
|
||||
class PuContractSerializer(CustomModelSerializer):
|
||||
supplier_name = serializers.CharField(source='supplier.name', read_only=True)
|
||||
create_by_name = serializers.CharField(source='create_by.name', read_only=True)
|
||||
update_by_name = serializers.CharField(source='update_by.name', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = PuContract
|
||||
fields = '__all__'
|
||||
read_only_fields = EXCLUDE_FIELDS + ['belong_dept', 'paid_amount', 'unpaid_amount', 'pay_progress', 'settlement_status']
|
||||
|
||||
|
||||
class PuContractRecordSerializer(CustomModelSerializer):
|
||||
contract_number = serializers.CharField(source='contract.number', read_only=True)
|
||||
supplier_name = serializers.CharField(source='contract.supplier.name', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = PuContractRecord
|
||||
fields = '__all__'
|
||||
read_only_fields = EXCLUDE_FIELDS + ['belong_dept']
|
||||
|
||||
def validate(self, attrs):
|
||||
contract = attrs.get('contract', getattr(self.instance, 'contract', None))
|
||||
amount = attrs.get('amount', getattr(self.instance, 'amount', None))
|
||||
if contract is None or amount is None:
|
||||
return attrs
|
||||
if contract.status == PuContract.STATUS_TERMINATED:
|
||||
raise ValidationError('合同已终止,不可操作付款流水')
|
||||
qs = PuContractRecord.objects.filter(contract=contract)
|
||||
if self.instance is not None:
|
||||
qs = qs.exclude(id=self.instance.id)
|
||||
total = sum((item.amount for item in qs), Decimal('0.00')) + amount
|
||||
if total > contract.contract_amount:
|
||||
raise ValidationError('累计付款金额不可超过合同金额')
|
||||
return attrs
|
||||
|
|
|
|||
|
|
@ -1,3 +1,178 @@
|
|||
from decimal import Decimal
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
from apps.pum.models import Supplier
|
||||
from apps.pum.serializers import PuOrderSerializer
|
||||
from apps.pum.serializers import PuContractRecordSerializer
|
||||
from rest_framework.exceptions import ParseError
|
||||
|
||||
|
||||
class PuContractSettlementTests(TestCase):
|
||||
def test_purchase_contract_record_updates_paid_summary(self):
|
||||
supplier = Supplier.objects.create(name='供应商A')
|
||||
|
||||
from apps.pum.models import PuContract, PuContractRecord
|
||||
|
||||
contract = PuContract.objects.create(
|
||||
name='采购合同A',
|
||||
number='PC-001',
|
||||
contract_amount=Decimal('2000.00'),
|
||||
supplier=supplier,
|
||||
sign_date='2026-04-20',
|
||||
)
|
||||
|
||||
PuContractRecord.objects.create(
|
||||
contract=contract,
|
||||
record_date='2026-04-21',
|
||||
amount=Decimal('800.00'),
|
||||
stage_type=PuContractRecord.STAGE_FIRST,
|
||||
)
|
||||
|
||||
contract.refresh_from_db()
|
||||
self.assertEqual(contract.paid_amount, Decimal('800.00'))
|
||||
self.assertEqual(contract.unpaid_amount, Decimal('1200.00'))
|
||||
self.assertEqual(contract.pay_progress, Decimal('40.00'))
|
||||
self.assertEqual(contract.status, contract.STATUS_ACTIVE)
|
||||
|
||||
def test_purchase_contract_record_delete_refreshes_summary_and_is_physical(self):
|
||||
supplier = Supplier.objects.create(name='供应商C')
|
||||
|
||||
from apps.pum.models import PuContract, PuContractRecord
|
||||
|
||||
contract = PuContract.objects.create(
|
||||
name='采购合同C',
|
||||
number='PC-003',
|
||||
contract_amount=Decimal('2000.00'),
|
||||
supplier=supplier,
|
||||
sign_date='2026-04-20',
|
||||
)
|
||||
|
||||
record = PuContractRecord.objects.create(
|
||||
contract=contract,
|
||||
record_date='2026-04-21',
|
||||
amount=Decimal('800.00'),
|
||||
stage_type=PuContractRecord.STAGE_FIRST,
|
||||
)
|
||||
|
||||
record.delete()
|
||||
contract.refresh_from_db()
|
||||
|
||||
self.assertEqual(contract.paid_amount, Decimal('0.00'))
|
||||
self.assertEqual(contract.unpaid_amount, Decimal('2000.00'))
|
||||
self.assertEqual(contract.pay_progress, Decimal('0.00'))
|
||||
self.assertEqual(contract.status, contract.STATUS_DRAFT)
|
||||
self.assertFalse(PuContractRecord._base_manager.filter(pk=record.pk).exists())
|
||||
|
||||
def test_purchase_contract_delete_is_physical(self):
|
||||
supplier = Supplier.objects.create(name='供应商D')
|
||||
|
||||
from apps.pum.models import PuContract
|
||||
|
||||
contract = PuContract.objects.create(
|
||||
name='采购合同D',
|
||||
number='PC-004',
|
||||
contract_amount=Decimal('500.00'),
|
||||
supplier=supplier,
|
||||
sign_date='2026-04-20',
|
||||
)
|
||||
|
||||
contract.delete()
|
||||
|
||||
self.assertFalse(PuContract._base_manager.filter(pk=contract.pk).exists())
|
||||
|
||||
def test_purchase_contract_status_auto_transitions_by_records(self):
|
||||
supplier = Supplier.objects.create(name='供应商E')
|
||||
|
||||
from apps.pum.models import PuContract, PuContractRecord
|
||||
|
||||
contract = PuContract.objects.create(
|
||||
name='采购合同E',
|
||||
number='PC-005',
|
||||
contract_amount=Decimal('2000.00'),
|
||||
supplier=supplier,
|
||||
sign_date='2026-04-20',
|
||||
)
|
||||
|
||||
self.assertEqual(contract.status, PuContract.STATUS_DRAFT)
|
||||
|
||||
first_record = PuContractRecord.objects.create(
|
||||
contract=contract,
|
||||
record_date='2026-04-21',
|
||||
amount=Decimal('800.00'),
|
||||
stage_type=PuContractRecord.STAGE_FIRST,
|
||||
)
|
||||
contract.refresh_from_db()
|
||||
self.assertEqual(contract.status, PuContract.STATUS_ACTIVE)
|
||||
|
||||
PuContractRecord.objects.create(
|
||||
contract=contract,
|
||||
record_date='2026-04-22',
|
||||
amount=Decimal('1200.00'),
|
||||
stage_type=PuContractRecord.STAGE_FINAL,
|
||||
)
|
||||
contract.refresh_from_db()
|
||||
self.assertEqual(contract.status, PuContract.STATUS_DONE)
|
||||
|
||||
first_record.delete()
|
||||
contract.refresh_from_db()
|
||||
self.assertEqual(contract.status, PuContract.STATUS_ACTIVE)
|
||||
|
||||
def test_purchase_terminated_contract_forbids_record_changes(self):
|
||||
supplier = Supplier.objects.create(name='供应商F')
|
||||
|
||||
from apps.pum.models import PuContract, PuContractRecord
|
||||
|
||||
contract = PuContract.objects.create(
|
||||
name='采购合同F',
|
||||
number='PC-006',
|
||||
contract_amount=Decimal('1000.00'),
|
||||
supplier=supplier,
|
||||
sign_date='2026-04-20',
|
||||
status=PuContract.STATUS_TERMINATED,
|
||||
)
|
||||
|
||||
serializer = PuContractRecordSerializer(data={
|
||||
'contract': contract.id,
|
||||
'record_date': '2026-04-21',
|
||||
'amount': '100.00',
|
||||
'stage_type': 10,
|
||||
'pay_method': 10,
|
||||
})
|
||||
self.assertFalse(serializer.is_valid())
|
||||
self.assertIn('合同已终止,不可操作付款流水', str(serializer.errors))
|
||||
|
||||
contract.status = PuContract.STATUS_DRAFT
|
||||
contract.save(refresh_settlement=False)
|
||||
|
||||
record = PuContractRecord.objects.create(
|
||||
contract=contract,
|
||||
record_date='2026-04-20',
|
||||
amount=Decimal('100.00'),
|
||||
stage_type=PuContractRecord.STAGE_OTHER,
|
||||
)
|
||||
contract.status = PuContract.STATUS_TERMINATED
|
||||
contract.save(refresh_settlement=False)
|
||||
|
||||
with self.assertRaises(ParseError):
|
||||
from apps.pum.views import PuContractRecordViewSet
|
||||
viewset = PuContractRecordViewSet()
|
||||
viewset.request = None
|
||||
viewset.perform_destroy(record)
|
||||
|
||||
def test_purchase_order_serializer_accepts_purchase_contract(self):
|
||||
supplier = Supplier.objects.create(name='供应商B')
|
||||
|
||||
from apps.pum.models import PuContract
|
||||
|
||||
contract = PuContract.objects.create(
|
||||
name='采购合同B',
|
||||
number='PC-002',
|
||||
contract_amount=Decimal('500.00'),
|
||||
supplier=supplier,
|
||||
sign_date='2026-04-20',
|
||||
)
|
||||
|
||||
serializer = PuOrderSerializer(data={'supplier': supplier.id, 'contract': contract.id})
|
||||
|
||||
self.assertTrue(serializer.is_valid(), serializer.errors)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from apps.pum.views import (SupplierViewSet, PuPlanViewSet, PuPlanItemViewSet, PuOrderViewSet, PuOrderItemViewSet, SupplierAuditViewSet, QuotationApplyViewSet)
|
||||
from apps.pum.views import (SupplierViewSet, PuPlanViewSet, PuPlanItemViewSet, PuOrderViewSet, PuOrderItemViewSet, SupplierAuditViewSet, QuotationApplyViewSet, PuContractViewSet, PuContractRecordViewSet)
|
||||
# from apps.pum.views import SupplierViewSet, PuPlanViewSet, PuPlanItemViewSet, PuOrderViewSet, PuOrderItemViewSet
|
||||
|
||||
API_BASE_URL = 'api/pum/'
|
||||
|
|
@ -11,9 +11,11 @@ router.register('supplier', SupplierViewSet, basename='supplier')
|
|||
router.register('supplieraudit', SupplierAuditViewSet, basename='supplieraudit')
|
||||
router.register('pu_plan', PuPlanViewSet, basename='pu_plan')
|
||||
router.register('pu_planitem', PuPlanItemViewSet, basename='pu_planitem')
|
||||
router.register('pu_contract', PuContractViewSet, basename='pu_contract')
|
||||
router.register('pu_contract_record', PuContractRecordViewSet, basename='pu_contract_record')
|
||||
router.register('pu_order', PuOrderViewSet, basename='pu_order')
|
||||
router.register('pu_orderitem', PuOrderItemViewSet, basename='pu_orderitem')
|
||||
router.register('quotation', QuotationApplyViewSet, basename='quotation')
|
||||
urlpatterns = [
|
||||
path(API_BASE_URL, include(router.urls)),
|
||||
]
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
from django.shortcuts import render
|
||||
from apps.pum.models import Supplier, PuPlan, PuPlanItem, PuOrder, PuOrderItem, SupplierAudit, QuotationApply
|
||||
from apps.pum.models import Supplier, PuPlan, PuPlanItem, PuOrder, PuOrderItem, SupplierAudit, QuotationApply, PuContract, PuContractRecord
|
||||
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet, EuModelViewSet
|
||||
from apps.pum.serializers import (SupplierSerializer, PuPlanSerializer, PuPlanItemSerializer, QuotationApplySerializer,
|
||||
PuOrderSerializer, PuOrderItemSerializer, AddSerializer, SupplierAuditSerializer)
|
||||
PuOrderSerializer, PuOrderItemSerializer, AddSerializer, SupplierAuditSerializer,
|
||||
PuContractSerializer, PuContractRecordSerializer)
|
||||
from rest_framework.exceptions import ParseError, PermissionDenied
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework import serializers
|
||||
|
|
@ -222,4 +223,51 @@ class QuotationApplyViewSet(TicketMixin, CustomModelViewSet):
|
|||
filterset_fields = ['product_name', 'customer_name','apply_date', 'quoter']
|
||||
search_fields = ['product_name', 'customer_name','contact_person']
|
||||
ordering = ['create_time']
|
||||
workflow_key = "wf_quotation"
|
||||
workflow_key = "wf_quotation"
|
||||
|
||||
|
||||
class PuContractViewSet(CustomModelViewSet):
|
||||
"""
|
||||
list: 采购合同
|
||||
|
||||
采购合同
|
||||
"""
|
||||
queryset = PuContract.objects.all()
|
||||
serializer_class = PuContractSerializer
|
||||
search_fields = ['name', 'number', 'supplier__name']
|
||||
select_related_fields = ['supplier', 'create_by', 'update_by']
|
||||
filterset_fields = ['supplier', 'status', 'settlement_status']
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
if PuOrder.objects.filter(contract=instance).exists():
|
||||
raise ParseError('该采购合同存在采购订单不可删除')
|
||||
instance.delete()
|
||||
|
||||
|
||||
class PuContractRecordViewSet(CustomModelViewSet):
|
||||
"""
|
||||
list: 采购合同付款流水
|
||||
|
||||
采购合同付款流水
|
||||
"""
|
||||
perms_map = {
|
||||
'get': '*',
|
||||
'post': 'pu_contract.update',
|
||||
'put': 'pu_contract.update',
|
||||
'patch': 'pu_contract.update',
|
||||
'delete': 'pu_contract.update',
|
||||
}
|
||||
queryset = PuContractRecord.objects.all()
|
||||
serializer_class = PuContractRecordSerializer
|
||||
search_fields = ['contract__number', 'contract__name', 'voucher_no', 'remark']
|
||||
select_related_fields = ['contract', 'contract__supplier', 'create_by', 'update_by']
|
||||
filterset_fields = {
|
||||
'contract': ['exact'],
|
||||
'stage_type': ['exact', 'in'],
|
||||
'pay_method': ['exact', 'in'],
|
||||
}
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
if instance.contract.status == PuContract.STATUS_TERMINATED:
|
||||
raise ParseError('合同已终止,不可删除付款流水')
|
||||
instance.delete()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,122 @@
|
|||
# Generated by Django 4.2.27 on 2026-04-20 06:02
|
||||
|
||||
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'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('sam', '0008_alter_orderitem_order'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='contract',
|
||||
name='effective_date',
|
||||
field=models.DateField(blank=True, null=True, verbose_name='生效日期'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='contract',
|
||||
name='end_date',
|
||||
field=models.DateField(blank=True, null=True, verbose_name='截止日期'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='contract',
|
||||
name='receive_progress',
|
||||
field=models.DecimalField(decimal_places=2, default=0, max_digits=5, verbose_name='到款进度'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='contract',
|
||||
name='received_amount',
|
||||
field=models.DecimalField(decimal_places=2, default=0, max_digits=14, verbose_name='累计已到款'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='contract',
|
||||
name='settlement_status',
|
||||
field=models.PositiveSmallIntegerField(choices=[(10, '未到款'), (20, '部分到款'), (30, '全部到款')], default=10, help_text="((10, '未到款'), (20, '部分到款'), (30, '全部到款'))", verbose_name='结算状态'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='contract',
|
||||
name='status',
|
||||
field=models.PositiveSmallIntegerField(choices=[(10, '草稿'), (20, '执行中'), (30, '已完成'), (40, '已终止')], default=10, help_text="((10, '草稿'), (20, '执行中'), (30, '已完成'), (40, '已终止'))", verbose_name='合同状态'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='contract',
|
||||
name='unreceived_amount',
|
||||
field=models.DecimalField(decimal_places=2, default=0, max_digits=14, verbose_name='累计未到款'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='contract',
|
||||
name='belong_dept',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_belong_dept', to='system.dept', verbose_name='所属部门'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='contract',
|
||||
name='create_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='contract',
|
||||
name='update_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='customer',
|
||||
name='belong_dept',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_belong_dept', to='system.dept', verbose_name='所属部门'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='customer',
|
||||
name='create_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='customer',
|
||||
name='update_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='order',
|
||||
name='belong_dept',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_belong_dept', to='system.dept', verbose_name='所属部门'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='order',
|
||||
name='create_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='order',
|
||||
name='update_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ContractRecord',
|
||||
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='删除标记')),
|
||||
('record_date', models.DateField(verbose_name='到款日期')),
|
||||
('amount', models.DecimalField(decimal_places=2, max_digits=14, verbose_name='到款金额')),
|
||||
('stage_type', models.PositiveSmallIntegerField(choices=[(10, '首款'), (20, '中间款'), (30, '尾款'), (40, '其他')], default=40, help_text="((10, '首款'), (20, '中间款'), (30, '尾款'), (40, '其他'))", verbose_name='阶段类型')),
|
||||
('pay_method', models.PositiveSmallIntegerField(choices=[(10, '银行转账'), (20, '现金'), (30, '承兑'), (40, '微信'), (50, '支付宝'), (60, '其他')], default=10, help_text="((10, '银行转账'), (20, '现金'), (30, '承兑'), (40, '微信'), (50, '支付宝'), (60, '其他'))", verbose_name='收款方式')),
|
||||
('voucher_no', models.CharField(blank=True, max_length=100, null=True, verbose_name='凭证号')),
|
||||
('remark', models.CharField(blank=True, max_length=200, null=True, verbose_name='备注')),
|
||||
('belong_dept', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_belong_dept', to='system.dept', verbose_name='所属部门')),
|
||||
('contract', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='records', to='sam.contract', verbose_name='销售合同')),
|
||||
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
|
||||
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '销售合同到款流水',
|
||||
'verbose_name_plural': '销售合同到款流水',
|
||||
'ordering': ['-record_date', '-create_time'],
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -1,4 +1,7 @@
|
|||
from decimal import Decimal
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import Sum
|
||||
from apps.utils.models import CommonBModel, BaseModel, CommonBDModel
|
||||
from apps.mtm.models import Material
|
||||
|
||||
|
|
@ -24,16 +27,43 @@ class Customer(CommonBModel):
|
|||
return self.name
|
||||
|
||||
|
||||
class Contract(CommonBModel):
|
||||
class Contract(CommonBDModel):
|
||||
"""
|
||||
TN:合同信息
|
||||
"""
|
||||
STATUS_DRAFT = 10
|
||||
STATUS_ACTIVE = 20
|
||||
STATUS_DONE = 30
|
||||
STATUS_TERMINATED = 40
|
||||
STATUS_CHOICES = (
|
||||
(STATUS_DRAFT, '草稿'),
|
||||
(STATUS_ACTIVE, '执行中'),
|
||||
(STATUS_DONE, '已完成'),
|
||||
(STATUS_TERMINATED, '已终止'),
|
||||
)
|
||||
SETTLEMENT_UNRECEIVED = 10
|
||||
SETTLEMENT_PARTIAL = 20
|
||||
SETTLEMENT_FULL = 30
|
||||
SETTLEMENT_CHOICES = (
|
||||
(SETTLEMENT_UNRECEIVED, '未到款'),
|
||||
(SETTLEMENT_PARTIAL, '部分到款'),
|
||||
(SETTLEMENT_FULL, '全部到款'),
|
||||
)
|
||||
name = models.CharField('合同名称', max_length=100)
|
||||
number = models.CharField('合同编号', max_length=100, unique=True)
|
||||
amount = models.IntegerField('合同金额', default=0)
|
||||
customer = models.ForeignKey(Customer, verbose_name='关联客户',
|
||||
on_delete=models.CASCADE, related_name='contract_customer')
|
||||
sign_date = models.DateField('签订日期')
|
||||
effective_date = models.DateField('生效日期', null=True, blank=True)
|
||||
end_date = models.DateField('截止日期', null=True, blank=True)
|
||||
status = models.PositiveSmallIntegerField(
|
||||
'合同状态', choices=STATUS_CHOICES, default=STATUS_DRAFT, help_text=str(STATUS_CHOICES))
|
||||
settlement_status = models.PositiveSmallIntegerField(
|
||||
'结算状态', choices=SETTLEMENT_CHOICES, default=SETTLEMENT_UNRECEIVED, help_text=str(SETTLEMENT_CHOICES))
|
||||
received_amount = models.DecimalField('累计已到款', max_digits=14, decimal_places=2, default=0)
|
||||
unreceived_amount = models.DecimalField('累计未到款', max_digits=14, decimal_places=2, default=0)
|
||||
receive_progress = models.DecimalField('到款进度', max_digits=5, decimal_places=2, default=0)
|
||||
description = models.CharField('描述', max_length=200, blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
|
|
@ -43,6 +73,53 @@ class Contract(CommonBModel):
|
|||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
refresh_settlement = kwargs.pop('refresh_settlement', True)
|
||||
super().save(*args, **kwargs)
|
||||
if refresh_settlement:
|
||||
self.refresh_settlement()
|
||||
|
||||
def refresh_settlement(self):
|
||||
received_amount = ContractRecord.objects.filter(contract=self).aggregate(
|
||||
total=Sum('amount')
|
||||
)['total'] or Decimal('0.00')
|
||||
contract_amount = Decimal(str(self.amount or 0)).quantize(Decimal('0.01'))
|
||||
unreceived_amount = contract_amount - received_amount
|
||||
if unreceived_amount < Decimal('0.00'):
|
||||
unreceived_amount = Decimal('0.00')
|
||||
if contract_amount <= Decimal('0.00'):
|
||||
receive_progress = Decimal('0.00')
|
||||
else:
|
||||
receive_progress = (received_amount * Decimal('100.00') / contract_amount).quantize(Decimal('0.01'))
|
||||
if receive_progress > Decimal('100.00'):
|
||||
receive_progress = Decimal('100.00')
|
||||
if received_amount <= Decimal('0.00'):
|
||||
settlement_status = self.SETTLEMENT_UNRECEIVED
|
||||
elif received_amount >= contract_amount and contract_amount > Decimal('0.00'):
|
||||
settlement_status = self.SETTLEMENT_FULL
|
||||
else:
|
||||
settlement_status = self.SETTLEMENT_PARTIAL
|
||||
status = self.status
|
||||
if status != self.STATUS_TERMINATED:
|
||||
if received_amount <= Decimal('0.00'):
|
||||
status = self.STATUS_DRAFT
|
||||
elif received_amount >= contract_amount and contract_amount > Decimal('0.00'):
|
||||
status = self.STATUS_DONE
|
||||
else:
|
||||
status = self.STATUS_ACTIVE
|
||||
type(self).objects.filter(pk=self.pk).update(
|
||||
received_amount=received_amount,
|
||||
unreceived_amount=unreceived_amount,
|
||||
receive_progress=receive_progress,
|
||||
settlement_status=settlement_status,
|
||||
status=status,
|
||||
)
|
||||
self.received_amount = received_amount
|
||||
self.unreceived_amount = unreceived_amount
|
||||
self.receive_progress = receive_progress
|
||||
self.settlement_status = settlement_status
|
||||
self.status = status
|
||||
|
||||
|
||||
class Order(CommonBModel):
|
||||
"""
|
||||
|
|
@ -87,3 +164,58 @@ class OrderItem(BaseModel):
|
|||
delivered_count = models.PositiveIntegerField('已交货数量', default=0)
|
||||
utask = models.ForeignKey('pm.utask', verbose_name='关联生产大任务',
|
||||
on_delete=models.SET_NULL, null=True, blank=True)
|
||||
|
||||
|
||||
class ContractRecord(CommonBDModel):
|
||||
"""
|
||||
TN:销售合同到款流水
|
||||
"""
|
||||
STAGE_FIRST = 10
|
||||
STAGE_MIDDLE = 20
|
||||
STAGE_FINAL = 30
|
||||
STAGE_OTHER = 40
|
||||
STAGE_CHOICES = (
|
||||
(STAGE_FIRST, '首款'),
|
||||
(STAGE_MIDDLE, '中间款'),
|
||||
(STAGE_FINAL, '尾款'),
|
||||
(STAGE_OTHER, '其他'),
|
||||
)
|
||||
PAY_BANK = 10
|
||||
PAY_CASH = 20
|
||||
PAY_ACCEPTANCE = 30
|
||||
PAY_WECHAT = 40
|
||||
PAY_ALIPAY = 50
|
||||
PAY_OTHER = 60
|
||||
PAY_METHOD_CHOICES = (
|
||||
(PAY_BANK, '银行转账'),
|
||||
(PAY_CASH, '现金'),
|
||||
(PAY_ACCEPTANCE, '承兑'),
|
||||
(PAY_WECHAT, '微信'),
|
||||
(PAY_ALIPAY, '支付宝'),
|
||||
(PAY_OTHER, '其他'),
|
||||
)
|
||||
contract = models.ForeignKey(
|
||||
Contract, verbose_name='销售合同', on_delete=models.CASCADE, related_name='records')
|
||||
record_date = models.DateField('到款日期')
|
||||
amount = models.DecimalField('到款金额', max_digits=14, decimal_places=2)
|
||||
stage_type = models.PositiveSmallIntegerField(
|
||||
'阶段类型', choices=STAGE_CHOICES, default=STAGE_OTHER, help_text=str(STAGE_CHOICES))
|
||||
pay_method = models.PositiveSmallIntegerField(
|
||||
'收款方式', choices=PAY_METHOD_CHOICES, default=PAY_BANK, help_text=str(PAY_METHOD_CHOICES))
|
||||
voucher_no = models.CharField('凭证号', max_length=100, null=True, blank=True)
|
||||
remark = models.CharField('备注', max_length=200, null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = '销售合同到款流水'
|
||||
verbose_name_plural = verbose_name
|
||||
ordering = ['-record_date', '-create_time']
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
self.contract.refresh_settlement()
|
||||
|
||||
def delete(self, using=None, *args, **kwargs):
|
||||
contract = self.contract
|
||||
result = super().delete(using=using, *args, **kwargs)
|
||||
contract.refresh_settlement()
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
from decimal import Decimal
|
||||
|
||||
from rest_framework import serializers
|
||||
from apps.utils.serializers import CustomModelSerializer
|
||||
from apps.sam.models import Customer, Contract, Order, OrderItem
|
||||
from apps.sam.models import Customer, Contract, Order, OrderItem, ContractRecord
|
||||
from apps.utils.constants import EXCLUDE_FIELDS, EXCLUDE_FIELDS_BASE
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from apps.mtm.serializers import MaterialSerializer
|
||||
|
|
@ -81,3 +83,29 @@ class OrderItemSerializer(CustomModelSerializer):
|
|||
validated_data.pop('product', None)
|
||||
validated_data.pop('order', None)
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
class ContractRecordSerializer(CustomModelSerializer):
|
||||
contract_number = serializers.CharField(source='contract.number', read_only=True)
|
||||
customer_name = serializers.CharField(source='contract.customer.name', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ContractRecord
|
||||
fields = '__all__'
|
||||
read_only_fields = EXCLUDE_FIELDS + ['belong_dept']
|
||||
|
||||
def validate(self, attrs):
|
||||
contract = attrs.get('contract', getattr(self.instance, 'contract', None))
|
||||
amount = attrs.get('amount', getattr(self.instance, 'amount', None))
|
||||
if contract is None or amount is None:
|
||||
return attrs
|
||||
if contract.status == Contract.STATUS_TERMINATED:
|
||||
raise ValidationError('合同已终止,不可操作到款流水')
|
||||
qs = ContractRecord.objects.filter(contract=contract)
|
||||
if self.instance is not None:
|
||||
qs = qs.exclude(id=self.instance.id)
|
||||
total = sum((item.amount for item in qs), Decimal('0.00')) + amount
|
||||
contract_amount = Decimal(str(contract.amount or 0))
|
||||
if total > contract_amount:
|
||||
raise ValidationError('累计到款金额不可超过合同金额')
|
||||
return attrs
|
||||
|
|
|
|||
|
|
@ -1,3 +1,178 @@
|
|||
from decimal import Decimal
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
from apps.sam.models import Contract, Customer
|
||||
from apps.sam.serializers import ContractRecordSerializer
|
||||
from rest_framework.exceptions import ParseError
|
||||
|
||||
|
||||
class ContractSettlementTests(TestCase):
|
||||
def test_sales_contract_record_updates_received_summary(self):
|
||||
customer = Customer.objects.create(
|
||||
name='客户A',
|
||||
contact='张三',
|
||||
contact_phone='13800000001',
|
||||
)
|
||||
contract = Contract.objects.create(
|
||||
name='销售合同A',
|
||||
number='SC-001',
|
||||
amount=1000,
|
||||
customer=customer,
|
||||
sign_date='2026-04-20',
|
||||
)
|
||||
|
||||
from apps.sam.models import ContractRecord
|
||||
|
||||
ContractRecord.objects.create(
|
||||
contract=contract,
|
||||
record_date='2026-04-21',
|
||||
amount=Decimal('300.00'),
|
||||
stage_type=ContractRecord.STAGE_FIRST,
|
||||
)
|
||||
ContractRecord.objects.create(
|
||||
contract=contract,
|
||||
record_date='2026-04-22',
|
||||
amount=Decimal('200.00'),
|
||||
stage_type=ContractRecord.STAGE_MIDDLE,
|
||||
)
|
||||
|
||||
contract.refresh_from_db()
|
||||
self.assertEqual(contract.received_amount, Decimal('500.00'))
|
||||
self.assertEqual(contract.unreceived_amount, Decimal('500.00'))
|
||||
self.assertEqual(contract.receive_progress, Decimal('50.00'))
|
||||
self.assertEqual(contract.status, Contract.STATUS_ACTIVE)
|
||||
|
||||
def test_sales_contract_record_delete_refreshes_summary_and_is_physical(self):
|
||||
customer = Customer.objects.create(
|
||||
name='客户B',
|
||||
contact='李四',
|
||||
contact_phone='13800000002',
|
||||
)
|
||||
contract = Contract.objects.create(
|
||||
name='销售合同B',
|
||||
number='SC-002',
|
||||
amount=1000,
|
||||
customer=customer,
|
||||
sign_date='2026-04-20',
|
||||
)
|
||||
|
||||
from apps.sam.models import ContractRecord
|
||||
|
||||
record = ContractRecord.objects.create(
|
||||
contract=contract,
|
||||
record_date='2026-04-21',
|
||||
amount=Decimal('300.00'),
|
||||
stage_type=ContractRecord.STAGE_FIRST,
|
||||
)
|
||||
|
||||
record.delete()
|
||||
contract.refresh_from_db()
|
||||
|
||||
self.assertEqual(contract.received_amount, Decimal('0.00'))
|
||||
self.assertEqual(contract.unreceived_amount, Decimal('1000.00'))
|
||||
self.assertEqual(contract.receive_progress, Decimal('0.00'))
|
||||
self.assertEqual(contract.status, Contract.STATUS_DRAFT)
|
||||
self.assertFalse(ContractRecord._base_manager.filter(pk=record.pk).exists())
|
||||
|
||||
def test_sales_contract_delete_is_physical(self):
|
||||
customer = Customer.objects.create(
|
||||
name='客户C',
|
||||
contact='王五',
|
||||
contact_phone='13800000003',
|
||||
)
|
||||
contract = Contract.objects.create(
|
||||
name='销售合同C',
|
||||
number='SC-003',
|
||||
amount=500,
|
||||
customer=customer,
|
||||
sign_date='2026-04-20',
|
||||
)
|
||||
|
||||
contract.delete()
|
||||
|
||||
self.assertFalse(Contract._base_manager.filter(pk=contract.pk).exists())
|
||||
|
||||
def test_sales_contract_status_auto_transitions_by_records(self):
|
||||
customer = Customer.objects.create(
|
||||
name='客户D',
|
||||
contact='赵六',
|
||||
contact_phone='13800000004',
|
||||
)
|
||||
contract = Contract.objects.create(
|
||||
name='销售合同D',
|
||||
number='SC-004',
|
||||
amount=1000,
|
||||
customer=customer,
|
||||
sign_date='2026-04-20',
|
||||
)
|
||||
|
||||
from apps.sam.models import ContractRecord
|
||||
|
||||
self.assertEqual(contract.status, Contract.STATUS_DRAFT)
|
||||
|
||||
first_record = ContractRecord.objects.create(
|
||||
contract=contract,
|
||||
record_date='2026-04-21',
|
||||
amount=Decimal('300.00'),
|
||||
stage_type=ContractRecord.STAGE_FIRST,
|
||||
)
|
||||
contract.refresh_from_db()
|
||||
self.assertEqual(contract.status, Contract.STATUS_ACTIVE)
|
||||
|
||||
ContractRecord.objects.create(
|
||||
contract=contract,
|
||||
record_date='2026-04-22',
|
||||
amount=Decimal('700.00'),
|
||||
stage_type=ContractRecord.STAGE_FINAL,
|
||||
)
|
||||
contract.refresh_from_db()
|
||||
self.assertEqual(contract.status, Contract.STATUS_DONE)
|
||||
|
||||
first_record.delete()
|
||||
contract.refresh_from_db()
|
||||
self.assertEqual(contract.status, Contract.STATUS_ACTIVE)
|
||||
|
||||
def test_sales_terminated_contract_forbids_record_changes(self):
|
||||
customer = Customer.objects.create(
|
||||
name='客户E',
|
||||
contact='孙七',
|
||||
contact_phone='13800000005',
|
||||
)
|
||||
contract = Contract.objects.create(
|
||||
name='销售合同E',
|
||||
number='SC-005',
|
||||
amount=1000,
|
||||
customer=customer,
|
||||
sign_date='2026-04-20',
|
||||
status=Contract.STATUS_TERMINATED,
|
||||
)
|
||||
|
||||
serializer = ContractRecordSerializer(data={
|
||||
'contract': contract.id,
|
||||
'record_date': '2026-04-21',
|
||||
'amount': '100.00',
|
||||
'stage_type': 10,
|
||||
'pay_method': 10,
|
||||
})
|
||||
self.assertFalse(serializer.is_valid())
|
||||
self.assertIn('合同已终止,不可操作到款流水', str(serializer.errors))
|
||||
|
||||
contract.status = Contract.STATUS_DRAFT
|
||||
contract.save(refresh_settlement=False)
|
||||
|
||||
from apps.sam.models import ContractRecord
|
||||
record = ContractRecord.objects.create(
|
||||
contract=contract,
|
||||
record_date='2026-04-20',
|
||||
amount=Decimal('100.00'),
|
||||
stage_type=ContractRecord.STAGE_OTHER,
|
||||
)
|
||||
contract.status = Contract.STATUS_TERMINATED
|
||||
contract.save(refresh_settlement=False)
|
||||
|
||||
with self.assertRaises(ParseError):
|
||||
from apps.sam.views import ContractRecordViewSet
|
||||
viewset = ContractRecordViewSet()
|
||||
viewset.request = None
|
||||
viewset.perform_destroy(record)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from apps.sam.views import (CustomerViewSet, ContractViewSet, OrderViewSet, OrderItemViewSet)
|
||||
from apps.sam.views import (CustomerViewSet, ContractViewSet, OrderViewSet, OrderItemViewSet, ContractRecordViewSet)
|
||||
|
||||
API_BASE_URL = 'api/sam/'
|
||||
HTML_BASE_URL = 'dhtml/sam/'
|
||||
|
|
@ -8,8 +8,9 @@ HTML_BASE_URL = 'dhtml/sam/'
|
|||
router = DefaultRouter()
|
||||
router.register('customer', CustomerViewSet, basename='customer')
|
||||
router.register('contract', ContractViewSet, basename='contract')
|
||||
router.register('contract_record', ContractRecordViewSet, basename='contract_record')
|
||||
router.register('order', OrderViewSet, basename='order')
|
||||
router.register('orderitem', OrderItemViewSet, basename='orderitem')
|
||||
urlpatterns = [
|
||||
path(API_BASE_URL, include(router.urls)),
|
||||
]
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from django.shortcuts import render
|
||||
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet
|
||||
from apps.sam.models import Customer, Contract, Order, OrderItem
|
||||
from apps.sam.serializers import CustomerSerializer, ContractSerializer, OrderSerializer, OrderItemSerializer
|
||||
from apps.sam.models import Customer, Contract, Order, OrderItem, ContractRecord
|
||||
from apps.sam.serializers import CustomerSerializer, ContractSerializer, OrderSerializer, OrderItemSerializer, ContractRecordSerializer
|
||||
from rest_framework.exceptions import ParseError
|
||||
from rest_framework.mixins import ListModelMixin, CreateModelMixin, DestroyModelMixin
|
||||
from apps.utils.mixins import BulkCreateModelMixin
|
||||
|
|
@ -46,6 +46,7 @@ class ContractViewSet(CustomModelViewSet):
|
|||
def perform_destroy(self, instance):
|
||||
if Order.objects.filter(contract=instance).exists():
|
||||
raise ParseError('该合同存在订单不可删除')
|
||||
instance.delete()
|
||||
|
||||
|
||||
class OrderViewSet(CustomModelViewSet):
|
||||
|
|
@ -106,3 +107,32 @@ class OrderItemViewSet(ListModelMixin, CreateModelMixin, DestroyModelMixin, Cust
|
|||
if instance.order.state != Order.ORDER_CREATE:
|
||||
raise ParseError('该订单状态下不可删除')
|
||||
return super().perform_destroy(instance)
|
||||
|
||||
|
||||
class ContractRecordViewSet(CustomModelViewSet):
|
||||
"""
|
||||
list: 销售合同到款流水
|
||||
|
||||
销售合同到款流水
|
||||
"""
|
||||
perms_map = {
|
||||
'get': '*',
|
||||
'post': 'contract.update',
|
||||
'put': 'contract.update',
|
||||
'patch': 'contract.update',
|
||||
'delete': 'contract.update',
|
||||
}
|
||||
queryset = ContractRecord.objects.all()
|
||||
serializer_class = ContractRecordSerializer
|
||||
search_fields = ['contract__number', 'contract__name', 'voucher_no', 'remark']
|
||||
select_related_fields = ['contract', 'contract__customer', 'create_by', 'update_by']
|
||||
filterset_fields = {
|
||||
'contract': ['exact'],
|
||||
'stage_type': ['exact', 'in'],
|
||||
'pay_method': ['exact', 'in'],
|
||||
}
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
if instance.contract.status == Contract.STATUS_TERMINATED:
|
||||
raise ParseError('合同已终止,不可删除到款流水')
|
||||
instance.delete()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from apps.wpm.models import BatchSt
|
||||
import logging
|
||||
from apps.wpm.models import Mlogb, Mlogbw, MlogbDefect
|
||||
from apps.wpm.models import Mlogb, Mlogbw, MlogbDefect, MlogUser
|
||||
from apps.mtm.models import Mgroup
|
||||
import decimal
|
||||
from django.db.models import Sum
|
||||
|
|
@ -27,7 +27,7 @@ def main(batch: str, mgroup_obj:Mgroup=None):
|
|||
mgroup_name = mgroup.name
|
||||
mlogb1_qs = Mlogb.objects.filter(mlog__submit_time__isnull=False,
|
||||
material_out__isnull=False, mlog__mgroup=mgroup,
|
||||
mlog__is_fix=False, batch=batch, need_inout=True)
|
||||
mlog__is_fix=False, batch=batch, need_inout=True).order_by("mlog__submit_time")
|
||||
if mlogb1_qs.exists():
|
||||
data[f"{mgroup_name}_日期"] = []
|
||||
data[f"{mgroup_name}_操作人"] = []
|
||||
|
|
@ -38,6 +38,7 @@ def main(batch: str, mgroup_obj:Mgroup=None):
|
|||
data[f"{mgroup_name}_count_ok_full"] = 0
|
||||
data[f"{mgroup_name}_count_pn_jgqbl"] = 0
|
||||
mlogb_q_ids = []
|
||||
cal_mlog = []
|
||||
for item in mlogb1_qs:
|
||||
# 找到对应的输入
|
||||
mlogb_from:Mlogb = item.mlogb_from
|
||||
|
|
@ -51,6 +52,13 @@ def main(batch: str, mgroup_obj:Mgroup=None):
|
|||
data[f"{mgroup_name}_count_pn_jgqbl"] += 0
|
||||
if item.mlog.handle_user:
|
||||
data[f"{mgroup_name}_操作人"].append(item.mlog.handle_user)
|
||||
# 子工序操作人
|
||||
if item.mlog not in cal_mlog:
|
||||
mlog_users_qs = MlogUser.objects.filter(mlog=item.mlog)
|
||||
if mlog_users_qs.exists():
|
||||
for mlog_user in mlog_users_qs:
|
||||
data[f"{mgroup_name}_{mlog_user.process.name}_操作人"] = mlog_user.handle_user.name
|
||||
cal_mlog.append(item.mlog)
|
||||
if item.mlog.handle_date:
|
||||
data[f"{mgroup_name}_日期"].append(item.mlog.handle_date)
|
||||
data[f"{mgroup_name}_count_real"] += item.count_real
|
||||
|
|
|
|||
Loading…
Reference in New Issue