Compare commits
No commits in common. "master" and "3.1.2026031316" have entirely different histories.
master
...
3.1.202603
|
|
@ -13,7 +13,6 @@ class Migration(migrations.Migration):
|
||||||
sql=[
|
sql=[
|
||||||
(
|
(
|
||||||
"""
|
"""
|
||||||
CREATE EXTENSION IF NOT EXISTS timescaledb;
|
|
||||||
CREATE TABLE public.enm_mplogx (
|
CREATE TABLE public.enm_mplogx (
|
||||||
"timex" timestamptz NOT NULL,
|
"timex" timestamptz NOT NULL,
|
||||||
"mpoint_id" text NOT NULL,
|
"mpoint_id" text NOT NULL,
|
||||||
|
|
|
||||||
|
|
@ -206,7 +206,6 @@ class EnStat2Serializer(CustomModelSerializer):
|
||||||
class ReCalSerializer(serializers.Serializer):
|
class ReCalSerializer(serializers.Serializer):
|
||||||
start_time = serializers.DateTimeField(label="开始时间")
|
start_time = serializers.DateTimeField(label="开始时间")
|
||||||
end_time = serializers.DateTimeField(label="结束时间")
|
end_time = serializers.DateTimeField(label="结束时间")
|
||||||
mpoint_stat = serializers.BooleanField(label="从MpointStat开始计算", required=False, default=False)
|
|
||||||
|
|
||||||
|
|
||||||
class MpointStatCorrectSerializer(CustomModelSerializer):
|
class MpointStatCorrectSerializer(CustomModelSerializer):
|
||||||
|
|
|
||||||
|
|
@ -114,185 +114,22 @@ def db_ins_mplogx():
|
||||||
|
|
||||||
|
|
||||||
@shared_task(base=CustomTask)
|
@shared_task(base=CustomTask)
|
||||||
def cal_mpointstats_duration(start_time: str, end_time: str, m_code_list=[], cal_attrs=[], mpoint_stat=False):
|
def cal_mpointstats_duration(start_time: str, end_time: str, m_code_list=[], cal_attrs=[]):
|
||||||
"""
|
"""
|
||||||
重跑某一段时间的任务
|
重跑某一段时间的任务
|
||||||
mpoint_stat: True时从已有的MpointStat hour记录开始重算(跳过MpLogx),只重算day/month/year/sflog聚合,速度更快
|
|
||||||
"""
|
"""
|
||||||
mytz = tz.gettz(settings.TIME_ZONE)
|
mytz = tz.gettz(settings.TIME_ZONE)
|
||||||
start_time = datetime.datetime.strptime(start_time, "%Y-%m-%d %H:%M:%S")
|
start_time = datetime.datetime.strptime(start_time, "%Y-%m-%d %H:%M:%S")
|
||||||
start_time = start_time.replace(tzinfo=mytz)
|
start_time.replace(tzinfo=mytz)
|
||||||
end_time = datetime.datetime.strptime(end_time, "%Y-%m-%d %H:%M:%S")
|
end_time = datetime.datetime.strptime(end_time, "%Y-%m-%d %H:%M:%S")
|
||||||
end_time = end_time.replace(tzinfo=mytz)
|
start_time.replace(tzinfo=mytz)
|
||||||
|
|
||||||
if mpoint_stat:
|
|
||||||
_recal_from_mpointstat(start_time, end_time, m_code_list, cal_attrs)
|
|
||||||
else:
|
|
||||||
current_time = start_time
|
|
||||||
while current_time <= end_time:
|
|
||||||
year, month, day, hour = current_time.year, current_time.month, current_time.day, current_time.hour
|
|
||||||
cal_mpointstats(0, year, month, day, hour, m_code_list, cal_attrs)
|
|
||||||
myLogger.info("now: {} cal_mpointstats completed: {}".format(datetime.datetime.now(), current_time))
|
|
||||||
current_time += datetime.timedelta(hours=1)
|
|
||||||
|
|
||||||
|
|
||||||
def _recal_from_mpointstat(start_time, end_time, m_code_list=[], cal_attrs=[]):
|
|
||||||
"""
|
|
||||||
从已有的MpointStat hour记录开始,批量重算day/month/year/sflog聚合
|
|
||||||
不读取MpLogx,速度远快于完整重算
|
|
||||||
"""
|
|
||||||
# 确定需要重算的测点
|
|
||||||
if m_code_list:
|
|
||||||
mpoints = list(Mpoint.objects.filter(code__in=m_code_list, enabled=True))
|
|
||||||
# 也要包含依赖这些测点的计算测点
|
|
||||||
related = Mpoint.objects.none()
|
|
||||||
for code in m_code_list:
|
|
||||||
related = related | Mpoint.objects.filter(
|
|
||||||
type=Mpoint.MT_COMPUTE, enabled=True, material__isnull=False,
|
|
||||||
formula__contains='{' + code + '}'
|
|
||||||
)
|
|
||||||
mpoints.extend(list(related.distinct()))
|
|
||||||
mpoints = list({mp.id: mp for mp in mpoints}.values()) # 去重
|
|
||||||
else:
|
|
||||||
mpoints = list(Mpoint.objects.filter(enabled=True, material__isnull=False))
|
|
||||||
|
|
||||||
mpoint_ids = [mp.id for mp in mpoints]
|
|
||||||
|
|
||||||
# 收集需要重算的所有 (year, month, day) 和 (year, month)
|
|
||||||
days_set = set()
|
|
||||||
months_set = set()
|
|
||||||
years_set = set()
|
|
||||||
current_time = start_time
|
|
||||||
while current_time <= end_time:
|
|
||||||
days_set.add((current_time.year, current_time.month, current_time.day))
|
|
||||||
months_set.add((current_time.year, current_time.month))
|
|
||||||
years_set.add(current_time.year)
|
|
||||||
current_time += datetime.timedelta(hours=1)
|
|
||||||
|
|
||||||
myLogger.info(f"_recal_from_mpointstat: {len(mpoints)} mpoints, {len(days_set)} days")
|
|
||||||
|
|
||||||
# 批量重算 day = sum(hour)
|
|
||||||
for year, month, day in days_set:
|
|
||||||
# 一次查询拿到所有测点该天的hour汇总
|
|
||||||
hour_sums = dict(
|
|
||||||
MpointStat.objects.filter(
|
|
||||||
type="hour", mpoint_id__in=mpoint_ids, year=year, month=month, day=day
|
|
||||||
).values('mpoint_id').annotate(total=Sum('val')).values_list('mpoint_id', 'total')
|
|
||||||
)
|
|
||||||
for mp in mpoints:
|
|
||||||
val = hour_sums.get(mp.id)
|
|
||||||
if val is None:
|
|
||||||
continue
|
|
||||||
params_day = {"type": "day", "mpoint": mp, "year": year, "month": month, "day": day}
|
|
||||||
ms_day, _ = MpointStat.safe_get_or_create(**params_day, defaults=params_day)
|
|
||||||
if ms_day.val_correct is None:
|
|
||||||
ms_day.val = val
|
|
||||||
ms_day.val_origin = val
|
|
||||||
ms_day.save()
|
|
||||||
|
|
||||||
# 批量重算 month = sum(day)
|
|
||||||
for year, month in months_set:
|
|
||||||
day_sums = dict(
|
|
||||||
MpointStat.objects.filter(
|
|
||||||
type="day", mpoint_id__in=mpoint_ids, year=year, month=month
|
|
||||||
).values('mpoint_id').annotate(total=Sum('val')).values_list('mpoint_id', 'total')
|
|
||||||
)
|
|
||||||
for mp in mpoints:
|
|
||||||
val = day_sums.get(mp.id)
|
|
||||||
if val is None:
|
|
||||||
continue
|
|
||||||
params_month = {"type": "month", "mpoint": mp, "year": year, "month": month}
|
|
||||||
ms_month, _ = MpointStat.safe_get_or_create(**params_month, defaults=params_month)
|
|
||||||
if ms_month.val_correct is None:
|
|
||||||
ms_month.val = val
|
|
||||||
ms_month.val_origin = val
|
|
||||||
ms_month.save()
|
|
||||||
|
|
||||||
# 批量重算 year = sum(month)
|
|
||||||
for year in years_set:
|
|
||||||
month_sums = dict(
|
|
||||||
MpointStat.objects.filter(
|
|
||||||
type="month", mpoint_id__in=mpoint_ids, year=year
|
|
||||||
).values('mpoint_id').annotate(total=Sum('val')).values_list('mpoint_id', 'total')
|
|
||||||
)
|
|
||||||
for mp in mpoints:
|
|
||||||
val = month_sums.get(mp.id)
|
|
||||||
if val is None:
|
|
||||||
continue
|
|
||||||
params_year = {"type": "year", "mpoint": mp, "year": year}
|
|
||||||
ms_year, _ = MpointStat.safe_get_or_create(**params_year, defaults=params_year)
|
|
||||||
if ms_year.val_correct is None:
|
|
||||||
ms_year.val = val
|
|
||||||
ms_year.val_origin = val
|
|
||||||
ms_year.save()
|
|
||||||
|
|
||||||
# 重算 sflog 相关统计 (hour_s -> sflog)
|
|
||||||
mytz = tz.gettz(settings.TIME_ZONE)
|
|
||||||
mgroups = Mgroup.objects.filter(need_enm=True).order_by("sort")
|
|
||||||
current_time = start_time
|
|
||||||
sflog_cache = {}
|
|
||||||
while current_time <= end_time:
|
|
||||||
year, month, day, hour = current_time.year, current_time.month, current_time.day, current_time.hour
|
|
||||||
dt = datetime.datetime(year=year, month=month, day=day, hour=hour, minute=0, second=0, tzinfo=mytz)
|
|
||||||
for mgroup in mgroups:
|
|
||||||
cache_key = (mgroup.id, year, month, day, hour)
|
|
||||||
if cache_key not in sflog_cache:
|
|
||||||
sflog = get_sflog(mgroup, dt)
|
|
||||||
sflog_cache[cache_key] = sflog
|
|
||||||
sflog = sflog_cache[cache_key]
|
|
||||||
if sflog is None:
|
|
||||||
continue
|
|
||||||
year_s, month_s, day_s = sflog.get_ymd
|
|
||||||
# 获取该mgroup下的测点
|
|
||||||
group_mpoints = [mp for mp in mpoints if mp.mgroup_id == mgroup.id]
|
|
||||||
for mp in group_mpoints:
|
|
||||||
# 找到对应的hour stat
|
|
||||||
ms_hour = MpointStat.objects.filter(
|
|
||||||
type="hour", mpoint=mp, year=year, month=month, day=day, hour=hour
|
|
||||||
).first()
|
|
||||||
if ms_hour is None:
|
|
||||||
continue
|
|
||||||
params_hour_s = {
|
|
||||||
"type": "hour_s", "mpoint": mp, "sflog": sflog, "mgroup": mgroup,
|
|
||||||
"year": year, "month": month, "day": day,
|
|
||||||
"year_s": year_s, "month_s": month_s, "day_s": day_s, "hour": hour,
|
|
||||||
}
|
|
||||||
ms_hour_s, _ = MpointStat.safe_get_or_create(**params_hour_s, defaults=params_hour_s)
|
|
||||||
ms_hour_s.val = ms_hour_s.val_correct if ms_hour_s.val_correct is not None else ms_hour.val
|
|
||||||
ms_hour_s.save()
|
|
||||||
|
|
||||||
# 重算 sflog 聚合
|
|
||||||
for mp in group_mpoints:
|
|
||||||
sflog_key = (mp.id, sflog.id, year_s, month_s, day_s)
|
|
||||||
if sflog_key in sflog_cache:
|
|
||||||
continue # 同一sflog只算一次
|
|
||||||
sflog_cache[sflog_key] = True
|
|
||||||
params_sflog_s = {
|
|
||||||
"type": "sflog", "mpoint": mp, "sflog": sflog,
|
|
||||||
"year_s": year_s, "month_s": month_s, "day_s": day_s, "mgroup": mgroup,
|
|
||||||
}
|
|
||||||
ms_sflog_s, _ = MpointStat.safe_get_or_create(**params_sflog_s, defaults=params_sflog_s)
|
|
||||||
if ms_sflog_s.val_correct is None:
|
|
||||||
sum_val = MpointStat.objects.filter(
|
|
||||||
type="hour_s", mpoint=mp, year_s=year_s, month_s=month_s, day_s=day_s, sflog=sflog
|
|
||||||
).aggregate(sum=Sum("val"))
|
|
||||||
ms_sflog_s.val = sum_val['sum'] if sum_val['sum'] is not None else 0
|
|
||||||
ms_sflog_s.val_origin = ms_sflog_s.val
|
|
||||||
ms_sflog_s.save()
|
|
||||||
|
|
||||||
myLogger.info("now: {} _recal_from_mpointstat completed: {}".format(datetime.datetime.now(), current_time))
|
|
||||||
current_time += datetime.timedelta(hours=1)
|
|
||||||
|
|
||||||
# 重算 enstat
|
|
||||||
current_time = start_time
|
current_time = start_time
|
||||||
while current_time <= end_time:
|
while current_time <= end_time:
|
||||||
year, month, day, hour = current_time.year, current_time.month, current_time.day, current_time.hour
|
year, month, day, hour = current_time.year, current_time.month, current_time.day, current_time.hour
|
||||||
for mgroup in mgroups:
|
cal_mpointstats(0, year, month, day, hour, m_code_list, cal_attrs)
|
||||||
cal_enstat("hour_s", None, mgroup.id, year, month, day, hour, None, None, None, True, cal_attrs)
|
myLogger.info("now: {} cal_mpointstats completed: {}".format(datetime.datetime.now(), current_time))
|
||||||
current_time += datetime.timedelta(hours=1)
|
current_time += datetime.timedelta(hours=1)
|
||||||
|
|
||||||
myLogger.info("_recal_from_mpointstat completed all")
|
|
||||||
|
|
||||||
|
|
||||||
@shared_task(base=CustomTask)
|
@shared_task(base=CustomTask)
|
||||||
def correct_bill_date():
|
def correct_bill_date():
|
||||||
|
|
|
||||||
|
|
@ -378,14 +378,10 @@ class MpointStatViewSet(BulkCreateModelMixin, BulkDestroyModelMixin, CustomListM
|
||||||
|
|
||||||
重新运行某段时间的enm计算
|
重新运行某段时间的enm计算
|
||||||
"""
|
"""
|
||||||
sr = ReCalSerializer(data=request.data)
|
data = request.data
|
||||||
|
sr = ReCalSerializer(data=data)
|
||||||
sr.is_valid(raise_exception=True)
|
sr.is_valid(raise_exception=True)
|
||||||
data = sr.validated_data
|
task = cal_mpointstats_duration.delay(data["start_time"], data["end_time"])
|
||||||
task = cal_mpointstats_duration.delay(
|
|
||||||
data["start_time"].strftime("%Y-%m-%d %H:%M:%S"),
|
|
||||||
data["end_time"].strftime("%Y-%m-%d %H:%M:%S"),
|
|
||||||
mpoint_stat=data.get("mpoint_stat", False)
|
|
||||||
)
|
|
||||||
return Response({"task_id": task.task_id})
|
return Response({"task_id": task.task_id})
|
||||||
|
|
||||||
@action(methods=["get"], detail=False, perms_map={"get": "*"})
|
@action(methods=["get"], detail=False, perms_map={"get": "*"})
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ class Migration(migrations.Migration):
|
||||||
sql=[
|
sql=[
|
||||||
(
|
(
|
||||||
"""
|
"""
|
||||||
CREATE EXTENSION IF NOT EXISTS timescaledb;
|
|
||||||
CREATE TABLE public.enp_envdata (
|
CREATE TABLE public.enp_envdata (
|
||||||
"timex" timestamptz NOT NULL,
|
"timex" timestamptz NOT NULL,
|
||||||
"equipment_id" text NOT NULL,
|
"equipment_id" text NOT NULL,
|
||||||
|
|
|
||||||
|
|
@ -388,8 +388,6 @@ class TransferSerializer(CustomModelSerializer):
|
||||||
belong_dept_name = serializers.CharField(source='employee.belong_dept.name', read_only=True)
|
belong_dept_name = serializers.CharField(source='employee.belong_dept.name', read_only=True)
|
||||||
new_post_name = serializers.CharField(source="new_post.name", read_only=True)
|
new_post_name = serializers.CharField(source="new_post.name", read_only=True)
|
||||||
original_post_name = serializers.CharField(source="original_post.name", read_only=True)
|
original_post_name = serializers.CharField(source="original_post.name", read_only=True)
|
||||||
new_dept_name = serializers.CharField(source="new_dept.name", read_only=True)
|
|
||||||
original_dept_name = serializers.CharField(source="original_dept.name", read_only=True)
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = EmployeeTransfer
|
model = EmployeeTransfer
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ class HrmService:
|
||||||
Returns:
|
Returns:
|
||||||
_type_: _description_
|
_type_: _description_
|
||||||
"""
|
"""
|
||||||
if not getattr(settings, 'DAHUA_ENABLED', False): # 如果大华没启用, 直接返回
|
if not settings.DAHUA_ENABLED: # 如果大华没启用, 直接返回
|
||||||
return
|
return
|
||||||
dh_id = ep.third_info.get('dh_id', None)
|
dh_id = ep.third_info.get('dh_id', None)
|
||||||
dh_photo = ep.third_info.get('dh_photo', None)
|
dh_photo = ep.third_info.get('dh_photo', None)
|
||||||
|
|
|
||||||
|
|
@ -387,39 +387,20 @@ class EmployeeViewSet(CustomModelViewSet):
|
||||||
if not re.match(r'^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$', data['id_number']):
|
if not re.match(r'^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$', data['id_number']):
|
||||||
raise ParseError(f'第{row_num}行,身份证号格式不正确')
|
raise ParseError(f'第{row_num}行,身份证号格式不正确')
|
||||||
|
|
||||||
# 查找或创建/补全
|
# 查找或更新
|
||||||
try:
|
try:
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
# 优先按身份证号匹配,匹配不到再按姓名匹配
|
obj, created = Employee.objects.update_or_create(
|
||||||
existing = None
|
id_number=id_number,
|
||||||
if id_number:
|
name=name,
|
||||||
existing = Employee.objects.filter(id_number=id_number).first()
|
defaults=data
|
||||||
if not existing and name:
|
)
|
||||||
existing = Employee.objects.filter(name=name, id_number__isnull=True).first() or \
|
|
||||||
Employee.objects.filter(name=name, id_number='').first()
|
|
||||||
if existing:
|
|
||||||
# 只用 Excel 非空值填补数据库中为空的字段
|
|
||||||
updated_fields = []
|
|
||||||
for field_name, value in data.items():
|
|
||||||
if value in [None, '']:
|
|
||||||
continue
|
|
||||||
current_value = getattr(existing, field_name, None)
|
|
||||||
if current_value in [None, '']:
|
|
||||||
setattr(existing, field_name, value)
|
|
||||||
updated_fields.append(field_name)
|
|
||||||
if updated_fields:
|
|
||||||
existing.save(update_fields=updated_fields + ['update_time'])
|
|
||||||
myLogger.info(f"✅ 第{row_num}行补全成功:{name},更新字段:{updated_fields}")
|
|
||||||
else:
|
|
||||||
myLogger.info(f"⏭️ 第{row_num}行无需补全:{name}")
|
|
||||||
created = False
|
|
||||||
else:
|
|
||||||
Employee.objects.create(id_number=id_number, name=name, **data)
|
|
||||||
created = True
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise
|
raise
|
||||||
if created:
|
if created:
|
||||||
myLogger.info(f"✅ 第{row_num}行新增成功:{name}")
|
myLogger.info(f"✅ 第{row_num}行新增成功:{name}")
|
||||||
|
else:
|
||||||
|
myLogger.info(f"✅ 第{row_num}行更新成功:{name}")
|
||||||
|
|
||||||
success += 1
|
success += 1
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
# Register your models here.
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class ChatConfig(AppConfig):
|
|
||||||
default_auto_field = 'django.db.models.BigAutoField'
|
|
||||||
name = 'apps.ichat'
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
# Generated by Django 3.2.12 on 2025-05-21 05:59
|
|
||||||
|
|
||||||
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 = [
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Conversation',
|
|
||||||
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='删除标记')),
|
|
||||||
('title', models.CharField(default='新对话', max_length=200, verbose_name='对话标题')),
|
|
||||||
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='conversation_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='conversation_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Message',
|
|
||||||
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='删除标记')),
|
|
||||||
('content', models.TextField(verbose_name='消息内容')),
|
|
||||||
('role', models.CharField(default='user', help_text='system/user', max_length=10, verbose_name='角色')),
|
|
||||||
('conversation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='ichat.conversation', verbose_name='对话')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
from django.db import models
|
|
||||||
from apps.system.models import CommonADModel, BaseModel
|
|
||||||
|
|
||||||
# Create your models here.
|
|
||||||
class Conversation(CommonADModel):
|
|
||||||
"""
|
|
||||||
TN: 对话
|
|
||||||
"""
|
|
||||||
title = models.CharField(max_length=200, default='新对话',verbose_name='对话标题')
|
|
||||||
|
|
||||||
class Message(BaseModel):
|
|
||||||
"""
|
|
||||||
TN: 消息
|
|
||||||
"""
|
|
||||||
conversation = models.ForeignKey(Conversation, on_delete=models.CASCADE, related_name='messages', verbose_name='对话')
|
|
||||||
content = models.TextField(verbose_name='消息内容')
|
|
||||||
role = models.CharField("角色", max_length=10, default='user', help_text="system/user")
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
# 角色
|
|
||||||
你是一位数据分析专家和前端程序员,具备深厚的专业知识和丰富的实践经验。你能够精准理解用户的文本描述, 并形成报告。
|
|
||||||
# 技能
|
|
||||||
1. 仔细分析用户提供的JSON格式数据,分析用户需求。
|
|
||||||
2. 依据得到的需求, 分别获取JSON数据中的关键信息。
|
|
||||||
3. 根据2中的关键信息最优化选择表格/饼图/柱状图/折线图等格式绘制报告。
|
|
||||||
# 回答要求
|
|
||||||
1. 仅生成完整的HTML代码,所有功能都需要实现,支持响应式,不要输出任何解释或说明。
|
|
||||||
2. 代码中如需要Echarts等js库,请直接使用中国大陆的CDN链接例如bootcdn的链接。
|
|
||||||
3. 标题为 数据分析报告。
|
|
||||||
3. 在开始部分,请以表格形式简略展示获取的JSON数据。
|
|
||||||
4. 之后选择最合适的图表方式生成相应的图。
|
|
||||||
5. 在最后提供可下载该报告的完整PDF的按钮和功能。
|
|
||||||
6. 在最后提供可下载含有JSON数据的EXCEL文件的按钮和功能。
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
# 角色
|
|
||||||
你是一位资深的Postgresql数据库SQL专家,具备深厚的专业知识和丰富的实践经验。你能够精准理解用户的文本描述,并生成准确可执行的SQL语句。
|
|
||||||
# 技能
|
|
||||||
1. 仔细分析用户提供的文本描述,明确用户需求。
|
|
||||||
2. 根据对用户需求的理解,生成符合Postgresql数据库语法的准确可执行的SQL语句。
|
|
||||||
# 回答要求
|
|
||||||
1. 如果用户的询问未以 查询 开头,请直接回复 "请以 查询 开头,重新描述你的需求"。
|
|
||||||
2. 生成的SQL语句必须符合Postgresql数据库的语法规范。
|
|
||||||
3. 不要使用 Markerdown 和 SQL 语法格式输出,禁止添加语法标准、备注、说明等信息。
|
|
||||||
4. 直接输出符合Postgresql标准的SQL语句,用txt纯文本格式展示即可。
|
|
||||||
5. 如果无法生成符合要求的SQL语句,请直接回复 "无法生成"。
|
|
||||||
# 示例
|
|
||||||
1. 问:查询 外协白片抛 工段在2025年6月1日到2025年6月15日之间的生产合格数以及合格率等
|
|
||||||
答:select
|
|
||||||
sum(mlog.count_use) as 领用数,
|
|
||||||
sum(mlog.count_real) as 生产数,
|
|
||||||
sum(mlog.count_ok) as 合格数,
|
|
||||||
sum(mlog.count_notok) as 不合格数,
|
|
||||||
CAST ( SUM ( mlog.count_ok ) AS FLOAT ) / NULLIF ( SUM ( mlog.count_real ), 0 ) * 100 AS 合格率
|
|
||||||
from wpm_mlog mlog
|
|
||||||
left join mtm_mgroup mgroup on mgroup.id = mlog.mgroup_id
|
|
||||||
where mlog.submit_time is not null
|
|
||||||
and mgroup.name = '外协白片抛'
|
|
||||||
and mlog.handle_date >= '2025-06-01'
|
|
||||||
and mlog.handle_date <= '2025-06-15'
|
|
||||||
2. 问:查询 黑化 工段在2025年6月的生产合格数以及合格率等
|
|
||||||
答: select
|
|
||||||
sum(mlog.count_use) as 领用数,
|
|
||||||
sum(mlog.count_real) as 生产数,
|
|
||||||
sum(mlog.count_ok) as 合格数,
|
|
||||||
sum(mlog.count_notok) as 不合格数,
|
|
||||||
CAST ( SUM ( mlog.count_ok ) AS FLOAT ) / NULLIF ( SUM ( mlog.count_real ), 0 ) * 100 AS 合格率
|
|
||||||
from wpm_mlog mlog
|
|
||||||
left join mtm_mgroup mgroup on mgroup.id = mlog.mgroup_id
|
|
||||||
where mlog.submit_time is not null
|
|
||||||
and mgroup.name = '黑化'
|
|
||||||
and mlog.handle_date >= '2025-06-01'
|
|
||||||
and mlog.handle_date <= '2025-06-30'
|
|
||||||
3. 问:查询 各工段 在2025年6月的生产合格数以及合格率等
|
|
||||||
答: select
|
|
||||||
mgroup.name as 工段,
|
|
||||||
sum(mlog.count_use) as 领用数,
|
|
||||||
sum(mlog.count_real) as 生产数,
|
|
||||||
sum(mlog.count_ok) as 合格数,
|
|
||||||
sum(mlog.count_notok) as 不合格数,
|
|
||||||
CAST ( SUM ( mlog.count_ok ) AS FLOAT ) / NULLIF ( SUM ( mlog.count_real ), 0 ) * 100 AS 合格率
|
|
||||||
from wpm_mlog mlog
|
|
||||||
left join mtm_mgroup mgroup on mgroup.id = mlog.mgroup_id
|
|
||||||
where mlog.submit_time is not null
|
|
||||||
and mlog.handle_date >= '2025-06-01'
|
|
||||||
and mlog.handle_date <= '2025-06-30'
|
|
||||||
group by mgroup.id
|
|
||||||
order by mgroup.sort
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
import json
|
|
||||||
from .models import Message
|
|
||||||
from django.http import StreamingHttpResponse
|
|
||||||
|
|
||||||
def stream_generator(stream_response: bytes, conversation_id: str):
|
|
||||||
full_content = ''
|
|
||||||
for chunk in stream_response.iter_content(chunk_size=1024):
|
|
||||||
if chunk:
|
|
||||||
full_content += chunk.decode('utf-8')
|
|
||||||
try:
|
|
||||||
data = json.loads(full_content)
|
|
||||||
content = data.get("choices", [{}])[0].get("delta", {}).get("content", "")
|
|
||||||
Message.objects.create(
|
|
||||||
conversation_id=conversation_id,
|
|
||||||
content=content
|
|
||||||
)
|
|
||||||
yield f" data:{content}\n\n"
|
|
||||||
full_content = ''
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
continue
|
|
||||||
return StreamingHttpResponse(stream_generator(stream_response, conversation_id), content_type='text/event-stream')
|
|
||||||
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
from rest_framework import serializers
|
|
||||||
from .models import Conversation, Message
|
|
||||||
from apps.utils.constants import EXCLUDE_FIELDS
|
|
||||||
|
|
||||||
|
|
||||||
class MessageSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = Message
|
|
||||||
fields = ['id', 'conversation', 'content', 'role']
|
|
||||||
read_only_fields = EXCLUDE_FIELDS
|
|
||||||
|
|
||||||
|
|
||||||
class ConversationSerializer(serializers.ModelSerializer):
|
|
||||||
messages = MessageSerializer(many=True, read_only=True)
|
|
||||||
class Meta:
|
|
||||||
model = Conversation
|
|
||||||
fields = ['id', 'title', 'messages']
|
|
||||||
read_only_fields = EXCLUDE_FIELDS
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
|
|
||||||
from django.urls import path, include
|
|
||||||
from rest_framework.routers import DefaultRouter
|
|
||||||
from apps.ichat.views import QueryLLMviewSet, ConversationViewSet
|
|
||||||
from apps.ichat.views2 import WorkChain
|
|
||||||
|
|
||||||
API_BASE_URL = 'api/ichat/'
|
|
||||||
|
|
||||||
router = DefaultRouter()
|
|
||||||
|
|
||||||
router.register('conversation', ConversationViewSet, basename='conversation')
|
|
||||||
router.register('message', QueryLLMviewSet, basename='message')
|
|
||||||
urlpatterns = [
|
|
||||||
path(API_BASE_URL, include(router.urls)),
|
|
||||||
path(API_BASE_URL + 'workchain/ask/', WorkChain.as_view(), name='workchain')
|
|
||||||
]
|
|
||||||
|
|
@ -1,195 +0,0 @@
|
||||||
import re
|
|
||||||
import psycopg2
|
|
||||||
import threading
|
|
||||||
from django.db import transaction
|
|
||||||
from .models import Message
|
|
||||||
|
|
||||||
# 数据库连接
|
|
||||||
def connect_db():
|
|
||||||
from server.conf import DATABASES
|
|
||||||
db_conf = DATABASES['default']
|
|
||||||
conn = psycopg2.connect(
|
|
||||||
host=db_conf['HOST'],
|
|
||||||
port=db_conf['PORT'],
|
|
||||||
user=db_conf['USER'],
|
|
||||||
password=db_conf['PASSWORD'],
|
|
||||||
database=db_conf['NAME']
|
|
||||||
)
|
|
||||||
return conn
|
|
||||||
|
|
||||||
def extract_sql_code(text):
|
|
||||||
# 优先尝试 ```sql 包裹的语句
|
|
||||||
match = re.search(r"```sql\s*(.+?)```", text, re.DOTALL | re.IGNORECASE)
|
|
||||||
if match:
|
|
||||||
return match.group(1).strip()
|
|
||||||
|
|
||||||
# fallback: 寻找首个 select 语句
|
|
||||||
match = re.search(r"(SELECT\s.+?;)", text, re.IGNORECASE | re.DOTALL)
|
|
||||||
if match:
|
|
||||||
return match.group(1).strip()
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
# def get_schema_text(conn, table_names:list):
|
|
||||||
# cur = conn.cursor()
|
|
||||||
# query = """
|
|
||||||
# SELECT
|
|
||||||
# table_name, column_name, data_type
|
|
||||||
# FROM
|
|
||||||
# information_schema.columns
|
|
||||||
# WHERE
|
|
||||||
# table_schema = 'public'
|
|
||||||
# and table_name in %s;
|
|
||||||
# """
|
|
||||||
# cur.execute(query, (tuple(table_names), ))
|
|
||||||
|
|
||||||
# schema = {}
|
|
||||||
# for table_name, column_name, data_type in cur.fetchall():
|
|
||||||
# if table_name not in schema:
|
|
||||||
# schema[table_name] = []
|
|
||||||
# schema[table_name].append(f"{column_name} ({data_type})")
|
|
||||||
# cur.close()
|
|
||||||
# schema_text = ""
|
|
||||||
# for table_name, columns in schema.items():
|
|
||||||
# schema_text += f"表{table_name} 包含列:{', '.join(columns)}\n"
|
|
||||||
# return schema_text
|
|
||||||
|
|
||||||
def get_schema_text(conn, table_names: list):
|
|
||||||
cur = conn.cursor()
|
|
||||||
query = """
|
|
||||||
SELECT
|
|
||||||
c.relname AS table_name,
|
|
||||||
a.attname AS column_name,
|
|
||||||
pg_catalog.format_type(a.atttypid, a.atttypmod) AS data_type,
|
|
||||||
d.description AS column_comment
|
|
||||||
FROM
|
|
||||||
pg_class c
|
|
||||||
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
||||||
JOIN pg_attribute a ON a.attrelid = c.oid
|
|
||||||
LEFT JOIN pg_description d ON d.objoid = a.attrelid AND d.objsubid = a.attnum
|
|
||||||
WHERE
|
|
||||||
n.nspname = 'public'
|
|
||||||
AND c.relname = ANY(%s)
|
|
||||||
AND a.attnum > 0
|
|
||||||
AND NOT a.attisdropped
|
|
||||||
ORDER BY
|
|
||||||
c.relname, a.attnum;
|
|
||||||
"""
|
|
||||||
cur.execute(query, (table_names,))
|
|
||||||
|
|
||||||
schema = {}
|
|
||||||
for table_name, column_name, data_type, comment in cur.fetchall():
|
|
||||||
if comment and "备注" in comment:
|
|
||||||
comment = comment.split("备注")[0].strip()
|
|
||||||
schema.setdefault(table_name, []).append(
|
|
||||||
f"{column_name}-{comment}"
|
|
||||||
)
|
|
||||||
|
|
||||||
cur.close()
|
|
||||||
|
|
||||||
return [
|
|
||||||
{"table": table, "text": f"表 {table} 包含列:\n" + "\n".join(columns)}
|
|
||||||
for table, columns in schema.items()
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# def get_schema_text(conn, table_names: list):
|
|
||||||
# cur = conn.cursor()
|
|
||||||
|
|
||||||
# # 获取字段、类型、注释
|
|
||||||
# column_query = """
|
|
||||||
# SELECT
|
|
||||||
# c.relname AS table_name,
|
|
||||||
# a.attname AS column_name,
|
|
||||||
# pg_catalog.format_type(a.atttypid, a.atttypmod) AS data_type,
|
|
||||||
# d.description AS column_comment
|
|
||||||
# FROM
|
|
||||||
# pg_class c
|
|
||||||
# JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
||||||
# JOIN pg_attribute a ON a.attrelid = c.oid
|
|
||||||
# LEFT JOIN pg_description d ON d.objoid = a.attrelid AND d.objsubid = a.attnum
|
|
||||||
# WHERE
|
|
||||||
# n.nspname = 'public'
|
|
||||||
# AND c.relname = ANY(%s)
|
|
||||||
# AND a.attnum > 0
|
|
||||||
# AND NOT a.attisdropped
|
|
||||||
# ORDER BY
|
|
||||||
# c.relname, a.attnum;
|
|
||||||
# """
|
|
||||||
|
|
||||||
# # 获取外键信息
|
|
||||||
# fk_query = """
|
|
||||||
# SELECT
|
|
||||||
# conrelid::regclass::text AS table_name,
|
|
||||||
# a.attname AS column_name,
|
|
||||||
# confrelid::regclass::text AS foreign_table,
|
|
||||||
# af.attname AS foreign_column
|
|
||||||
# FROM
|
|
||||||
# pg_constraint
|
|
||||||
# JOIN pg_class ON conrelid = pg_class.oid
|
|
||||||
# JOIN pg_namespace n ON pg_class.relnamespace = n.oid
|
|
||||||
# JOIN pg_attribute a ON a.attrelid = conrelid AND a.attnum = ANY(conkey)
|
|
||||||
# JOIN pg_attribute af ON af.attrelid = confrelid AND af.attnum = ANY(confkey)
|
|
||||||
# WHERE
|
|
||||||
# contype = 'f'
|
|
||||||
# AND n.nspname = 'public'
|
|
||||||
# AND conrelid::regclass::text = ANY(%s);
|
|
||||||
# """
|
|
||||||
|
|
||||||
# cur.execute(column_query, (table_names,))
|
|
||||||
# columns = cur.fetchall()
|
|
||||||
|
|
||||||
# cur.execute(fk_query, (table_names,))
|
|
||||||
# fks = cur.fetchall()
|
|
||||||
|
|
||||||
# # 构建外键字典
|
|
||||||
# fk_map = {} # {(table, column): "foreign_table(foreign_column)"}
|
|
||||||
# for table, column, f_table, f_column in fks:
|
|
||||||
# fk_map[(table, column)] = f"{f_table}({f_column})"
|
|
||||||
|
|
||||||
# # 组织输出结构
|
|
||||||
# schema = {}
|
|
||||||
# for table, column, dtype, comment in columns:
|
|
||||||
# fk_note = f" -> {fk_map[(table, column)]}" if (table, column) in fk_map else ""
|
|
||||||
# comment_note = f" -- {comment}" if comment else ""
|
|
||||||
# schema.setdefault(table, []).append(f"{column} ({dtype}{fk_note}{comment_note})")
|
|
||||||
|
|
||||||
# cur.close()
|
|
||||||
|
|
||||||
# # 生成文本
|
|
||||||
# schema_text = ""
|
|
||||||
# for table, cols in schema.items():
|
|
||||||
# schema_text += f"表 {table} 包含列:\n - " + "\n - ".join(cols) + "\n"
|
|
||||||
# return schema_text
|
|
||||||
|
|
||||||
def is_safe_sql(sql:str) -> bool:
|
|
||||||
sql = sql.strip().lower()
|
|
||||||
return sql.startswith("select") or sql.startswith("show") and not re.search(r"delete|update|insert|drop|create|alter", sql)
|
|
||||||
|
|
||||||
def execute_sql(conn, sql_query):
|
|
||||||
cur = conn.cursor()
|
|
||||||
cur.execute(sql_query)
|
|
||||||
try:
|
|
||||||
rows = cur.fetchall()
|
|
||||||
columns = [desc[0] for desc in cur.description]
|
|
||||||
result = [dict(zip(columns, row)) for row in rows]
|
|
||||||
except psycopg2.ProgrammingError:
|
|
||||||
result = cur.statusmessage
|
|
||||||
cur.close()
|
|
||||||
return result
|
|
||||||
|
|
||||||
def strip_sql_markdown(content: str) -> str:
|
|
||||||
# 去掉包裹在 ```sql 或 ``` 中的内容
|
|
||||||
match = re.search(r"```sql\s*(.*?)```", content, re.DOTALL | re.IGNORECASE)
|
|
||||||
if match:
|
|
||||||
return match.group(1).strip()
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# ORM 写入包装函数
|
|
||||||
def save_message_thread_safe(**kwargs):
|
|
||||||
def _save():
|
|
||||||
with transaction.atomic():
|
|
||||||
Message.objects.create(**kwargs)
|
|
||||||
threading.Thread(target=_save).start()
|
|
||||||
|
|
@ -1,155 +0,0 @@
|
||||||
import requests
|
|
||||||
import json
|
|
||||||
from rest_framework.views import APIView
|
|
||||||
from apps.ichat.serializers import MessageSerializer, ConversationSerializer
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from apps.ichat.models import Conversation, Message
|
|
||||||
from apps.ichat.utils import connect_db, extract_sql_code, execute_sql, get_schema_text, is_safe_sql, save_message_thread_safe
|
|
||||||
from django.http import StreamingHttpResponse, JsonResponse
|
|
||||||
from rest_framework.decorators import action
|
|
||||||
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet
|
|
||||||
|
|
||||||
# API_KEY = "sk-5644e2d6077b46b9a04a8a2b12d6b693"
|
|
||||||
# API_BASE = "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
|
||||||
# MODEL = "qwen-plus"
|
|
||||||
|
|
||||||
# #本地部署的模式
|
|
||||||
API_KEY = "JJVAide0hw3eaugGmxecyYYFw45FX2LfhnYJtC+W2rw"
|
|
||||||
API_BASE = "http://106.0.4.200:9000/v1"
|
|
||||||
MODEL = "qwen14b"
|
|
||||||
|
|
||||||
# google gemini
|
|
||||||
# API_KEY = "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"
|
|
||||||
# API_BASE = "https://openrouter.ai/api/v1"
|
|
||||||
# MODEL="google/gemini-2.0-flash-exp:free"
|
|
||||||
|
|
||||||
# deepseek v3
|
|
||||||
# API_KEY = "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"
|
|
||||||
# API_BASE = "https://openrouter.ai/api/v1"
|
|
||||||
# MODEL="deepseek/deepseek-chat-v3-0324:free"
|
|
||||||
|
|
||||||
TABLES = ["enm_mpoint", "enm_mpointstat", "enm_mplogx"] # 如果整个数据库全都给模型,准确率下降,所以只给模型部分表
|
|
||||||
|
|
||||||
|
|
||||||
class QueryLLMviewSet(CustomModelViewSet):
|
|
||||||
queryset = Message.objects.all()
|
|
||||||
serializer_class = MessageSerializer
|
|
||||||
ordering = ['create_time']
|
|
||||||
perms_map = {'get':'*', 'post':'*', 'put':'*'}
|
|
||||||
|
|
||||||
@action(methods=['post'], detail=False, perms_map={'post':'*'} ,serializer_class=MessageSerializer)
|
|
||||||
def completion(self, request):
|
|
||||||
serializer = self.get_serializer(data=request.data)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
|
||||||
serializer.save()
|
|
||||||
prompt = serializer.validated_data['content']
|
|
||||||
conversation = serializer.validated_data['conversation']
|
|
||||||
if not prompt or not conversation:
|
|
||||||
return JsonResponse({"error": "缺少 prompt 或 conversation"}, status=400)
|
|
||||||
save_message_thread_safe(content=prompt, conversation=conversation, role="user")
|
|
||||||
url = f"{API_BASE}/chat/completions"
|
|
||||||
headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Authorization": f"Bearer {API_KEY}"
|
|
||||||
}
|
|
||||||
|
|
||||||
user_prompt = f"""
|
|
||||||
我提问的问题是:{prompt}请判断我的问题是否与数据库查询或操作相关。如果是,回答"database";如果不是,回答"general"。
|
|
||||||
|
|
||||||
注意:
|
|
||||||
只需回答"database"或"general"即可,不要有其他内容。
|
|
||||||
"""
|
|
||||||
_payload = {
|
|
||||||
"model": MODEL,
|
|
||||||
"messages": [{"role": "user", "content": user_prompt}],
|
|
||||||
"temperature": 0,
|
|
||||||
"max_tokens": 10
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
class_response = requests.post(url, headers=headers, json=_payload)
|
|
||||||
class_response.raise_for_status()
|
|
||||||
class_result = class_response.json()
|
|
||||||
question_type = class_result.get('choices', [{}])[0].get('message', {}).get('content', '').strip().lower()
|
|
||||||
print("question_type", question_type)
|
|
||||||
if question_type == "database":
|
|
||||||
conn = connect_db()
|
|
||||||
schema_text = get_schema_text(conn, TABLES)
|
|
||||||
print("schema_text----------------------", schema_text)
|
|
||||||
user_prompt = f"""你是一个专业的数据库工程师,根据以下数据库结构:
|
|
||||||
{schema_text}
|
|
||||||
请根据我的需求生成一条标准的PostgreSQL SQL语句,直接返回SQL,不要额外解释。
|
|
||||||
需求是:{prompt}
|
|
||||||
"""
|
|
||||||
else:
|
|
||||||
user_prompt = f"""
|
|
||||||
回答以下问题,不需要涉及数据库查询:
|
|
||||||
|
|
||||||
问题: {prompt}
|
|
||||||
|
|
||||||
请直接回答问题,不要提及数据库或SQL。
|
|
||||||
"""
|
|
||||||
# TODO 是否应该拿到conservastion的id,然后根据id去数据库查询所以的messages, 然后赋值给messages
|
|
||||||
# history = Message.objects.filter(conversation=conversation).order_by('create_time')
|
|
||||||
# chat_history = [{"role": msg.role, "content": msg.content} for msg in history]
|
|
||||||
# chat_history.append({"role": "user", "content": prompt})
|
|
||||||
chat_history = [{"role":"user", "content":user_prompt}]
|
|
||||||
print("chat_history", chat_history)
|
|
||||||
payload = {
|
|
||||||
"model": MODEL,
|
|
||||||
"messages": chat_history,
|
|
||||||
"temperature": 0,
|
|
||||||
"stream": True
|
|
||||||
}
|
|
||||||
response = requests.post(url, headers=headers, json=payload)
|
|
||||||
response.raise_for_status()
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
return JsonResponse({"error":f"LLM API调用失败: {e}"}, status=500)
|
|
||||||
def stream_generator():
|
|
||||||
accumulated_content = ""
|
|
||||||
for line in response.iter_lines():
|
|
||||||
if line:
|
|
||||||
decoded_line = line.decode('utf-8')
|
|
||||||
if decoded_line.startswith('data:'):
|
|
||||||
if decoded_line.strip() == "data: [DONE]":
|
|
||||||
break # OpenAI-style标志结束
|
|
||||||
try:
|
|
||||||
data = json.loads(decoded_line[6:])
|
|
||||||
content = data.get('choices', [{}])[0].get('delta', {}).get('content', '')
|
|
||||||
if content:
|
|
||||||
accumulated_content += content
|
|
||||||
yield f"data: {content}\n\n"
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
yield f"data: [解析失败]: {str(e)}\n\n"
|
|
||||||
print("accumulated_content", accumulated_content)
|
|
||||||
save_message_thread_safe(content=accumulated_content, conversation=conversation, role="system")
|
|
||||||
|
|
||||||
if question_type == "database":
|
|
||||||
sql = extract_sql_code(accumulated_content)
|
|
||||||
if sql:
|
|
||||||
try:
|
|
||||||
conn = connect_db()
|
|
||||||
if is_safe_sql(sql):
|
|
||||||
result = execute_sql(conn, sql)
|
|
||||||
save_message_thread_safe(content=f"SQL结果: {result}", conversation=conversation, role="system")
|
|
||||||
yield f"data: SQL执行结果: {result}\n\n"
|
|
||||||
else:
|
|
||||||
yield f"data: 拒绝执行非查询类 SQL:{sql}\n\n"
|
|
||||||
except Exception as e:
|
|
||||||
yield f"data: SQL执行失败: {str(e)}\n\n"
|
|
||||||
finally:
|
|
||||||
if conn:
|
|
||||||
conn.close()
|
|
||||||
else:
|
|
||||||
yield "data: \\n[文本结束]\n\n"
|
|
||||||
return StreamingHttpResponse(stream_generator(), content_type='text/event-stream')
|
|
||||||
|
|
||||||
|
|
||||||
# 先新建对话 生成对话session_id
|
|
||||||
class ConversationViewSet(CustomModelViewSet):
|
|
||||||
queryset = Conversation.objects.all()
|
|
||||||
serializer_class = ConversationSerializer
|
|
||||||
ordering = ['create_time']
|
|
||||||
perms_map = {'get':'*', 'post':'*', 'put':'*'}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,286 +0,0 @@
|
||||||
import requests
|
|
||||||
import json
|
|
||||||
import faiss
|
|
||||||
import numpy as np
|
|
||||||
from rest_framework.views import APIView
|
|
||||||
from apps.ichat.serializers import MessageSerializer, ConversationSerializer
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from apps.ichat.models import Conversation, Message
|
|
||||||
from apps.ichat.utils import connect_db, extract_sql_code, execute_sql, is_safe_sql, save_message_thread_safe, get_table_structures
|
|
||||||
from django.http import StreamingHttpResponse, JsonResponse
|
|
||||||
from rest_framework.decorators import action
|
|
||||||
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet
|
|
||||||
|
|
||||||
# API_KEY = "sk-5644e2d6077b46b9a04a8a2b12d6b693"
|
|
||||||
# API_BASE = "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
|
||||||
# MODEL = "qwen-plus"
|
|
||||||
|
|
||||||
#本地部署的模式
|
|
||||||
API_KEY = "JJVAide0hw3eaugGmxecyYYFw45FX2LfhnYJtC+W2rw"
|
|
||||||
API_BASE = "http://106.0.4.200:9000/v1"
|
|
||||||
MODEL = "qwen14b"
|
|
||||||
|
|
||||||
# 文本向量化模型
|
|
||||||
EM_MODEL = "m3e-base"
|
|
||||||
API_BASE_EM = "http://106.0.4.200:9997/v1"
|
|
||||||
|
|
||||||
# google gemini
|
|
||||||
# API_KEY = "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"
|
|
||||||
# API_BASE = "https://openrouter.ai/api/v1"
|
|
||||||
# MODEL="google/gemini-2.0-flash-exp:free"
|
|
||||||
|
|
||||||
# deepseek v3
|
|
||||||
# API_KEY = "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"
|
|
||||||
# API_BASE = "https://openrouter.ai/api/v1"
|
|
||||||
# MODEL="deepseek/deepseek-chat-v3-0324:free"
|
|
||||||
|
|
||||||
TABLES = ["enm_mpoint", "enm_mpointstat", "enm_mplogx"] # 如果整个数据库全都给模型,准确率下降,所以只给模型部分表
|
|
||||||
|
|
||||||
HEADERS = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Authorization": f"Bearer {API_KEY}"
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_table_names(conn):
|
|
||||||
sql = """
|
|
||||||
SELECT tablename
|
|
||||||
FROM pg_tables
|
|
||||||
WHERE schemaname = 'public';
|
|
||||||
"""
|
|
||||||
cur = conn.cursor()
|
|
||||||
cur.execute(sql)
|
|
||||||
data = cur.fetchall()
|
|
||||||
cur.close()
|
|
||||||
return [row[0] for row in data]
|
|
||||||
|
|
||||||
# def get_relation_table(query):
|
|
||||||
# conn = connect_db()
|
|
||||||
# # table_names = TABLES
|
|
||||||
# table_names = get_table_names(conn)
|
|
||||||
# schemas = get_table_structures(conn, table_names)
|
|
||||||
|
|
||||||
# texts = [
|
|
||||||
# f"这是一个数据库表结构,表名为 {s['table']},其结构如下:{s['text']}"
|
|
||||||
# for s in schemas
|
|
||||||
# ]
|
|
||||||
# table_names = [s["table"] for s in schemas]
|
|
||||||
# embeddings = embed_text(texts)
|
|
||||||
# index, index_table_map = create_index(embeddings, texts, table_names)
|
|
||||||
|
|
||||||
# results = search_similar_tables(query, index, index_table_map, top_k=3)
|
|
||||||
|
|
||||||
# if not results:
|
|
||||||
# return "没有找到相关表结构"
|
|
||||||
# return results
|
|
||||||
|
|
||||||
def get_relation_table(query: str):
|
|
||||||
conn = connect_db()
|
|
||||||
table_names = get_table_names(conn) # 只获取用户表
|
|
||||||
schemas = get_table_structures(conn, table_names)
|
|
||||||
texts = [s["text"] for s in schemas]
|
|
||||||
table_names = [s["table"] for s in schemas]
|
|
||||||
embeddings = embed_text(texts)
|
|
||||||
|
|
||||||
# 存储向量
|
|
||||||
store_embeddings_pg(conn, embeddings, texts, table_names)
|
|
||||||
|
|
||||||
# 查询相似表
|
|
||||||
results = search_similar_tables_pg(conn, query, top_k=5)
|
|
||||||
|
|
||||||
if len(results) == 0:
|
|
||||||
return "没有找到相关表结构"
|
|
||||||
# 只取相关表的结构
|
|
||||||
schemas = get_table_structures(conn, results)
|
|
||||||
|
|
||||||
llm_results = format_schema_for_llm(schemas)
|
|
||||||
return llm_results
|
|
||||||
|
|
||||||
def store_embeddings_pg(conn, embeddings: list[list[float]], texts: list[str], table_names: list[str]):
|
|
||||||
cur = conn.cursor()
|
|
||||||
for embedding, text, table_name in zip(embeddings, texts, table_names):
|
|
||||||
cur.execute("""
|
|
||||||
INSERT INTO table_embeddings (table_name, schema_text, embedding)
|
|
||||||
VALUES (%s, %s, %s)
|
|
||||||
ON CONFLICT (table_name) DO UPDATE
|
|
||||||
SET schema_text = EXCLUDED.schema_text,
|
|
||||||
embedding = EXCLUDED.embedding
|
|
||||||
""", (table_name, text, embedding))
|
|
||||||
conn.commit()
|
|
||||||
cur.close()
|
|
||||||
|
|
||||||
def search_similar_tables_pg(conn, query: str, top_k: int = 5):
|
|
||||||
# 第一步:将 query 转为 embedding
|
|
||||||
query_embedding = embed_text([query])[0]
|
|
||||||
# 第二步:embedding 转成 '[x, y, z]' 格式字符串
|
|
||||||
embedding_str = ",".join(map(str, query_embedding))
|
|
||||||
cur = conn.cursor()
|
|
||||||
query = f"""
|
|
||||||
SELECT table_name
|
|
||||||
FROM table_embeddings
|
|
||||||
ORDER BY embedding <-> '[{embedding_str}]'::vector
|
|
||||||
LIMIT {top_k};
|
|
||||||
"""
|
|
||||||
cur.execute(query)
|
|
||||||
results = [row[0] for row in cur.fetchall()]
|
|
||||||
cur.close()
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
def format_schema_for_llm(schemas: list[dict]) -> str:
|
|
||||||
lines = []
|
|
||||||
for schema in schemas:
|
|
||||||
lines.append(f"【表名】:{schema['table']}")
|
|
||||||
lines.append("【字段】:")
|
|
||||||
for col in schema["text"].split("结构如下:")[1].split("\n"):
|
|
||||||
if col.strip():
|
|
||||||
lines.append(f" - {col.strip()}")
|
|
||||||
lines.append("") # 空行分隔表
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
|
|
||||||
def embed_text(texts: list[str]) -> list[list[float]]:
|
|
||||||
paylaod = {
|
|
||||||
"input":texts,
|
|
||||||
"model":EM_MODEL
|
|
||||||
}
|
|
||||||
url = f"{API_BASE_EM}/embeddings"
|
|
||||||
response = requests.post(url, headers=HEADERS, json=paylaod)
|
|
||||||
json_data = response.json()
|
|
||||||
return [e['embedding'] for e in json_data['data']]
|
|
||||||
|
|
||||||
# def search_similar_tables(query: str, index, index_table_map, top_k:int=3):
|
|
||||||
# query_embedding = embed_text([query])[0]
|
|
||||||
# distances, indices = index.search(np.array([query_embedding]).astype("float32"), int(top_k))
|
|
||||||
# results = []
|
|
||||||
# for i in indices[0]:
|
|
||||||
# if i != -1 and i in index_table_map:
|
|
||||||
# results.append(index_table_map[i])
|
|
||||||
# return results
|
|
||||||
|
|
||||||
# def create_index(embeddings: list[list[float]], texts: list[str], table_names: list[str]):
|
|
||||||
# print(len(embeddings), '-----------')
|
|
||||||
# dim = len(embeddings[0])
|
|
||||||
# index = faiss.IndexFlatL2(dim)
|
|
||||||
# embeddings_np = np.array(embeddings).astype('float32')
|
|
||||||
# index.add(embeddings_np)
|
|
||||||
|
|
||||||
# # 构建索引到表名的映射字典
|
|
||||||
# index_table_map = {i: table_names[i] for i in range(len(table_names))}
|
|
||||||
# return index, index_table_map
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class QueryLLMviewSet(CustomModelViewSet):
|
|
||||||
queryset = Message.objects.all()
|
|
||||||
serializer_class = MessageSerializer
|
|
||||||
ordering = ['create_time']
|
|
||||||
perms_map = {'get':'*', 'post':'*', 'put':'*'}
|
|
||||||
|
|
||||||
@action(methods=['post'], detail=False, perms_map={'post':'*'} ,serializer_class=MessageSerializer)
|
|
||||||
def completion(self, request):
|
|
||||||
serializer = self.get_serializer(data=request.data)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
|
||||||
serializer.save()
|
|
||||||
prompt = serializer.validated_data['content']
|
|
||||||
conversation = serializer.validated_data['conversation']
|
|
||||||
if not prompt or not conversation:
|
|
||||||
return JsonResponse({"error": "缺少 prompt 或 conversation"}, status=400)
|
|
||||||
save_message_thread_safe(content=prompt, conversation=conversation, role="user")
|
|
||||||
url = f"{API_BASE}/chat/completions"
|
|
||||||
user_prompt = f"""
|
|
||||||
我提问的问题是:{prompt}请判断我的问题是否与数据库查询或操作相关。如果是,回答"database";如果不是,回答"general"。
|
|
||||||
|
|
||||||
注意:
|
|
||||||
只需回答"database"或"general"即可,不要有其他内容。
|
|
||||||
"""
|
|
||||||
_payload = {
|
|
||||||
"model": MODEL,
|
|
||||||
"messages": [{"role": "user", "content": user_prompt}],
|
|
||||||
"temperature": 0,
|
|
||||||
"max_tokens": 10
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
class_response = requests.post(url, headers=HEADERS, json=_payload)
|
|
||||||
class_response.raise_for_status()
|
|
||||||
class_result = class_response.json()
|
|
||||||
question_type = class_result.get('choices', [{}])[0].get('message', {}).get('content', '').strip().lower()
|
|
||||||
print("question_type", question_type)
|
|
||||||
if question_type == "database":
|
|
||||||
schema_text = get_relation_table(prompt)
|
|
||||||
user_prompt = f"""你是一个专业的数据库工程师,根据以下数据库结构:
|
|
||||||
{schema_text}
|
|
||||||
请根据我的需求生成一条标准的PostgreSQL SQL语句,直接返回SQL,不要额外解释。
|
|
||||||
需求是:{prompt}
|
|
||||||
"""
|
|
||||||
else:
|
|
||||||
user_prompt = f"""
|
|
||||||
回答以下问题,不需要涉及数据库查询:
|
|
||||||
|
|
||||||
问题: {prompt}
|
|
||||||
|
|
||||||
请直接回答问题,不要提及数据库或SQL。
|
|
||||||
"""
|
|
||||||
# TODO 是否应该拿到conservastion的id,然后根据id去数据库查询所以的messages, 然后赋值给messages
|
|
||||||
# history = Message.objects.filter(conversation=conversation).order_by('create_time')
|
|
||||||
# chat_history = [{"role": msg.role, "content": msg.content} for msg in history]
|
|
||||||
# chat_history.append({"role": "user", "content": prompt})
|
|
||||||
chat_history = [{"role":"user", "content":user_prompt}]
|
|
||||||
print("user_prompt", user_prompt)
|
|
||||||
payload = {
|
|
||||||
"model": MODEL,
|
|
||||||
"messages": chat_history,
|
|
||||||
"temperature": 0,
|
|
||||||
"stream": True
|
|
||||||
}
|
|
||||||
response = requests.post(url, headers=HEADERS, json=payload)
|
|
||||||
response.raise_for_status()
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
return JsonResponse({"error":f"LLM API调用失败: {e}"}, status=500)
|
|
||||||
def stream_generator():
|
|
||||||
accumulated_content = ""
|
|
||||||
for line in response.iter_lines():
|
|
||||||
if line:
|
|
||||||
decoded_line = line.decode('utf-8')
|
|
||||||
if decoded_line.startswith('data:'):
|
|
||||||
if decoded_line.strip() == "data: [DONE]":
|
|
||||||
break # OpenAI-style标志结束
|
|
||||||
try:
|
|
||||||
data = json.loads(decoded_line[6:])
|
|
||||||
content = data.get('choices', [{}])[0].get('delta', {}).get('content', '')
|
|
||||||
if content:
|
|
||||||
accumulated_content += content
|
|
||||||
yield f"data: {content}\n\n"
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
yield f"data: [解析失败]: {str(e)}\n\n"
|
|
||||||
print("accumulated_content", accumulated_content)
|
|
||||||
save_message_thread_safe(content=accumulated_content, conversation=conversation, role="system")
|
|
||||||
|
|
||||||
if question_type == "database":
|
|
||||||
sql = extract_sql_code(accumulated_content)
|
|
||||||
if sql:
|
|
||||||
try:
|
|
||||||
conn = connect_db()
|
|
||||||
if is_safe_sql(sql):
|
|
||||||
result = execute_sql(conn, sql)
|
|
||||||
save_message_thread_safe(content=f"SQL结果: {result}", conversation=conversation, role="system")
|
|
||||||
yield f"data: SQL执行结果: {result}\n\n"
|
|
||||||
else:
|
|
||||||
yield f"data: 拒绝执行非查询类 SQL:{sql}\n\n"
|
|
||||||
except Exception as e:
|
|
||||||
yield f"data: SQL执行失败: {str(e)}\n\n"
|
|
||||||
finally:
|
|
||||||
if conn:
|
|
||||||
conn.close()
|
|
||||||
else:
|
|
||||||
yield "data: \\n[文本结束]\n\n"
|
|
||||||
return StreamingHttpResponse(stream_generator(), content_type='text/event-stream')
|
|
||||||
|
|
||||||
# 先新建对话 生成对话session_id
|
|
||||||
class ConversationViewSet(CustomModelViewSet):
|
|
||||||
queryset = Conversation.objects.all()
|
|
||||||
serializer_class = ConversationSerializer
|
|
||||||
ordering = ['create_time']
|
|
||||||
perms_map = {'get':'*', 'post':'*', 'put':'*'}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,214 +0,0 @@
|
||||||
import requests
|
|
||||||
import json
|
|
||||||
import faiss
|
|
||||||
import numpy as np
|
|
||||||
from rest_framework.views import APIView
|
|
||||||
from apps.ichat.serializers import MessageSerializer, ConversationSerializer
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from apps.ichat.models import Conversation, Message
|
|
||||||
from apps.ichat.utils import connect_db, extract_sql_code, execute_sql, get_schema_text, is_safe_sql, save_message_thread_safe
|
|
||||||
from django.http import StreamingHttpResponse, JsonResponse
|
|
||||||
from rest_framework.decorators import action
|
|
||||||
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet
|
|
||||||
|
|
||||||
# API_KEY = "sk-5644e2d6077b46b9a04a8a2b12d6b693"
|
|
||||||
# API_BASE = "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
|
||||||
# MODEL = "qwen-plus"
|
|
||||||
|
|
||||||
#本地部署的模式
|
|
||||||
API_KEY = "JJVAide0hw3eaugGmxecyYYFw45FX2LfhnYJtC+W2rw"
|
|
||||||
API_BASE = "http://106.0.4.200:9000/v1"
|
|
||||||
MODEL = "qwen14b"
|
|
||||||
|
|
||||||
# 文本向量化模型
|
|
||||||
EM_MODEL = "m3e-base"
|
|
||||||
|
|
||||||
# google gemini
|
|
||||||
# API_KEY = "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"
|
|
||||||
# API_BASE = "https://openrouter.ai/api/v1"
|
|
||||||
# MODEL="google/gemini-2.0-flash-exp:free"
|
|
||||||
|
|
||||||
# deepseek v3
|
|
||||||
# API_KEY = "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"
|
|
||||||
# API_BASE = "https://openrouter.ai/api/v1"
|
|
||||||
# MODEL="deepseek/deepseek-chat-v3-0324:free"
|
|
||||||
|
|
||||||
TABLES = ["enm_mpoint", "enm_mpointstat", "enm_mplogx"] # 如果整个数据库全都给模型,准确率下降,所以只给模型部分表
|
|
||||||
|
|
||||||
HEADERS = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Authorization": f"Bearer {API_KEY}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 表结构向量化
|
|
||||||
def embed_text(texts: list[str]) -> list[list[float]]:
|
|
||||||
url = f"{API_BASE}/embeddings"
|
|
||||||
_payload = {
|
|
||||||
"model": EM_MODEL,
|
|
||||||
"input": texts
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
response = requests.post(url, headers=HEADERS, json=_payload)
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
return JsonResponse({"error":f"Embedding API调用失败: {e}"}, status=500)
|
|
||||||
print("embeddings", response["data"])
|
|
||||||
return [e['embedding'] for e in response['data']]
|
|
||||||
|
|
||||||
|
|
||||||
# 创建Faiss索引
|
|
||||||
def create_index(embeddings: list[list[float]], texts: list[str], table_names: list[str]):
|
|
||||||
index = faiss.IndexFlatL2(len(embeddings[0]))
|
|
||||||
index.add(np.array(embeddings)).astype("float32")
|
|
||||||
index_table_map = {i: {"table": table_names[i], "text": texts[i]} for i in range(len(table_names))}
|
|
||||||
return index, index_table_map
|
|
||||||
|
|
||||||
# 查询
|
|
||||||
def search_similar_tables(query:str, index, index_table_map, k:int=5):
|
|
||||||
query_embedding = embed_text([query])[0]
|
|
||||||
distances, indices = index.search(np.array([query_embedding]).astype("float32"), k)
|
|
||||||
return [index_table_map[i] for i in indices[0]]
|
|
||||||
|
|
||||||
def get_tables(conn) -> list[str]:
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
cur.execute("""
|
|
||||||
SELECT tablename
|
|
||||||
FROM pg_tables
|
|
||||||
WHERE schemaname = 'public'
|
|
||||||
AND tableowner = 'postgres';
|
|
||||||
""")
|
|
||||||
return [row[0] for row in cur.fetchall()]
|
|
||||||
|
|
||||||
|
|
||||||
# 主函数:提取表结构、嵌入向量并存储到 FAISS
|
|
||||||
def get_relation_table(query):
|
|
||||||
conn = connect_db()
|
|
||||||
table_names = get_tables(conn)
|
|
||||||
schemas = get_schema_text(conn, table_names)
|
|
||||||
texts = [s["text"] for s in schemas]
|
|
||||||
# table_names = [s["table"] for s in schemas]
|
|
||||||
embeddings = embed_text(texts)
|
|
||||||
index, index_table_map = create_index(embeddings, texts, table_names)
|
|
||||||
results = search_similar_tables(query, index, index_table_map)
|
|
||||||
|
|
||||||
for result in results:
|
|
||||||
print(f"表名: {result['table']}\n结构: {result['text']}")
|
|
||||||
if len(results) == 0:
|
|
||||||
return "没有找到相关表结构"
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
class QueryLLMviewSet(CustomModelViewSet):
|
|
||||||
queryset = Message.objects.all()
|
|
||||||
serializer_class = MessageSerializer
|
|
||||||
ordering = ['create_time']
|
|
||||||
perms_map = {'get':'*', 'post':'*', 'put':'*'}
|
|
||||||
|
|
||||||
@action(methods=['post'], detail=False, perms_map={'post':'*'} ,serializer_class=MessageSerializer)
|
|
||||||
def completion(self, request):
|
|
||||||
serializer = self.get_serializer(data=request.data)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
|
||||||
serializer.save()
|
|
||||||
prompt = serializer.validated_data['content']
|
|
||||||
conversation = serializer.validated_data['conversation']
|
|
||||||
if not prompt or not conversation:
|
|
||||||
return JsonResponse({"error": "缺少 prompt 或 conversation"}, status=400)
|
|
||||||
save_message_thread_safe(content=prompt, conversation=conversation, role="user")
|
|
||||||
url = f"{API_BASE}/chat/completions"
|
|
||||||
user_prompt = f"""
|
|
||||||
我提问的问题是:{prompt}请判断我的问题是否与数据库查询或操作相关。如果是,回答"database";如果不是,回答"general"。
|
|
||||||
|
|
||||||
注意:
|
|
||||||
只需回答"database"或"general"即可,不要有其他内容。
|
|
||||||
"""
|
|
||||||
_payload = {
|
|
||||||
"model": MODEL,
|
|
||||||
"messages": [{"role": "user", "content": user_prompt}],
|
|
||||||
"temperature": 0,
|
|
||||||
"max_tokens": 10
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
class_response = requests.post(url, headers=HEADERS, json=_payload)
|
|
||||||
class_response.raise_for_status()
|
|
||||||
class_result = class_response.json()
|
|
||||||
question_type = class_result.get('choices', [{}])[0].get('message', {}).get('content', '').strip().lower()
|
|
||||||
print("question_type", question_type)
|
|
||||||
if question_type == "database":
|
|
||||||
schema_text = get_relation_table(prompt)
|
|
||||||
print("schema_text----------------------", schema_text)
|
|
||||||
user_prompt = f"""你是一个专业的数据库工程师,根据以下数据库结构:
|
|
||||||
{schema_text}
|
|
||||||
请根据我的需求生成一条标准的PostgreSQL SQL语句,直接返回SQL,不要额外解释。
|
|
||||||
需求是:{prompt}
|
|
||||||
"""
|
|
||||||
else:
|
|
||||||
user_prompt = f"""
|
|
||||||
回答以下问题,不需要涉及数据库查询:
|
|
||||||
|
|
||||||
问题: {prompt}
|
|
||||||
|
|
||||||
请直接回答问题,不要提及数据库或SQL。
|
|
||||||
"""
|
|
||||||
# TODO 是否应该拿到conservastion的id,然后根据id去数据库查询所以的messages, 然后赋值给messages
|
|
||||||
# history = Message.objects.filter(conversation=conversation).order_by('create_time')
|
|
||||||
# chat_history = [{"role": msg.role, "content": msg.content} for msg in history]
|
|
||||||
# chat_history.append({"role": "user", "content": prompt})
|
|
||||||
chat_history = [{"role":"user", "content":user_prompt}]
|
|
||||||
print("chat_history", chat_history)
|
|
||||||
payload = {
|
|
||||||
"model": MODEL,
|
|
||||||
"messages": chat_history,
|
|
||||||
"temperature": 0,
|
|
||||||
"stream": True
|
|
||||||
}
|
|
||||||
response = requests.post(url, headers=HEADERS, json=payload)
|
|
||||||
response.raise_for_status()
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
return JsonResponse({"error":f"LLM API调用失败: {e}"}, status=500)
|
|
||||||
def stream_generator():
|
|
||||||
accumulated_content = ""
|
|
||||||
for line in response.iter_lines():
|
|
||||||
if line:
|
|
||||||
decoded_line = line.decode('utf-8')
|
|
||||||
if decoded_line.startswith('data:'):
|
|
||||||
if decoded_line.strip() == "data: [DONE]":
|
|
||||||
break # OpenAI-style标志结束
|
|
||||||
try:
|
|
||||||
data = json.loads(decoded_line[6:])
|
|
||||||
content = data.get('choices', [{}])[0].get('delta', {}).get('content', '')
|
|
||||||
if content:
|
|
||||||
accumulated_content += content
|
|
||||||
yield f"data: {content}\n\n"
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
yield f"data: [解析失败]: {str(e)}\n\n"
|
|
||||||
print("accumulated_content", accumulated_content)
|
|
||||||
save_message_thread_safe(content=accumulated_content, conversation=conversation, role="system")
|
|
||||||
|
|
||||||
if question_type == "database":
|
|
||||||
sql = extract_sql_code(accumulated_content)
|
|
||||||
if sql:
|
|
||||||
try:
|
|
||||||
conn = connect_db()
|
|
||||||
if is_safe_sql(sql):
|
|
||||||
result = execute_sql(conn, sql)
|
|
||||||
save_message_thread_safe(content=f"SQL结果: {result}", conversation=conversation, role="system")
|
|
||||||
yield f"data: SQL执行结果: {result}\n\n"
|
|
||||||
else:
|
|
||||||
yield f"data: 拒绝执行非查询类 SQL:{sql}\n\n"
|
|
||||||
except Exception as e:
|
|
||||||
yield f"data: SQL执行失败: {str(e)}\n\n"
|
|
||||||
finally:
|
|
||||||
if conn:
|
|
||||||
conn.close()
|
|
||||||
else:
|
|
||||||
yield "data: \\n[文本结束]\n\n"
|
|
||||||
return StreamingHttpResponse(stream_generator(), content_type='text/event-stream')
|
|
||||||
|
|
||||||
# 先新建对话 生成对话session_id
|
|
||||||
class ConversationViewSet(CustomModelViewSet):
|
|
||||||
queryset = Conversation.objects.all()
|
|
||||||
serializer_class = ConversationSerializer
|
|
||||||
ordering = ['create_time']
|
|
||||||
perms_map = {'get':'*', 'post':'*', 'put':'*'}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,129 +0,0 @@
|
||||||
import requests
|
|
||||||
import os
|
|
||||||
from apps.utils.sql import execute_raw_sql
|
|
||||||
import json
|
|
||||||
from apps.utils.tools import MyJSONEncoder
|
|
||||||
from .utils import is_safe_sql
|
|
||||||
from rest_framework.views import APIView
|
|
||||||
from drf_yasg.utils import swagger_auto_schema
|
|
||||||
from rest_framework import serializers
|
|
||||||
from rest_framework.exceptions import ParseError
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from django.conf import settings
|
|
||||||
from apps.utils.mixins import MyLoggingMixin
|
|
||||||
from django.core.cache import cache
|
|
||||||
import uuid
|
|
||||||
from apps.utils.thread import MyThread
|
|
||||||
|
|
||||||
LLM_URL = getattr(settings, "LLM_URL", "")
|
|
||||||
API_KEY = getattr(settings, "LLM_API_KEY", "")
|
|
||||||
MODEL = "qwen14b"
|
|
||||||
HEADERS = {
|
|
||||||
"Authorization": f"Bearer {API_KEY}",
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
CUR_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
|
|
||||||
def load_promot(name):
|
|
||||||
with open(os.path.join(CUR_DIR, f'promot/{name}.md'), 'r') as f:
|
|
||||||
return f.read()
|
|
||||||
|
|
||||||
|
|
||||||
def ask(input:str, p_name:str, stream=False):
|
|
||||||
his = [{"role":"system", "content": load_promot(p_name)}]
|
|
||||||
his.append({"role":"user", "content": input})
|
|
||||||
payload = {
|
|
||||||
"model": MODEL,
|
|
||||||
"messages": his,
|
|
||||||
"temperature": 0,
|
|
||||||
"stream": stream
|
|
||||||
}
|
|
||||||
response = requests.post(LLM_URL, headers=HEADERS, json=payload, stream=stream)
|
|
||||||
if not stream:
|
|
||||||
return response.json()["choices"][0]["message"]["content"]
|
|
||||||
else:
|
|
||||||
# 处理流式响应
|
|
||||||
full_content = ""
|
|
||||||
for chunk in response.iter_lines():
|
|
||||||
if chunk:
|
|
||||||
# 通常流式响应是SSE格式(data: {...})
|
|
||||||
decoded_chunk = chunk.decode('utf-8')
|
|
||||||
if decoded_chunk.startswith("data:"):
|
|
||||||
json_str = decoded_chunk[5:].strip()
|
|
||||||
if json_str == "[DONE]":
|
|
||||||
break
|
|
||||||
try:
|
|
||||||
chunk_data = json.loads(json_str)
|
|
||||||
if "choices" in chunk_data and chunk_data["choices"]:
|
|
||||||
delta = chunk_data["choices"][0].get("delta", {})
|
|
||||||
if "content" in delta:
|
|
||||||
print(delta["content"])
|
|
||||||
full_content += delta["content"]
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
continue
|
|
||||||
return full_content
|
|
||||||
|
|
||||||
def work_chain(input:str, t_key:str):
|
|
||||||
pdict = {"state": "progress", "steps": [{"state":"ok", "msg":"正在生成查询语句"}]}
|
|
||||||
cache.set(t_key, pdict)
|
|
||||||
res_text = ask(input, 'w_sql')
|
|
||||||
if res_text == '请以 查询 开头,重新描述你的需求':
|
|
||||||
pdict["state"] = "error"
|
|
||||||
pdict["steps"].append({"state":"error", "msg":res_text})
|
|
||||||
cache.set(t_key, pdict)
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
pdict["steps"].append({"state":"ok", "msg":"查询语句生成成功", "content":res_text})
|
|
||||||
cache.set(t_key, pdict)
|
|
||||||
if not is_safe_sql(res_text):
|
|
||||||
pdict["state"] = "error"
|
|
||||||
pdict["steps"].append({"state":"error", "msg":"当前查询存在风险,请重新描述你的需求"})
|
|
||||||
cache.set(t_key, pdict)
|
|
||||||
return
|
|
||||||
pdict["steps"].append({"state":"ok", "msg":"正在执行查询语句"})
|
|
||||||
cache.set(t_key, pdict)
|
|
||||||
res = execute_raw_sql(res_text)
|
|
||||||
pdict["steps"].append({"state":"ok", "msg":"查询语句执行成功", "content":res})
|
|
||||||
cache.set(t_key, pdict)
|
|
||||||
pdict["steps"].append({"state":"ok", "msg":"正在生成报告"})
|
|
||||||
cache.set(t_key, pdict)
|
|
||||||
res2 = ask(json.dumps(res, cls=MyJSONEncoder, ensure_ascii=False), 'w_ana')
|
|
||||||
content = res2.lstrip('```html ').rstrip('```')
|
|
||||||
pdict["state"] = "done"
|
|
||||||
pdict["content"] = content
|
|
||||||
pdict["steps"].append({"state":"ok", "msg":"报告生成成功", "content": content})
|
|
||||||
cache.set(t_key, pdict)
|
|
||||||
return
|
|
||||||
|
|
||||||
class InputSerializer(serializers.Serializer):
|
|
||||||
input = serializers.CharField(label="查询需求")
|
|
||||||
|
|
||||||
class WorkChain(MyLoggingMixin, APIView):
|
|
||||||
|
|
||||||
@swagger_auto_schema(
|
|
||||||
operation_summary="提交查询需求",
|
|
||||||
request_body=InputSerializer)
|
|
||||||
def post(self, request):
|
|
||||||
llm_enabled = getattr(settings, "LLM_ENABLED", False)
|
|
||||||
if not llm_enabled:
|
|
||||||
raise ParseError('LLM功能未启用')
|
|
||||||
input = request.data.get('input')
|
|
||||||
t_key = f'ichat_{uuid.uuid4()}'
|
|
||||||
MyThread(target=work_chain, args=(input, t_key)).start()
|
|
||||||
return Response({'ichat_tid': t_key})
|
|
||||||
|
|
||||||
@swagger_auto_schema(
|
|
||||||
operation_summary="获取查询进度")
|
|
||||||
def get(self, request):
|
|
||||||
llm_enabled = getattr(settings, "LLM_ENABLED", False)
|
|
||||||
if not llm_enabled:
|
|
||||||
raise ParseError('LLM功能未启用')
|
|
||||||
ichat_tid = request.GET.get('ichat_tid')
|
|
||||||
if ichat_tid:
|
|
||||||
return Response(cache.get(ichat_tid))
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
print(work_chain("查询 一次超洗 工段在2025年6月的生产合格数等并形成报告"))
|
|
||||||
|
|
||||||
from apps.ichat.views2 import work_chain
|
|
||||||
print(work_chain('查询外观检验工段在2025年6月的生产合格数等并形成报告'))
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# Generated by Django 4.2.27 on 2026-04-24 06:50
|
# Generated by Django 3.2.12 on 2026-03-12 03:26
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
@ -12,31 +12,11 @@ class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('system', '0007_alter_dept_create_by_alter_dept_third_info_and_more'),
|
('system', '0007_alter_dept_create_by_alter_dept_third_info_and_more'),
|
||||||
('wf', '0006_auto_20251215_1645'),
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('wf', '0006_auto_20251215_1645'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
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='%(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='创建人')),
|
|
||||||
('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='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='PurchaseRequisition',
|
name='PurchaseRequisition',
|
||||||
fields=[
|
fields=[
|
||||||
|
|
@ -49,100 +29,10 @@ class Migration(migrations.Migration):
|
||||||
('req_date', models.DateField(blank=True, 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='合计金额')),
|
('total_amount', models.DecimalField(decimal_places=2, default=0, max_digits=14, verbose_name='合计金额')),
|
||||||
('note', models.TextField(blank=True, 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='%(class)s_belong_dept', to='system.dept', 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='%(class)s_create_by', to=settings.AUTH_USER_MODEL, 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='关联工单')),
|
('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='%(class)s_update_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='purchaserequisition_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='WareHouse',
|
|
||||||
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, verbose_name='库房编号')),
|
|
||||||
('name', models.CharField(max_length=20, verbose_name='库房名称')),
|
|
||||||
('place', models.CharField(max_length=50, verbose_name='具体地点')),
|
|
||||||
('belong_dept', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='mpr_warehouse_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='mpr_warehouse_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='mpr_warehouse_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
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='%(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='创建人')),
|
|
||||||
('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='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
|
|
||||||
('warehouse', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='entries', to='mpr.warehouse', verbose_name='库房')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
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='账单是否收到')),
|
|
||||||
('status', models.CharField(choices=[('idle', '闲置'), ('in_requisition', '领用中'), ('requisitioned', '已领用')], default='idle', max_length=20, 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='mpr.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={
|
options={
|
||||||
'abstract': False,
|
'abstract': False,
|
||||||
|
|
@ -171,25 +61,4 @@ class Migration(migrations.Migration):
|
||||||
'abstract': False,
|
'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,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='状态'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -1,27 +1,9 @@
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from apps.utils.models import BaseModel, CommonBDModel, CommonBModel
|
from apps.utils.models import BaseModel, CommonBDModel
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from django.db.models import Max, Sum
|
from django.db.models import Max, Sum
|
||||||
|
|
||||||
|
|
||||||
class WareHouse(CommonBModel):
|
|
||||||
"""
|
|
||||||
TN:库房信息
|
|
||||||
"""
|
|
||||||
number = models.CharField('库房编号', max_length=20)
|
|
||||||
name = models.CharField('库房名称', max_length=20)
|
|
||||||
place = models.CharField('具体地点', max_length=50)
|
|
||||||
create_by = models.ForeignKey(
|
|
||||||
'system.user', null=True, blank=True, on_delete=models.SET_NULL,
|
|
||||||
verbose_name='创建人', related_name='mpr_warehouse_create_by')
|
|
||||||
update_by = models.ForeignKey(
|
|
||||||
'system.user', null=True, blank=True, on_delete=models.SET_NULL,
|
|
||||||
verbose_name='最后编辑人', related_name='mpr_warehouse_update_by')
|
|
||||||
belong_dept = models.ForeignKey(
|
|
||||||
'system.dept', null=True, blank=True, on_delete=models.SET_NULL,
|
|
||||||
verbose_name='所属部门', related_name='mpr_warehouse_belong_dept')
|
|
||||||
|
|
||||||
|
|
||||||
def _get_number(model_cls):
|
def _get_number(model_cls):
|
||||||
today_str = datetime.now().strftime('%Y%m%d')
|
today_str = datetime.now().strftime('%Y%m%d')
|
||||||
prefix = model_cls.PREFIX
|
prefix = model_cls.PREFIX
|
||||||
|
|
@ -95,7 +77,7 @@ class WarehouseEntry(CommonBDModel):
|
||||||
|
|
||||||
number = models.CharField('编号', max_length=20, unique=True)
|
number = models.CharField('编号', max_length=20, unique=True)
|
||||||
warehouse = models.ForeignKey(
|
warehouse = models.ForeignKey(
|
||||||
WareHouse, verbose_name='库房',
|
'inm.WareHouse', verbose_name='仓库',
|
||||||
on_delete=models.CASCADE, related_name='entries')
|
on_delete=models.CASCADE, related_name='entries')
|
||||||
entry_date = models.DateField('入库日期', null=True, blank=True)
|
entry_date = models.DateField('入库日期', null=True, blank=True)
|
||||||
entry_type = models.CharField('入库类型', max_length=20, choices=ENTRY_TYPE_CHOICES, default='raw_normal')
|
entry_type = models.CharField('入库类型', max_length=20, choices=ENTRY_TYPE_CHOICES, default='raw_normal')
|
||||||
|
|
@ -141,7 +123,7 @@ class WarehouseStock(BaseModel):
|
||||||
)
|
)
|
||||||
|
|
||||||
warehouse = models.ForeignKey(
|
warehouse = models.ForeignKey(
|
||||||
WareHouse, verbose_name='库房',
|
'inm.WareHouse', verbose_name='仓库',
|
||||||
on_delete=models.CASCADE, related_name='mpr_stocks')
|
on_delete=models.CASCADE, related_name='mpr_stocks')
|
||||||
entry = models.ForeignKey(
|
entry = models.ForeignKey(
|
||||||
WarehouseEntry, verbose_name='来源入库单',
|
WarehouseEntry, verbose_name='来源入库单',
|
||||||
|
|
|
||||||
|
|
@ -7,20 +7,10 @@ from apps.mpr.models import (
|
||||||
PurchaseRequisition, PurchaseRequisitionItem,
|
PurchaseRequisition, PurchaseRequisitionItem,
|
||||||
WarehouseEntry, WarehouseEntryItem, WarehouseStock,
|
WarehouseEntry, WarehouseEntryItem, WarehouseStock,
|
||||||
MaterialRequisition, MaterialRequisitionItem,
|
MaterialRequisition, MaterialRequisitionItem,
|
||||||
WareHouse,
|
|
||||||
)
|
)
|
||||||
from apps.wf.serializers import TicketSimpleSerializer
|
from apps.wf.serializers import TicketSimpleSerializer
|
||||||
|
|
||||||
|
|
||||||
# ========== 库房 ==========
|
|
||||||
|
|
||||||
class WareHouseSerializer(CustomModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = WareHouse
|
|
||||||
fields = '__all__'
|
|
||||||
read_only_fields = ['create_time', 'update_time', 'is_deleted']
|
|
||||||
|
|
||||||
|
|
||||||
# ========== 物资申购单 ==========
|
# ========== 物资申购单 ==========
|
||||||
|
|
||||||
class PurchaseRequisitionItemSerializer(CustomModelSerializer):
|
class PurchaseRequisitionItemSerializer(CustomModelSerializer):
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
from rest_framework.routers import DefaultRouter
|
from rest_framework.routers import DefaultRouter
|
||||||
from apps.mpr.views import (
|
from apps.mpr.views import (
|
||||||
WareHouseViewSet,
|
|
||||||
PurchaseRequisitionViewSet, PurchaseRequisitionItemViewSet,
|
PurchaseRequisitionViewSet, PurchaseRequisitionItemViewSet,
|
||||||
WarehouseEntryViewSet, WarehouseEntryItemViewSet,
|
WarehouseEntryViewSet, WarehouseEntryItemViewSet,
|
||||||
WarehouseStockViewSet,
|
WarehouseStockViewSet,
|
||||||
|
|
@ -11,7 +10,6 @@ from apps.mpr.views import (
|
||||||
API_BASE_URL = 'api/mpr/'
|
API_BASE_URL = 'api/mpr/'
|
||||||
|
|
||||||
router = DefaultRouter()
|
router = DefaultRouter()
|
||||||
router.register('warehouse', WareHouseViewSet, basename='mpr_warehouse')
|
|
||||||
router.register('requisition', PurchaseRequisitionViewSet, basename='requisition')
|
router.register('requisition', PurchaseRequisitionViewSet, basename='requisition')
|
||||||
router.register('requisition_item', PurchaseRequisitionItemViewSet, basename='requisition_item')
|
router.register('requisition_item', PurchaseRequisitionItemViewSet, basename='requisition_item')
|
||||||
router.register('warehouse_entry', WarehouseEntryViewSet, basename='warehouse_entry')
|
router.register('warehouse_entry', WarehouseEntryViewSet, basename='warehouse_entry')
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,8 @@ from apps.mpr.models import (
|
||||||
PurchaseRequisition, PurchaseRequisitionItem,
|
PurchaseRequisition, PurchaseRequisitionItem,
|
||||||
WarehouseEntry, WarehouseEntryItem, WarehouseStock,
|
WarehouseEntry, WarehouseEntryItem, WarehouseStock,
|
||||||
MaterialRequisition, MaterialRequisitionItem,
|
MaterialRequisition, MaterialRequisitionItem,
|
||||||
WareHouse,
|
|
||||||
)
|
)
|
||||||
from apps.mpr.serializers import (
|
from apps.mpr.serializers import (
|
||||||
WareHouseSerializer,
|
|
||||||
PurchaseRequisitionListSerializer,
|
PurchaseRequisitionListSerializer,
|
||||||
PurchaseRequisitionDetailSerializer,
|
PurchaseRequisitionDetailSerializer,
|
||||||
PurchaseRequisitionCreateSerializer,
|
PurchaseRequisitionCreateSerializer,
|
||||||
|
|
@ -32,20 +30,6 @@ from apps.mpr.filters import (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class WareHouseViewSet(CustomModelViewSet):
|
|
||||||
"""
|
|
||||||
库房管理
|
|
||||||
"""
|
|
||||||
queryset = WareHouse.objects.all()
|
|
||||||
serializer_class = WareHouseSerializer
|
|
||||||
search_fields = ['number', 'name', 'place']
|
|
||||||
ordering = '-create_time'
|
|
||||||
perms_map = {
|
|
||||||
'get': '*', 'post': 'warehouse.create',
|
|
||||||
'put': 'warehouse.update', 'delete': 'warehouse.delete',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class PurchaseRequisitionViewSet(TicketMixin, CustomModelViewSet):
|
class PurchaseRequisitionViewSet(TicketMixin, CustomModelViewSet):
|
||||||
"""
|
"""
|
||||||
物资申购单
|
物资申购单
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
from django_filters import rest_framework as filters
|
from django_filters import rest_framework as filters
|
||||||
from apps.mtm.models import Goal, Material, Route, RouteMat, RoutePack, Process
|
from apps.mtm.models import Goal, Material, Route, RoutePack
|
||||||
from django.db.models.expressions import F
|
from django.db.models.expressions import F
|
||||||
from rest_framework.exceptions import ParseError
|
from rest_framework.exceptions import ParseError
|
||||||
from django.db.models import Sum, Q, Value, F, ExpressionWrapper, DecimalField
|
from django.db.models import Sum, Q, Value, F, ExpressionWrapper, DecimalField
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
|
|
||||||
class MaterialFilter(filters.FilterSet):
|
class MaterialFilter(filters.FilterSet):
|
||||||
tag = filters.CharFilter(method='filter_tag', label="low_inm:库存不足;todo:可用")
|
tag = filters.CharFilter(method='filter_tag', label="low_inm:库存不足")
|
||||||
process_todo = filters.CharFilter(method='filter_process_todo', label="process_todo:待处理")
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Material
|
model = Material
|
||||||
|
|
@ -26,14 +25,6 @@ class MaterialFilter(filters.FilterSet):
|
||||||
"route_material_out__mgroup": ["exact"],
|
"route_material_out__mgroup": ["exact"],
|
||||||
"count_safe": ["gte", "lte", "exact", "gt", "lt"]
|
"count_safe": ["gte", "lte", "exact", "gt", "lt"]
|
||||||
}
|
}
|
||||||
|
|
||||||
def filter_process_todo(self, queryset, name, value):
|
|
||||||
if value:
|
|
||||||
queryset = queryset.filter(
|
|
||||||
Q(id__in=RouteMat.objects.filter(route__process__id=value).values('material_id')) |
|
|
||||||
Q(id__in=Route.objects.filter(process__id=value).values('material_in_id'))
|
|
||||||
)
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
def filter_tag(self, queryset, name, value):
|
def filter_tag(self, queryset, name, value):
|
||||||
if value == 'low_inm':
|
if value == 'low_inm':
|
||||||
|
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('ofm', '0005_alter_vehicleuse_end_km'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='publicity',
|
|
||||||
name='final_file',
|
|
||||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='终版文件路径'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('ofm', '0006_publicity_final_file'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='lendingseal',
|
|
||||||
name='final_file',
|
|
||||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='终版文件路径'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
@ -101,7 +101,6 @@ class LendingSeal(CommonBDModel):
|
||||||
return_date = models.DateField('拟归还日期', blank=True, null=True)
|
return_date = models.DateField('拟归还日期', blank=True, null=True)
|
||||||
actual_return_date = models.DateField('实际归还日期', blank=True, null=True)
|
actual_return_date = models.DateField('实际归还日期', blank=True, null=True)
|
||||||
reason = models.CharField('借用理由', max_length=100, blank=True, null=True)
|
reason = models.CharField('借用理由', max_length=100, blank=True, null=True)
|
||||||
final_file = models.CharField('终版文件路径', max_length=200, null=True, blank=True)
|
|
||||||
ticket = models.ForeignKey('wf.ticket', verbose_name='关联工单',
|
ticket = models.ForeignKey('wf.ticket', verbose_name='关联工单',
|
||||||
on_delete=models.SET_NULL, related_name='seal_ticket', null=True, blank=True, db_constraint=False)
|
on_delete=models.SET_NULL, related_name='seal_ticket', null=True, blank=True, db_constraint=False)
|
||||||
note = models.TextField('备注', null=True, blank=True)
|
note = models.TextField('备注', null=True, blank=True)
|
||||||
|
|
@ -152,7 +151,6 @@ class Publicity(CommonBDModel):
|
||||||
secret_period = models.CharField('秘密期限', max_length=50, blank=True, null=True)
|
secret_period = models.CharField('秘密期限', max_length=50, blank=True, null=True)
|
||||||
dept_opinion_review = models.CharField('部门审查意见', max_length=100, blank=True, null=True)
|
dept_opinion_review = models.CharField('部门审查意见', max_length=100, blank=True, null=True)
|
||||||
publicity_opinion = models.CharField('宣传报道意见', max_length=100, blank=True, null=True)
|
publicity_opinion = models.CharField('宣传报道意见', max_length=100, blank=True, null=True)
|
||||||
final_file = models.CharField('终版文件路径', max_length=200, null=True, blank=True)
|
|
||||||
ticket = models.ForeignKey('wf.ticket', verbose_name='关联工单',
|
ticket = models.ForeignKey('wf.ticket', verbose_name='关联工单',
|
||||||
on_delete=models.SET_NULL, related_name='publicity_ticket', null=True, blank=True, db_constraint=False)
|
on_delete=models.SET_NULL, related_name='publicity_ticket', null=True, blank=True, db_constraint=False)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,164 +0,0 @@
|
||||||
# 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,7 +1,4 @@
|
||||||
from decimal import Decimal
|
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Sum
|
|
||||||
from apps.utils.models import CommonBModel, BaseModel, CommonBDModel, CommonADModel
|
from apps.utils.models import CommonBModel, BaseModel, CommonBDModel, CommonADModel
|
||||||
from apps.mtm.models import Material
|
from apps.mtm.models import Material
|
||||||
from apps.wf.models import Ticket
|
from apps.wf.models import Ticket
|
||||||
|
|
@ -74,9 +71,6 @@ class PuOrder(CommonBModel):
|
||||||
number = models.CharField('订单编号', max_length=20, null=True, blank=True)
|
number = models.CharField('订单编号', max_length=20, null=True, blank=True)
|
||||||
supplier = models.ForeignKey(
|
supplier = models.ForeignKey(
|
||||||
Supplier, verbose_name='供应商', on_delete=models.CASCADE)
|
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)
|
delivery_date = models.DateField('截止到货日期', null=True, blank=True)
|
||||||
submit_time = models.DateTimeField('提交时间', null=True, blank=True)
|
submit_time = models.DateTimeField('提交时间', null=True, blank=True)
|
||||||
submit_user = models.ForeignKey(
|
submit_user = models.ForeignKey(
|
||||||
|
|
@ -131,152 +125,4 @@ class QuotationApply(CommonADModel):
|
||||||
quoter = models.CharField(max_length=50,verbose_name="报价人", null=True, blank=True)
|
quoter = models.CharField(max_length=50,verbose_name="报价人", null=True, blank=True)
|
||||||
apply_date = models.DateField(verbose_name="申请日期",auto_now_add=True)
|
apply_date = models.DateField(verbose_name="申请日期",auto_now_add=True)
|
||||||
ticket = models.OneToOneField('wf.ticket', verbose_name='关联工单',
|
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,11 +1,9 @@
|
||||||
from decimal import Decimal
|
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from apps.utils.serializers import CustomModelSerializer
|
from apps.utils.serializers import CustomModelSerializer
|
||||||
from apps.utils.constants import EXCLUDE_FIELDS_DEPT, EXCLUDE_FIELDS_BASE, EXCLUDE_FIELDS
|
from apps.utils.constants import EXCLUDE_FIELDS_DEPT, EXCLUDE_FIELDS_BASE, EXCLUDE_FIELDS
|
||||||
from rest_framework.exceptions import ValidationError, ParseError
|
from rest_framework.exceptions import ValidationError, ParseError
|
||||||
|
|
||||||
from apps.pum.models import Supplier, PuPlan, PuPlanItem, PuOrder, PuOrderItem, SupplierAudit, QuotationApply, PuContract, PuContractRecord
|
from apps.pum.models import Supplier, PuPlan, PuPlanItem, PuOrder, PuOrderItem, SupplierAudit, QuotationApply
|
||||||
from apps.mtm.serializers import MaterialSerializer, MaterialSimpleSerializer
|
from apps.mtm.serializers import MaterialSerializer, MaterialSimpleSerializer
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from .services import PumService
|
from .services import PumService
|
||||||
|
|
@ -101,14 +99,6 @@ class PuOrderSerializer(CustomModelSerializer):
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
read_only_fields = EXCLUDE_FIELDS_DEPT + ['state', 'submit_time', 'total_price']
|
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):
|
def update(self, instance, validated_data):
|
||||||
validated_data.pop('supplier')
|
validated_data.pop('supplier')
|
||||||
if instance.state != PuOrder.PUORDER_CREATE:
|
if instance.state != PuOrder.PUORDER_CREATE:
|
||||||
|
|
@ -174,40 +164,4 @@ class QuotationApplySerializer(CustomModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = QuotationApply
|
model = QuotationApply
|
||||||
fields = "__all__"
|
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,178 +1,3 @@
|
||||||
from decimal import Decimal
|
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from apps.pum.models import Supplier
|
# Create your tests here.
|
||||||
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 django.urls import path, include
|
||||||
from rest_framework.routers import DefaultRouter
|
from rest_framework.routers import DefaultRouter
|
||||||
from apps.pum.views import (SupplierViewSet, PuPlanViewSet, PuPlanItemViewSet, PuOrderViewSet, PuOrderItemViewSet, SupplierAuditViewSet, QuotationApplyViewSet, PuContractViewSet, PuContractRecordViewSet)
|
from apps.pum.views import (SupplierViewSet, PuPlanViewSet, PuPlanItemViewSet, PuOrderViewSet, PuOrderItemViewSet, SupplierAuditViewSet, QuotationApplyViewSet)
|
||||||
# from apps.pum.views import SupplierViewSet, PuPlanViewSet, PuPlanItemViewSet, PuOrderViewSet, PuOrderItemViewSet
|
# from apps.pum.views import SupplierViewSet, PuPlanViewSet, PuPlanItemViewSet, PuOrderViewSet, PuOrderItemViewSet
|
||||||
|
|
||||||
API_BASE_URL = 'api/pum/'
|
API_BASE_URL = 'api/pum/'
|
||||||
|
|
@ -11,11 +11,9 @@ router.register('supplier', SupplierViewSet, basename='supplier')
|
||||||
router.register('supplieraudit', SupplierAuditViewSet, basename='supplieraudit')
|
router.register('supplieraudit', SupplierAuditViewSet, basename='supplieraudit')
|
||||||
router.register('pu_plan', PuPlanViewSet, basename='pu_plan')
|
router.register('pu_plan', PuPlanViewSet, basename='pu_plan')
|
||||||
router.register('pu_planitem', PuPlanItemViewSet, basename='pu_planitem')
|
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_order', PuOrderViewSet, basename='pu_order')
|
||||||
router.register('pu_orderitem', PuOrderItemViewSet, basename='pu_orderitem')
|
router.register('pu_orderitem', PuOrderItemViewSet, basename='pu_orderitem')
|
||||||
router.register('quotation', QuotationApplyViewSet, basename='quotation')
|
router.register('quotation', QuotationApplyViewSet, basename='quotation')
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path(API_BASE_URL, include(router.urls)),
|
path(API_BASE_URL, include(router.urls)),
|
||||||
]
|
]
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from apps.pum.models import Supplier, PuPlan, PuPlanItem, PuOrder, PuOrderItem, SupplierAudit, QuotationApply, PuContract, PuContractRecord
|
from apps.pum.models import Supplier, PuPlan, PuPlanItem, PuOrder, PuOrderItem, SupplierAudit, QuotationApply
|
||||||
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet, EuModelViewSet
|
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet, EuModelViewSet
|
||||||
from apps.pum.serializers import (SupplierSerializer, PuPlanSerializer, PuPlanItemSerializer, QuotationApplySerializer,
|
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.exceptions import ParseError, PermissionDenied
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
@ -223,51 +222,4 @@ class QuotationApplyViewSet(TicketMixin, CustomModelViewSet):
|
||||||
filterset_fields = ['product_name', 'customer_name','apply_date', 'quoter']
|
filterset_fields = ['product_name', 'customer_name','apply_date', 'quoter']
|
||||||
search_fields = ['product_name', 'customer_name','contact_person']
|
search_fields = ['product_name', 'customer_name','contact_person']
|
||||||
ordering = ['create_time']
|
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()
|
|
||||||
|
|
@ -1,112 +0,0 @@
|
||||||
# Generated by Django 4.2.27 on 2026-03-27 00:48
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
('system', '0007_alter_dept_create_by_alter_dept_third_info_and_more'),
|
|
||||||
('qm', '0055_alter_ftestitem_ftest'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='ptest',
|
|
||||||
name='conclusion',
|
|
||||||
field=models.TextField(blank=True, null=True, verbose_name='结论'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='ptest',
|
|
||||||
name='specification_zwnd',
|
|
||||||
field=models.TextField(blank=True, null=True, verbose_name='中温粘度规格'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='defect',
|
|
||||||
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='defect',
|
|
||||||
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='ftest',
|
|
||||||
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='ftest',
|
|
||||||
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='ftest',
|
|
||||||
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='ftestwork',
|
|
||||||
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='ftestwork',
|
|
||||||
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='ftestwork',
|
|
||||||
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='ptest',
|
|
||||||
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='ptest',
|
|
||||||
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='qct',
|
|
||||||
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='qct',
|
|
||||||
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='quastat',
|
|
||||||
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='quastat',
|
|
||||||
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='quastat',
|
|
||||||
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='testitem',
|
|
||||||
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='testitem',
|
|
||||||
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='最后编辑人'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
@ -449,5 +449,3 @@ class Ptest(CommonAModel):
|
||||||
val_pzxs = models.FloatField(
|
val_pzxs = models.FloatField(
|
||||||
'膨胀系数', help_text='30-300℃', null=True, blank=True)
|
'膨胀系数', help_text='30-300℃', null=True, blank=True)
|
||||||
val_zgwd = models.FloatField('升至最高温度', null=True, blank=True)
|
val_zgwd = models.FloatField('升至最高温度', null=True, blank=True)
|
||||||
specification_zwnd = models.TextField('中温粘度规格', null=True, blank=True)
|
|
||||||
conclusion = models.TextField('结论', null=True, blank=True)
|
|
||||||
|
|
|
||||||
|
|
@ -148,7 +148,7 @@ class RemployeeCreateSerializer(CustomModelSerializer):
|
||||||
if Remployee.objects.filter(id_number=validated_data['id_number'], rparty=validated_data['rparty']).exists():
|
if Remployee.objects.filter(id_number=validated_data['id_number'], rparty=validated_data['rparty']).exists():
|
||||||
raise ValidationError('该成员已存在')
|
raise ValidationError('该成员已存在')
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
if getattr(settings, 'DAHUA_ENABLED', False):
|
if settings.DAHUA_ENABLED:
|
||||||
dhClient.request(**dhapis['person_img_upload'], file_path_rela=validated_data['photo'])
|
dhClient.request(**dhapis['person_img_upload'], file_path_rela=validated_data['photo'])
|
||||||
return super().create(validated_data)
|
return super().create(validated_data)
|
||||||
|
|
||||||
|
|
@ -161,7 +161,7 @@ class RemployeeUpdateSerializer(CustomModelSerializer):
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
if getattr(settings, 'DAHUA_ENABLED', False):
|
if settings.DAHUA_ENABLED:
|
||||||
dhClient.request(**dhapis['person_img_upload'], file_path_rela=validated_data['photo'])
|
dhClient.request(**dhapis['person_img_upload'], file_path_rela=validated_data['photo'])
|
||||||
return super().update(instance, validated_data)
|
return super().update(instance, validated_data)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,122 +0,0 @@
|
||||||
# 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,7 +1,4 @@
|
||||||
from decimal import Decimal
|
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Sum
|
|
||||||
from apps.utils.models import CommonBModel, BaseModel, CommonBDModel
|
from apps.utils.models import CommonBModel, BaseModel, CommonBDModel
|
||||||
from apps.mtm.models import Material
|
from apps.mtm.models import Material
|
||||||
|
|
||||||
|
|
@ -27,43 +24,16 @@ class Customer(CommonBModel):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
class Contract(CommonBDModel):
|
class Contract(CommonBModel):
|
||||||
"""
|
"""
|
||||||
TN:合同信息
|
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)
|
name = models.CharField('合同名称', max_length=100)
|
||||||
number = models.CharField('合同编号', max_length=100, unique=True)
|
number = models.CharField('合同编号', max_length=100, unique=True)
|
||||||
amount = models.IntegerField('合同金额', default=0)
|
amount = models.IntegerField('合同金额', default=0)
|
||||||
customer = models.ForeignKey(Customer, verbose_name='关联客户',
|
customer = models.ForeignKey(Customer, verbose_name='关联客户',
|
||||||
on_delete=models.CASCADE, related_name='contract_customer')
|
on_delete=models.CASCADE, related_name='contract_customer')
|
||||||
sign_date = models.DateField('签订日期')
|
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)
|
description = models.CharField('描述', max_length=200, blank=True, null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
@ -73,53 +43,6 @@ class Contract(CommonBDModel):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
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):
|
class Order(CommonBModel):
|
||||||
"""
|
"""
|
||||||
|
|
@ -164,58 +87,3 @@ class OrderItem(BaseModel):
|
||||||
delivered_count = models.PositiveIntegerField('已交货数量', default=0)
|
delivered_count = models.PositiveIntegerField('已交货数量', default=0)
|
||||||
utask = models.ForeignKey('pm.utask', verbose_name='关联生产大任务',
|
utask = models.ForeignKey('pm.utask', verbose_name='关联生产大任务',
|
||||||
on_delete=models.SET_NULL, null=True, blank=True)
|
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,8 +1,6 @@
|
||||||
from decimal import Decimal
|
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from apps.utils.serializers import CustomModelSerializer
|
from apps.utils.serializers import CustomModelSerializer
|
||||||
from apps.sam.models import Customer, Contract, Order, OrderItem, ContractRecord
|
from apps.sam.models import Customer, Contract, Order, OrderItem
|
||||||
from apps.utils.constants import EXCLUDE_FIELDS, EXCLUDE_FIELDS_BASE
|
from apps.utils.constants import EXCLUDE_FIELDS, EXCLUDE_FIELDS_BASE
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
from apps.mtm.serializers import MaterialSerializer
|
from apps.mtm.serializers import MaterialSerializer
|
||||||
|
|
@ -83,29 +81,3 @@ class OrderItemSerializer(CustomModelSerializer):
|
||||||
validated_data.pop('product', None)
|
validated_data.pop('product', None)
|
||||||
validated_data.pop('order', None)
|
validated_data.pop('order', None)
|
||||||
return super().update(instance, validated_data)
|
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,178 +1,3 @@
|
||||||
from decimal import Decimal
|
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from apps.sam.models import Contract, Customer
|
# Create your tests here.
|
||||||
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 django.urls import path, include
|
||||||
from rest_framework.routers import DefaultRouter
|
from rest_framework.routers import DefaultRouter
|
||||||
from apps.sam.views import (CustomerViewSet, ContractViewSet, OrderViewSet, OrderItemViewSet, ContractRecordViewSet)
|
from apps.sam.views import (CustomerViewSet, ContractViewSet, OrderViewSet, OrderItemViewSet)
|
||||||
|
|
||||||
API_BASE_URL = 'api/sam/'
|
API_BASE_URL = 'api/sam/'
|
||||||
HTML_BASE_URL = 'dhtml/sam/'
|
HTML_BASE_URL = 'dhtml/sam/'
|
||||||
|
|
@ -8,9 +8,8 @@ HTML_BASE_URL = 'dhtml/sam/'
|
||||||
router = DefaultRouter()
|
router = DefaultRouter()
|
||||||
router.register('customer', CustomerViewSet, basename='customer')
|
router.register('customer', CustomerViewSet, basename='customer')
|
||||||
router.register('contract', ContractViewSet, basename='contract')
|
router.register('contract', ContractViewSet, basename='contract')
|
||||||
router.register('contract_record', ContractRecordViewSet, basename='contract_record')
|
|
||||||
router.register('order', OrderViewSet, basename='order')
|
router.register('order', OrderViewSet, basename='order')
|
||||||
router.register('orderitem', OrderItemViewSet, basename='orderitem')
|
router.register('orderitem', OrderItemViewSet, basename='orderitem')
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path(API_BASE_URL, include(router.urls)),
|
path(API_BASE_URL, include(router.urls)),
|
||||||
]
|
]
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet
|
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet
|
||||||
from apps.sam.models import Customer, Contract, Order, OrderItem, ContractRecord
|
from apps.sam.models import Customer, Contract, Order, OrderItem
|
||||||
from apps.sam.serializers import CustomerSerializer, ContractSerializer, OrderSerializer, OrderItemSerializer, ContractRecordSerializer
|
from apps.sam.serializers import CustomerSerializer, ContractSerializer, OrderSerializer, OrderItemSerializer
|
||||||
from rest_framework.exceptions import ParseError
|
from rest_framework.exceptions import ParseError
|
||||||
from rest_framework.mixins import ListModelMixin, CreateModelMixin, DestroyModelMixin
|
from rest_framework.mixins import ListModelMixin, CreateModelMixin, DestroyModelMixin
|
||||||
from apps.utils.mixins import BulkCreateModelMixin
|
from apps.utils.mixins import BulkCreateModelMixin
|
||||||
|
|
@ -46,7 +46,6 @@ class ContractViewSet(CustomModelViewSet):
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
if Order.objects.filter(contract=instance).exists():
|
if Order.objects.filter(contract=instance).exists():
|
||||||
raise ParseError('该合同存在订单不可删除')
|
raise ParseError('该合同存在订单不可删除')
|
||||||
instance.delete()
|
|
||||||
|
|
||||||
|
|
||||||
class OrderViewSet(CustomModelViewSet):
|
class OrderViewSet(CustomModelViewSet):
|
||||||
|
|
@ -107,32 +106,3 @@ class OrderItemViewSet(ListModelMixin, CreateModelMixin, DestroyModelMixin, Cust
|
||||||
if instance.order.state != Order.ORDER_CREATE:
|
if instance.order.state != Order.ORDER_CREATE:
|
||||||
raise ParseError('该订单状态下不可删除')
|
raise ParseError('该订单状态下不可删除')
|
||||||
return super().perform_destroy(instance)
|
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()
|
|
||||||
|
|
|
||||||
|
|
@ -464,30 +464,6 @@ class WfService(object):
|
||||||
|
|
||||||
cls.task_ticket(ticket=ticket)
|
cls.task_ticket(ticket=ticket)
|
||||||
|
|
||||||
# 自动跳过连续相同审批人:如果下一个节点的处理人与当前处理人相同,自动执行同意操作
|
|
||||||
if (handler is not None
|
|
||||||
and destination_state.type not in (State.STATE_TYPE_START, State.STATE_TYPE_END)
|
|
||||||
and transition.attribute_type == Transition.TRANSITION_ATTRIBUTE_TYPE_ACCEPT
|
|
||||||
and destination_participant_type == State.PARTICIPANT_TYPE_PERSONAL
|
|
||||||
and str(destination_participant) == str(handler.id)):
|
|
||||||
# 查找下一个状态的"同意"流转
|
|
||||||
next_transition = Transition.objects.filter(
|
|
||||||
is_deleted=False,
|
|
||||||
source_state=destination_state,
|
|
||||||
attribute_type=Transition.TRANSITION_ATTRIBUTE_TYPE_ACCEPT
|
|
||||||
).first()
|
|
||||||
if next_transition:
|
|
||||||
import logging
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
logger.info(f'工单{ticket.sn}: 连续节点审批人相同({handler.username}),自动跳过节点[{destination_state.name}]')
|
|
||||||
ticket = cls.handle_ticket(
|
|
||||||
ticket=ticket,
|
|
||||||
transition=next_transition,
|
|
||||||
new_ticket_data=ticket.ticket_data,
|
|
||||||
handler=handler,
|
|
||||||
suggestion='(系统自动审批:与上一节点审批人相同)',
|
|
||||||
)
|
|
||||||
|
|
||||||
return ticket
|
return ticket
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
# Generated by Django 4.2.27 on 2026-03-26 08:56
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('wpm', '0127_handoverb_oinfo_json_alter_attlog_create_by_and_more'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='wmaterial',
|
|
||||||
name='is_manual',
|
|
||||||
field=models.BooleanField(default=False, verbose_name='手动创建'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("wpm", "0128_add_is_manual_to_wmaterial"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="mlogbdefect",
|
|
||||||
name="is_inherited",
|
|
||||||
field=models.BooleanField(default=False, verbose_name="是否继承"),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
@ -126,21 +126,6 @@ class WMaterial(CommonBDModel):
|
||||||
batch_ofrom = models.TextField('原料批次号', null=True, blank=True)
|
batch_ofrom = models.TextField('原料批次号', null=True, blank=True)
|
||||||
material_ofrom = models.ForeignKey(Material, verbose_name='原料物料', on_delete=models.SET_NULL, null=True, blank=True, related_name='wm_mofrom')
|
material_ofrom = models.ForeignKey(Material, verbose_name='原料物料', on_delete=models.SET_NULL, null=True, blank=True, related_name='wm_mofrom')
|
||||||
number_from = models.TextField("来源于个号", null=True, blank=True)
|
number_from = models.TextField("来源于个号", null=True, blank=True)
|
||||||
is_manual = models.BooleanField('手动创建', default=False)
|
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
|
||||||
if not self.is_manual:
|
|
||||||
raise ParseError('只能删除手动创建的车间库存')
|
|
||||||
checks = [
|
|
||||||
(self.mlogb_set.exists, '存在关联的生产明细'),
|
|
||||||
(self.handoverb_wm.exists, '存在关联的交接明细'),
|
|
||||||
(self.ftestwork_set.exists, '存在关联的检验记录'),
|
|
||||||
(self.wm_mioitem.exists, '存在关联的出入库明细'),
|
|
||||||
]
|
|
||||||
for check, msg in checks:
|
|
||||||
if check():
|
|
||||||
raise ParseError(msg)
|
|
||||||
super().delete(*args, **kwargs)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def belong_dept_or_mgroup_id(self):
|
def belong_dept_or_mgroup_id(self):
|
||||||
|
|
@ -535,51 +520,12 @@ class Mlogb(BaseModel):
|
||||||
if mlog and cal_mlog:
|
if mlog and cal_mlog:
|
||||||
mlog.cal_mlog_count_from_mlogb()
|
mlog.cal_mlog_count_from_mlogb()
|
||||||
|
|
||||||
def get_default_inherited_defect(self):
|
|
||||||
if self.material_out is None or self.material_out.tracking != Material.MA_TRACKING_BATCH:
|
|
||||||
return None
|
|
||||||
if self.mlogb_from_id and self.mlogb_from and self.mlogb_from.wm_in_id:
|
|
||||||
return self.mlogb_from.wm_in.defect
|
|
||||||
if self.wm_in_id and self.wm_in:
|
|
||||||
return self.wm_in.defect
|
|
||||||
if self.mlog and self.mlog.wm_in_id:
|
|
||||||
return self.mlog.wm_in.defect
|
|
||||||
return None
|
|
||||||
|
|
||||||
def has_legacy_defect_count(self):
|
|
||||||
return any(getattr(self, f.name) > 0 for f in Mlogb._meta.fields if 'count_n_' in f.name)
|
|
||||||
|
|
||||||
def sync_inherited_defect(self, cal_count=True):
|
|
||||||
inherited_qs = MlogbDefect.objects.filter(mlogb=self, is_inherited=True)
|
|
||||||
if MlogbDefect.objects.filter(mlogb=self, is_inherited=False).exists() or self.has_legacy_defect_count():
|
|
||||||
inherited_qs.delete()
|
|
||||||
return
|
|
||||||
|
|
||||||
defect = self.get_default_inherited_defect()
|
|
||||||
if defect is None:
|
|
||||||
inherited_qs.delete()
|
|
||||||
return
|
|
||||||
|
|
||||||
count = self.count_real
|
|
||||||
inherited, _ = MlogbDefect.objects.get_or_create(
|
|
||||||
mlogb=self,
|
|
||||||
defect=defect,
|
|
||||||
is_inherited=True,
|
|
||||||
)
|
|
||||||
inherited.count = count
|
|
||||||
inherited.count_has = count
|
|
||||||
inherited.save(update_fields=["count", "count_has"])
|
|
||||||
inherited_qs.exclude(id=inherited.id).delete()
|
|
||||||
if cal_count:
|
|
||||||
self.cal_count_notok(cal_mlog=False)
|
|
||||||
|
|
||||||
class MlogbDefect(BaseModel):
|
class MlogbDefect(BaseModel):
|
||||||
"""TN: 生成记录的缺陷记录"""
|
"""TN: 生成记录的缺陷记录"""
|
||||||
mlogb = models.ForeignKey(Mlogb, verbose_name='生产记录', on_delete=models.CASCADE)
|
mlogb = models.ForeignKey(Mlogb, verbose_name='生产记录', on_delete=models.CASCADE)
|
||||||
defect = models.ForeignKey("qm.Defect", verbose_name='缺陷', on_delete=models.CASCADE, null=True, blank=True)
|
defect = models.ForeignKey("qm.Defect", verbose_name='缺陷', on_delete=models.CASCADE, null=True, blank=True)
|
||||||
count = models.DecimalField('数量', default=0, max_digits=11, decimal_places=1)
|
count = models.DecimalField('数量', default=0, max_digits=11, decimal_places=1)
|
||||||
count_has = models.DecimalField('含有该缺陷的数量', default=0, max_digits=11, decimal_places=1)
|
count_has = models.DecimalField('含有该缺陷的数量', default=0, max_digits=11, decimal_places=1)
|
||||||
is_inherited = models.BooleanField("是否继承", default=False)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_defect_qs(cls, ftype="all"):
|
def get_defect_qs(cls, ftype="all"):
|
||||||
|
|
@ -964,4 +910,4 @@ class BatchLog(BaseModel):
|
||||||
"last_batch": last["batch"],
|
"last_batch": last["batch"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from apps.wpm.models import BatchSt
|
from apps.wpm.models import BatchSt
|
||||||
import logging
|
import logging
|
||||||
from apps.wpm.models import Mlogb, Mlogbw, MlogbDefect, MlogUser
|
from apps.wpm.models import Mlogb, Mlogbw, MlogbDefect
|
||||||
from apps.mtm.models import Mgroup
|
from apps.mtm.models import Mgroup
|
||||||
import decimal
|
import decimal
|
||||||
from django.db.models import Sum
|
from django.db.models import Sum
|
||||||
|
|
@ -27,7 +27,7 @@ def main(batch: str, mgroup_obj:Mgroup=None):
|
||||||
mgroup_name = mgroup.name
|
mgroup_name = mgroup.name
|
||||||
mlogb1_qs = Mlogb.objects.filter(mlog__submit_time__isnull=False,
|
mlogb1_qs = Mlogb.objects.filter(mlog__submit_time__isnull=False,
|
||||||
material_out__isnull=False, mlog__mgroup=mgroup,
|
material_out__isnull=False, mlog__mgroup=mgroup,
|
||||||
mlog__is_fix=False, batch=batch, need_inout=True).order_by("mlog__submit_time")
|
mlog__is_fix=False, batch=batch, need_inout=True)
|
||||||
if mlogb1_qs.exists():
|
if mlogb1_qs.exists():
|
||||||
data[f"{mgroup_name}_日期"] = []
|
data[f"{mgroup_name}_日期"] = []
|
||||||
data[f"{mgroup_name}_操作人"] = []
|
data[f"{mgroup_name}_操作人"] = []
|
||||||
|
|
@ -38,7 +38,6 @@ def main(batch: str, mgroup_obj:Mgroup=None):
|
||||||
data[f"{mgroup_name}_count_ok_full"] = 0
|
data[f"{mgroup_name}_count_ok_full"] = 0
|
||||||
data[f"{mgroup_name}_count_pn_jgqbl"] = 0
|
data[f"{mgroup_name}_count_pn_jgqbl"] = 0
|
||||||
mlogb_q_ids = []
|
mlogb_q_ids = []
|
||||||
cal_mlog = []
|
|
||||||
for item in mlogb1_qs:
|
for item in mlogb1_qs:
|
||||||
# 找到对应的输入
|
# 找到对应的输入
|
||||||
mlogb_from:Mlogb = item.mlogb_from
|
mlogb_from:Mlogb = item.mlogb_from
|
||||||
|
|
@ -52,13 +51,6 @@ def main(batch: str, mgroup_obj:Mgroup=None):
|
||||||
data[f"{mgroup_name}_count_pn_jgqbl"] += 0
|
data[f"{mgroup_name}_count_pn_jgqbl"] += 0
|
||||||
if item.mlog.handle_user:
|
if item.mlog.handle_user:
|
||||||
data[f"{mgroup_name}_操作人"].append(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:
|
if item.mlog.handle_date:
|
||||||
data[f"{mgroup_name}_日期"].append(item.mlog.handle_date)
|
data[f"{mgroup_name}_日期"].append(item.mlog.handle_date)
|
||||||
data[f"{mgroup_name}_count_real"] += item.count_real
|
data[f"{mgroup_name}_count_real"] += item.count_real
|
||||||
|
|
|
||||||
|
|
@ -207,32 +207,15 @@ class WMaterialSerializer(CustomModelSerializer):
|
||||||
ret['count_canhandover'] = str(Decimal(ret['count']) - Decimal(ret['count_handovering']))
|
ret['count_canhandover'] = str(Decimal(ret['count']) - Decimal(ret['count_handovering']))
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
class WMaterialCreateSerializer(CustomModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = WMaterial
|
|
||||||
fields = ['material', 'count', 'batch', 'mgroup']
|
|
||||||
extra_kwargs = {
|
|
||||||
'material': {'required': True},
|
|
||||||
'count': {'required': True},
|
|
||||||
'batch': {'required': True},
|
|
||||||
'mgroup': {'required': True, 'allow_null': False},
|
|
||||||
}
|
|
||||||
|
|
||||||
def validate(self, attrs):
|
|
||||||
attrs['belong_dept'] = attrs['mgroup'].belong_dept
|
|
||||||
return attrs
|
|
||||||
|
|
||||||
|
|
||||||
class MlogbDefectSerializer(CustomModelSerializer):
|
class MlogbDefectSerializer(CustomModelSerializer):
|
||||||
defect_name = serializers.CharField(source="defect.name", read_only=True)
|
defect_name = serializers.CharField(source="defect.name", read_only=True)
|
||||||
defect_okcate = serializers.CharField(source="defect.okcate", read_only=True)
|
defect_okcate = serializers.CharField(source="defect.okcate", read_only=True)
|
||||||
class Meta:
|
class Meta:
|
||||||
model = MlogbDefect
|
model = MlogbDefect
|
||||||
fields = ["id", "defect_name", "count", "mlogb", "defect", "defect_okcate", "count_has", "is_inherited"]
|
fields = ["id", "defect_name", "count", "mlogb", "defect", "defect_okcate", "count_has"]
|
||||||
read_only_fields = EXCLUDE_FIELDS_BASE + ["mlogb"]
|
read_only_fields = EXCLUDE_FIELDS_BASE + ["mlogb"]
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'count_has': {'required': False},
|
'count_has': {'required': False},
|
||||||
'is_inherited': {'required': False},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
|
|
@ -478,7 +461,6 @@ class MlogSerializer(CustomModelSerializer):
|
||||||
if mlogb_defect_objects:
|
if mlogb_defect_objects:
|
||||||
MlogbDefect.objects.bulk_create(mlogb_defect_objects)
|
MlogbDefect.objects.bulk_create(mlogb_defect_objects)
|
||||||
mlogb.cal_count_notok(cal_mlog=False)
|
mlogb.cal_count_notok(cal_mlog=False)
|
||||||
mlogb.sync_inherited_defect(cal_count=True)
|
|
||||||
instance.cal_mlog_count_from_mlogb()
|
instance.cal_mlog_count_from_mlogb()
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|
@ -569,7 +551,7 @@ class MlogSerializer(CustomModelSerializer):
|
||||||
mox.save()
|
mox.save()
|
||||||
Mlogb.objects.filter(mlog=instance, material_out__isnull=False).exclude(id=mox.id).delete()
|
Mlogb.objects.filter(mlog=instance, material_out__isnull=False).exclude(id=mox.id).delete()
|
||||||
if need_mdefect:
|
if need_mdefect:
|
||||||
MlogbDefect.objects.filter(mlogb__mlog=instance, is_inherited=False).delete()
|
MlogbDefect.objects.filter(mlogb__mlog=instance).delete()
|
||||||
mlogb_defect_objects = [
|
mlogb_defect_objects = [
|
||||||
MlogbDefect(**{**item, "mlogb": mox, "id": idWorker.get_id()})
|
MlogbDefect(**{**item, "mlogb": mox, "id": idWorker.get_id()})
|
||||||
for item in mlogdefect if item["count"] > 0
|
for item in mlogdefect if item["count"] > 0
|
||||||
|
|
@ -577,7 +559,6 @@ class MlogSerializer(CustomModelSerializer):
|
||||||
if mlogb_defect_objects:
|
if mlogb_defect_objects:
|
||||||
MlogbDefect.objects.bulk_create(mlogb_defect_objects)
|
MlogbDefect.objects.bulk_create(mlogb_defect_objects)
|
||||||
mox.cal_count_notok(cal_mlog=False)
|
mox.cal_count_notok(cal_mlog=False)
|
||||||
mox.sync_inherited_defect(cal_count=True)
|
|
||||||
instance.cal_mlog_count_from_mlogb()
|
instance.cal_mlog_count_from_mlogb()
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|
@ -1114,8 +1095,6 @@ class MlogbOutUpdateSerializer(CustomModelSerializer):
|
||||||
if mlogb_defect_objects:
|
if mlogb_defect_objects:
|
||||||
MlogbDefect.objects.bulk_create(mlogb_defect_objects)
|
MlogbDefect.objects.bulk_create(mlogb_defect_objects)
|
||||||
ins.cal_count_notok(cal_mlog=False)
|
ins.cal_count_notok(cal_mlog=False)
|
||||||
elif ins.material_out.tracking == Material.MA_TRACKING_BATCH:
|
|
||||||
ins.sync_inherited_defect(cal_count=True)
|
|
||||||
return ins
|
return ins
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
|
|
@ -1600,4 +1579,4 @@ class MlogQuickSerializer(serializers.Serializer):
|
||||||
|
|
||||||
class BatchChangeSerializer(serializers.Serializer):
|
class BatchChangeSerializer(serializers.Serializer):
|
||||||
old_batch = serializers.CharField(label="原批号")
|
old_batch = serializers.CharField(label="原批号")
|
||||||
new_batch = serializers.CharField(label="新批号")
|
new_batch = serializers.CharField(label="新批号")
|
||||||
|
|
@ -11,8 +11,7 @@ from apps.system.models import User
|
||||||
|
|
||||||
from apps.mtm.models import Material, Process, Route, Mgroup, RoutePack, RouteMat
|
from apps.mtm.models import Material, Process, Route, Mgroup, RoutePack, RouteMat
|
||||||
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet
|
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet
|
||||||
from rest_framework.mixins import DestroyModelMixin
|
from apps.utils.mixins import CustomListModelMixin, BulkCreateModelMixin, ComplexQueryMixin, BulkDestroyModelMixin, BulkUpdateModelMixin
|
||||||
from apps.utils.mixins import CustomListModelMixin, CustomCreateModelMixin, BulkCreateModelMixin, ComplexQueryMixin, BulkDestroyModelMixin, BulkUpdateModelMixin
|
|
||||||
|
|
||||||
from .filters import StLogFilter, SfLogFilter, WMaterialFilter, MlogFilter, HandoverFilter, MlogbFilter, BatchStFilter, MlogbwFilter
|
from .filters import StLogFilter, SfLogFilter, WMaterialFilter, MlogFilter, HandoverFilter, MlogbFilter, BatchStFilter, MlogbwFilter
|
||||||
from .models import SfLog, SfLogExp, StLog, WMaterial, Mlog, Handover, Mlogb, Mlogbw, AttLog, OtherLog, Fmlog, BatchSt, MlogbDefect, MlogUser, BatchLog, Handoverb
|
from .models import SfLog, SfLogExp, StLog, WMaterial, Mlog, Handover, Mlogb, Mlogbw, AttLog, OtherLog, Fmlog, BatchSt, MlogbDefect, MlogUser, BatchLog, Handoverb
|
||||||
|
|
@ -21,7 +20,6 @@ from .serializers import (
|
||||||
SfLogSerializer,
|
SfLogSerializer,
|
||||||
StLogSerializer,
|
StLogSerializer,
|
||||||
WMaterialSerializer,
|
WMaterialSerializer,
|
||||||
WMaterialCreateSerializer,
|
|
||||||
MlogRevertSerializer,
|
MlogRevertSerializer,
|
||||||
MlogSerializer,
|
MlogSerializer,
|
||||||
MlogRelatedSerializer,
|
MlogRelatedSerializer,
|
||||||
|
|
@ -162,19 +160,16 @@ class SfLogExpViewSet(CustomListModelMixin, BulkUpdateModelMixin, CustomGenericV
|
||||||
filterset_fields = ["sflog", "stlog"]
|
filterset_fields = ["sflog", "stlog"]
|
||||||
|
|
||||||
|
|
||||||
class WMaterialViewSet(CustomCreateModelMixin, DestroyModelMixin, CustomListModelMixin, CustomGenericViewSet):
|
class WMaterialViewSet(CustomListModelMixin, CustomGenericViewSet):
|
||||||
"""
|
"""
|
||||||
list: 车间库存
|
list: 车间库存
|
||||||
create: 手动创建车间库存
|
|
||||||
destroy: 删除手动创建的车间库存
|
|
||||||
|
|
||||||
车间库存
|
车间库存
|
||||||
"""
|
"""
|
||||||
|
|
||||||
perms_map = {"get": "*", "post": "wmaterial.create", "delete": "wmaterial.delete"}
|
perms_map = {"get": "*"}
|
||||||
queryset = WMaterial.objects.filter(count__gt=0)
|
queryset = WMaterial.objects.filter(count__gt=0)
|
||||||
serializer_class = WMaterialSerializer
|
serializer_class = WMaterialSerializer
|
||||||
create_serializer_class = WMaterialCreateSerializer
|
|
||||||
select_related_fields = ["material", "belong_dept", "material__process", "supplier"]
|
select_related_fields = ["material", "belong_dept", "material__process", "supplier"]
|
||||||
search_fields = ["material__name", "material__specification", "batch", "material__model", "defect__name", "notok_sign"]
|
search_fields = ["material__name", "material__specification", "batch", "material__model", "defect__name", "notok_sign"]
|
||||||
filterset_class = WMaterialFilter
|
filterset_class = WMaterialFilter
|
||||||
|
|
@ -191,12 +186,6 @@ class WMaterialViewSet(CustomCreateModelMixin, DestroyModelMixin, CustomListMode
|
||||||
return queryset
|
return queryset
|
||||||
return queryset.exclude(state=WMaterial.WM_SCRAP)
|
return queryset.exclude(state=WMaterial.WM_SCRAP)
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
|
||||||
serializer.save(create_by=self.request.user, is_manual=True)
|
|
||||||
|
|
||||||
def perform_destroy(self, instance):
|
|
||||||
instance.delete()
|
|
||||||
|
|
||||||
@action(methods=["post"], detail=False, perms_map={"post": "*"}, serializer_class=DeptBatchSerializer)
|
@action(methods=["post"], detail=False, perms_map={"post": "*"}, serializer_class=DeptBatchSerializer)
|
||||||
def batchs(self, request):
|
def batchs(self, request):
|
||||||
"""获取车间的批次号(废弃)
|
"""获取车间的批次号(废弃)
|
||||||
|
|
@ -977,8 +966,6 @@ class MlogbInViewSet(BulkCreateModelMixin, BulkUpdateModelMixin, BulkDestroyMode
|
||||||
Mlogbw.objects.get_or_create(number=numberx, mlogb=mlogbout)
|
Mlogbw.objects.get_or_create(number=numberx, mlogb=mlogbout)
|
||||||
else:
|
else:
|
||||||
raise ParseError("不支持生成产出物料!")
|
raise ParseError("不支持生成产出物料!")
|
||||||
for mlogbout in Mlogb.objects.filter(mlog=mlog, material_out__isnull=False):
|
|
||||||
mlogbout.sync_inherited_defect(cal_count=True)
|
|
||||||
mlog.cal_mlog_count_from_mlogb()
|
mlog.cal_mlog_count_from_mlogb()
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,3 @@
|
||||||
## 3.1.2026033008
|
|
||||||
- feat: 新增功能
|
|
||||||
- WMaterialViewSet 添加手动来料创建和删除接口 [caoqianming]
|
|
||||||
- Ptest添加中温粘度规格和结论字段 [caoqianming]
|
|
||||||
- WMaterialViewSet 添加手动创建和删除接口 [caoqianming]
|
|
||||||
- 修改光芯OA审批的BUG与新增导出功能 [TianyangZhang]
|
|
||||||
- 恢复ichat 功能和 defaut 下的文件 [TianyangZhang]
|
|
||||||
- fix: 问题修复
|
|
||||||
- WMaterialCreateSerializer 所有字段设为必填 [caoqianming]
|
|
||||||
## 3.1.2026031316
|
## 3.1.2026031316
|
||||||
- feat: 新增功能
|
- feat: 新增功能
|
||||||
- 删除-ichat [TianyangZhang]
|
- 删除-ichat [TianyangZhang]
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 6.9 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -35,7 +35,7 @@ sys.path.insert(0, os.path.join(BASE_DIR, 'apps'))
|
||||||
ALLOWED_HOSTS = ['*']
|
ALLOWED_HOSTS = ['*']
|
||||||
|
|
||||||
SYS_NAME = '星途工厂综合管理系统'
|
SYS_NAME = '星途工厂综合管理系统'
|
||||||
SYS_VERSION = '3.1.2026033008'
|
SYS_VERSION = '3.1.2026031316'
|
||||||
X_FRAME_OPTIONS = 'SAMEORIGIN'
|
X_FRAME_OPTIONS = 'SAMEORIGIN'
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
@ -63,7 +63,7 @@ INSTALLED_APPS = [
|
||||||
'apps.wf',
|
'apps.wf',
|
||||||
'apps.ecm',
|
'apps.ecm',
|
||||||
'apps.hrm',
|
'apps.hrm',
|
||||||
#'apps.ichat',
|
'apps.ichat',
|
||||||
'apps.am',
|
'apps.am',
|
||||||
'apps.vm',
|
'apps.vm',
|
||||||
'apps.rpm',
|
'apps.rpm',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue