Compare commits
23 Commits
3.1.202603
...
master
| Author | SHA1 | Date |
|---|---|---|
|
|
7e9b559723 | |
|
|
40ebf720f7 | |
|
|
46c9eba306 | |
|
|
1f7e17b6c1 | |
|
|
d2581d8500 | |
|
|
881e16626d | |
|
|
d177cae663 | |
|
|
13c9d8a258 | |
|
|
3129cc0e54 | |
|
|
4d2a88ab1e | |
|
|
32b0313349 | |
|
|
e2b1f266aa | |
|
|
c2ee88d2bf | |
|
|
7577a46900 | |
|
|
f6d934bbb1 | |
|
|
b6b79da3b1 | |
|
|
48305ed6fb | |
|
|
949620809a | |
|
|
cafecd4d4a | |
|
|
a111f493e1 | |
|
|
3c931040cf | |
|
|
159b644126 | |
|
|
a82405e451 |
|
|
@ -0,0 +1,65 @@
|
|||
---
|
||||
description: 生成 changelog、更新 SYS_VERSION、打 tag 并推到远端
|
||||
---
|
||||
|
||||
发布一个新版本,遵循以下既定流程,**严格按顺序执行**,不要省略或调整步骤。
|
||||
|
||||
## 步骤
|
||||
|
||||
### 1. 生成 changelog 与版本号
|
||||
|
||||
执行:
|
||||
|
||||
```bash
|
||||
bash update_changelog.sh
|
||||
```
|
||||
|
||||
脚本会:
|
||||
- 在 `changelog.md` 顶部写入 `## 3.1.YYYYMMDDHH`,并附上自上一个 tag 起的 feat / fix / other 提交。
|
||||
- 终端输出形如 `当前版本号: 3.1.YYYYMMDDHH (请手动修改)`。
|
||||
|
||||
从输出中**抓取版本号**(即 `3.1.YYYYMMDDHH`),后续步骤记作 `<VER>`。
|
||||
|
||||
### 2. 更新 `server/settings.py` 的 `SYS_VERSION`
|
||||
|
||||
把 `SYS_VERSION = '<旧版本>'` 改为 `SYS_VERSION = '<VER>'`。
|
||||
|
||||
> 该字段是项目里唯一权威的版本号,前端 `/swagger` 也读它。
|
||||
|
||||
### 3. 检查 changelog 顶部内容
|
||||
|
||||
`Read` 一下 `changelog.md` 头 ~20 行,确认:
|
||||
- 顶部是 `## <VER>`
|
||||
- 自上次 tag 之后的提交都被分类列出。
|
||||
- 如有内容明显不对(例如重复段、无关分类),先与用户确认,不要直接打 tag。
|
||||
|
||||
### 4. 提交 + 打 tag + 推送
|
||||
|
||||
参考过往风格(commit message 用 `release: <VER>`,tag 名直接用版本号、无 `v` 前缀):
|
||||
|
||||
```bash
|
||||
git add changelog.md server/settings.py
|
||||
git commit -m "release: <VER>
|
||||
|
||||
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
|
||||
git tag <VER>
|
||||
git push
|
||||
git push origin <VER>
|
||||
```
|
||||
|
||||
可以串成一个 `&&` 链一次执行。
|
||||
|
||||
### 5. 汇报
|
||||
|
||||
向用户简述:
|
||||
- 本次版本号 `<VER>`。
|
||||
- changelog 顶部新增了多少行 / 包含哪几类提交。
|
||||
- commit hash、tag 名、master / tag 都已推到 origin。
|
||||
|
||||
## 注意事项
|
||||
|
||||
- **不要自己编版本号**:始终用 `update_changelog.sh` 输出里的那一个,不要试图改格式或日期。
|
||||
- **`SYS_VERSION` 是必须改的**:脚本只更新 `changelog.md`,settings 必须手动同步,否则上线后接口返回的版本号还是旧的。
|
||||
- **CRLF 警告可忽略**:Windows 上 `git commit` 会提示 `LF will be replaced by CRLF`,不是错误。
|
||||
- **打 tag 前确认 working tree 干净**:除了本次 commit 的两个文件外,不应有其他未追踪/未提交的改动混进 release commit。如有,先单独 commit 或 stash。
|
||||
- **失败回滚**:如果 push 失败需要回退,删除本地 tag (`git tag -d <VER>`) 与撤销 commit (`git reset --soft HEAD^`);远端 tag 已 push 的删除需要先与用户确认。
|
||||
|
|
@ -27,3 +27,4 @@ temp/*
|
|||
nohup.out
|
||||
*.zip
|
||||
scripts/*.py
|
||||
.claude/settings.local.json
|
||||
|
|
@ -13,6 +13,7 @@ class Migration(migrations.Migration):
|
|||
sql=[
|
||||
(
|
||||
"""
|
||||
CREATE EXTENSION IF NOT EXISTS timescaledb;
|
||||
CREATE TABLE public.enm_mplogx (
|
||||
"timex" timestamptz NOT NULL,
|
||||
"mpoint_id" text NOT NULL,
|
||||
|
|
|
|||
|
|
@ -206,6 +206,7 @@ class EnStat2Serializer(CustomModelSerializer):
|
|||
class ReCalSerializer(serializers.Serializer):
|
||||
start_time = serializers.DateTimeField(label="开始时间")
|
||||
end_time = serializers.DateTimeField(label="结束时间")
|
||||
mpoint_stat = serializers.BooleanField(label="从MpointStat开始计算", required=False, default=False)
|
||||
|
||||
|
||||
class MpointStatCorrectSerializer(CustomModelSerializer):
|
||||
|
|
|
|||
|
|
@ -114,15 +114,20 @@ def db_ins_mplogx():
|
|||
|
||||
|
||||
@shared_task(base=CustomTask)
|
||||
def cal_mpointstats_duration(start_time: str, end_time: str, m_code_list=[], cal_attrs=[]):
|
||||
def cal_mpointstats_duration(start_time: str, end_time: str, m_code_list=[], cal_attrs=[], mpoint_stat=False):
|
||||
"""
|
||||
重跑某一段时间的任务
|
||||
mpoint_stat: True时从已有的MpointStat hour记录开始重算(跳过MpLogx),只重算day/month/year/sflog聚合,速度更快
|
||||
"""
|
||||
mytz = tz.gettz(settings.TIME_ZONE)
|
||||
start_time = datetime.datetime.strptime(start_time, "%Y-%m-%d %H:%M:%S")
|
||||
start_time.replace(tzinfo=mytz)
|
||||
start_time = start_time.replace(tzinfo=mytz)
|
||||
end_time = datetime.datetime.strptime(end_time, "%Y-%m-%d %H:%M:%S")
|
||||
start_time.replace(tzinfo=mytz)
|
||||
end_time = end_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
|
||||
|
|
@ -131,6 +136,164 @@ def cal_mpointstats_duration(start_time: str, end_time: str, m_code_list=[], cal
|
|||
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
|
||||
while current_time <= end_time:
|
||||
year, month, day, hour = current_time.year, current_time.month, current_time.day, current_time.hour
|
||||
for mgroup in mgroups:
|
||||
cal_enstat("hour_s", None, mgroup.id, year, month, day, hour, None, None, None, True, cal_attrs)
|
||||
current_time += datetime.timedelta(hours=1)
|
||||
|
||||
myLogger.info("_recal_from_mpointstat completed all")
|
||||
|
||||
|
||||
@shared_task(base=CustomTask)
|
||||
def correct_bill_date():
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -378,10 +378,14 @@ class MpointStatViewSet(BulkCreateModelMixin, BulkDestroyModelMixin, CustomListM
|
|||
|
||||
重新运行某段时间的enm计算
|
||||
"""
|
||||
data = request.data
|
||||
sr = ReCalSerializer(data=data)
|
||||
sr = ReCalSerializer(data=request.data)
|
||||
sr.is_valid(raise_exception=True)
|
||||
task = cal_mpointstats_duration.delay(data["start_time"], data["end_time"])
|
||||
data = sr.validated_data
|
||||
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})
|
||||
|
||||
@action(methods=["get"], detail=False, perms_map={"get": "*"})
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ class Migration(migrations.Migration):
|
|||
sql=[
|
||||
(
|
||||
"""
|
||||
CREATE EXTENSION IF NOT EXISTS timescaledb;
|
||||
CREATE TABLE public.enp_envdata (
|
||||
"timex" timestamptz NOT NULL,
|
||||
"equipment_id" text NOT NULL,
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ class HrmService:
|
|||
Returns:
|
||||
_type_: _description_
|
||||
"""
|
||||
if not settings.DAHUA_ENABLED: # 如果大华没启用, 直接返回
|
||||
if not getattr(settings, 'DAHUA_ENABLED', False): # 如果大华没启用, 直接返回
|
||||
return
|
||||
dh_id = ep.third_info.get('dh_id', None)
|
||||
dh_photo = ep.third_info.get('dh_photo', None)
|
||||
|
|
|
|||
|
|
@ -387,20 +387,39 @@ 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']):
|
||||
raise ParseError(f'第{row_num}行,身份证号格式不正确')
|
||||
|
||||
# 查找或更新
|
||||
# 查找或创建/补全
|
||||
try:
|
||||
with transaction.atomic():
|
||||
obj, created = Employee.objects.update_or_create(
|
||||
id_number=id_number,
|
||||
name=name,
|
||||
defaults=data
|
||||
)
|
||||
# 优先按身份证号匹配,匹配不到再按姓名匹配
|
||||
existing = None
|
||||
if id_number:
|
||||
existing = Employee.objects.filter(id_number=id_number).first()
|
||||
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:
|
||||
raise
|
||||
if created:
|
||||
myLogger.info(f"✅ 第{row_num}行新增成功:{name}")
|
||||
else:
|
||||
myLogger.info(f"✅ 第{row_num}行更新成功:{name}")
|
||||
|
||||
success += 1
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from apps.utils.serializers import CustomModelSerializer
|
|||
from apps.mtm.models import Material
|
||||
from .models import MIO, MaterialBatch, MIOItem, WareHouse, MIOItemA, MaterialBatchA, MIOItemw, Pack
|
||||
from django.db import transaction
|
||||
from django.db.models import F, Sum, DecimalField
|
||||
from server.settings import get_sysconfig
|
||||
from apps.wpmw.models import Wpr
|
||||
from decimal import Decimal
|
||||
|
|
@ -97,6 +98,7 @@ class MIOListSerializer(CustomModelSerializer):
|
|||
order_number = serializers.CharField(source='order.number', read_only=True)
|
||||
pu_order_number = serializers.CharField(
|
||||
source='pu_order.number', read_only=True)
|
||||
total_price = serializers.SerializerMethodField(label='总价格')
|
||||
|
||||
class Meta:
|
||||
model = MIO
|
||||
|
|
@ -104,6 +106,12 @@ class MIOListSerializer(CustomModelSerializer):
|
|||
read_only_fields = EXCLUDE_FIELDS + \
|
||||
['state', 'submit_time', 'submit_user', 'number']
|
||||
|
||||
def get_total_price(self, obj):
|
||||
res = MIOItem.objects.filter(mio=obj, unit_price__isnull=False).aggregate(
|
||||
total=Sum(F('count') * F('unit_price'),
|
||||
output_field=DecimalField(max_digits=18, decimal_places=4)))
|
||||
return res['total']
|
||||
|
||||
|
||||
class MIOItemACreateSerializer(CustomModelSerializer):
|
||||
class Meta:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Generated by Django 3.2.12 on 2026-03-12 03:26
|
||||
# Generated by Django 4.2.27 on 2026-04-24 06:50
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
|
@ -12,11 +12,31 @@ class Migration(migrations.Migration):
|
|||
|
||||
dependencies = [
|
||||
('system', '0007_alter_dept_create_by_alter_dept_third_info_and_more'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('wf', '0006_auto_20251215_1645'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
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(
|
||||
name='PurchaseRequisition',
|
||||
fields=[
|
||||
|
|
@ -29,10 +49,100 @@ class Migration(migrations.Migration):
|
|||
('req_date', models.DateField(blank=True, null=True, verbose_name='申购日期')),
|
||||
('total_amount', models.DecimalField(decimal_places=2, default=0, max_digits=14, verbose_name='合计金额')),
|
||||
('note', models.TextField(blank=True, null=True, verbose_name='备注')),
|
||||
('belong_dept', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='purchaserequisition_belong_dept', to='system.dept', verbose_name='所属部门')),
|
||||
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='purchaserequisition_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
|
||||
('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='mpr_ticket', to='wf.ticket', verbose_name='关联工单')),
|
||||
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='purchaserequisition_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
|
||||
('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(
|
||||
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={
|
||||
'abstract': False,
|
||||
|
|
@ -61,4 +171,25 @@ class Migration(migrations.Migration):
|
|||
'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,
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,65 +0,0 @@
|
|||
# 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,
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
# 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,
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
# 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,
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
# 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,9 +1,27 @@
|
|||
from django.db import models
|
||||
from apps.utils.models import BaseModel, CommonBDModel
|
||||
from apps.utils.models import BaseModel, CommonBDModel, CommonBModel
|
||||
from datetime import datetime
|
||||
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):
|
||||
today_str = datetime.now().strftime('%Y%m%d')
|
||||
prefix = model_cls.PREFIX
|
||||
|
|
@ -77,7 +95,7 @@ class WarehouseEntry(CommonBDModel):
|
|||
|
||||
number = models.CharField('编号', max_length=20, unique=True)
|
||||
warehouse = models.ForeignKey(
|
||||
'inm.WareHouse', verbose_name='仓库',
|
||||
WareHouse, verbose_name='库房',
|
||||
on_delete=models.CASCADE, related_name='entries')
|
||||
entry_date = models.DateField('入库日期', null=True, blank=True)
|
||||
entry_type = models.CharField('入库类型', max_length=20, choices=ENTRY_TYPE_CHOICES, default='raw_normal')
|
||||
|
|
@ -123,7 +141,7 @@ class WarehouseStock(BaseModel):
|
|||
)
|
||||
|
||||
warehouse = models.ForeignKey(
|
||||
'inm.WareHouse', verbose_name='仓库',
|
||||
WareHouse, verbose_name='库房',
|
||||
on_delete=models.CASCADE, related_name='mpr_stocks')
|
||||
entry = models.ForeignKey(
|
||||
WarehouseEntry, verbose_name='来源入库单',
|
||||
|
|
|
|||
|
|
@ -7,10 +7,20 @@ from apps.mpr.models import (
|
|||
PurchaseRequisition, PurchaseRequisitionItem,
|
||||
WarehouseEntry, WarehouseEntryItem, WarehouseStock,
|
||||
MaterialRequisition, MaterialRequisitionItem,
|
||||
WareHouse,
|
||||
)
|
||||
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):
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from apps.mpr.views import (
|
||||
WareHouseViewSet,
|
||||
PurchaseRequisitionViewSet, PurchaseRequisitionItemViewSet,
|
||||
WarehouseEntryViewSet, WarehouseEntryItemViewSet,
|
||||
WarehouseStockViewSet,
|
||||
|
|
@ -10,6 +11,7 @@ from apps.mpr.views import (
|
|||
API_BASE_URL = 'api/mpr/'
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register('warehouse', WareHouseViewSet, basename='mpr_warehouse')
|
||||
router.register('requisition', PurchaseRequisitionViewSet, basename='requisition')
|
||||
router.register('requisition_item', PurchaseRequisitionItemViewSet, basename='requisition_item')
|
||||
router.register('warehouse_entry', WarehouseEntryViewSet, basename='warehouse_entry')
|
||||
|
|
|
|||
|
|
@ -8,8 +8,10 @@ from apps.mpr.models import (
|
|||
PurchaseRequisition, PurchaseRequisitionItem,
|
||||
WarehouseEntry, WarehouseEntryItem, WarehouseStock,
|
||||
MaterialRequisition, MaterialRequisitionItem,
|
||||
WareHouse,
|
||||
)
|
||||
from apps.mpr.serializers import (
|
||||
WareHouseSerializer,
|
||||
PurchaseRequisitionListSerializer,
|
||||
PurchaseRequisitionDetailSerializer,
|
||||
PurchaseRequisitionCreateSerializer,
|
||||
|
|
@ -30,6 +32,20 @@ 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):
|
||||
"""
|
||||
物资申购单
|
||||
|
|
|
|||
|
|
@ -102,16 +102,16 @@ class MaterialViewSet(CustomModelViewSet):
|
|||
data = [list(getter(item)) for item in odata]
|
||||
return Response({'path': export_excel(field_data, data, '物料清单')})
|
||||
|
||||
class ShiftViewSet(ListModelMixin, CustomGenericViewSet):
|
||||
class ShiftViewSet(CustomModelViewSet):
|
||||
"""
|
||||
list:班次
|
||||
|
||||
班次
|
||||
"""
|
||||
perms_map = {'get': '*'}
|
||||
queryset = Shift.objects.all()
|
||||
serializer_class = ShiftSerializer
|
||||
search_fields = ['name']
|
||||
search_fields = ['name', 'rule']
|
||||
filterset_fields = ['rule']
|
||||
ordering = ['rule', 'sort', 'id']
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,164 @@
|
|||
# Generated by Django 4.2.27 on 2026-04-20 06:02
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('system', '0007_alter_dept_create_by_alter_dept_third_info_and_more'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('pum', '0010_quotationapply'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PuContract',
|
||||
fields=[
|
||||
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
|
||||
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||
('name', models.CharField(max_length=100, verbose_name='合同名称')),
|
||||
('number', models.CharField(max_length=100, unique=True, verbose_name='合同编号')),
|
||||
('contract_amount', models.DecimalField(decimal_places=2, default=0, max_digits=14, verbose_name='合同金额')),
|
||||
('sign_date', models.DateField(verbose_name='签订日期')),
|
||||
('effective_date', models.DateField(blank=True, null=True, verbose_name='生效日期')),
|
||||
('end_date', models.DateField(blank=True, null=True, verbose_name='截止日期')),
|
||||
('status', models.PositiveSmallIntegerField(choices=[(10, '草稿'), (20, '执行中'), (30, '已完成'), (40, '已终止')], default=10, help_text="((10, '草稿'), (20, '执行中'), (30, '已完成'), (40, '已终止'))", verbose_name='合同状态')),
|
||||
('settlement_status', models.PositiveSmallIntegerField(choices=[(10, '未付款'), (20, '部分付款'), (30, '全部付款')], default=10, help_text="((10, '未付款'), (20, '部分付款'), (30, '全部付款'))", verbose_name='结算状态')),
|
||||
('paid_amount', models.DecimalField(decimal_places=2, default=0, max_digits=14, verbose_name='累计已付款')),
|
||||
('unpaid_amount', models.DecimalField(decimal_places=2, default=0, max_digits=14, verbose_name='累计未付款')),
|
||||
('pay_progress', models.DecimalField(decimal_places=2, default=0, max_digits=5, verbose_name='付款进度')),
|
||||
('description', models.CharField(blank=True, max_length=200, null=True, verbose_name='描述')),
|
||||
('belong_dept', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_belong_dept', to='system.dept', verbose_name='所属部门')),
|
||||
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '采购合同',
|
||||
'verbose_name_plural': '采购合同',
|
||||
},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='puorder',
|
||||
name='belong_dept',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_belong_dept', to='system.dept', verbose_name='所属部门'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='puorder',
|
||||
name='create_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='puorder',
|
||||
name='update_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='puplan',
|
||||
name='belong_dept',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_belong_dept', to='system.dept', verbose_name='所属部门'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='puplan',
|
||||
name='create_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='puplan',
|
||||
name='update_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='puplanitem',
|
||||
name='belong_dept',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_belong_dept', to='system.dept', verbose_name='所属部门'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='puplanitem',
|
||||
name='create_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='puplanitem',
|
||||
name='update_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='quotationapply',
|
||||
name='create_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='quotationapply',
|
||||
name='update_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='supplier',
|
||||
name='belong_dept',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_belong_dept', to='system.dept', verbose_name='所属部门'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='supplier',
|
||||
name='create_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='supplier',
|
||||
name='update_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='supplieraudit',
|
||||
name='create_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='supplieraudit',
|
||||
name='update_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PuContractRecord',
|
||||
fields=[
|
||||
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
|
||||
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||
('record_date', models.DateField(verbose_name='付款日期')),
|
||||
('amount', models.DecimalField(decimal_places=2, max_digits=14, verbose_name='付款金额')),
|
||||
('stage_type', models.PositiveSmallIntegerField(choices=[(10, '首款'), (20, '中间款'), (30, '尾款'), (40, '其他')], default=40, help_text="((10, '首款'), (20, '中间款'), (30, '尾款'), (40, '其他'))", verbose_name='阶段类型')),
|
||||
('pay_method', models.PositiveSmallIntegerField(choices=[(10, '银行转账'), (20, '现金'), (30, '承兑'), (40, '微信'), (50, '支付宝'), (60, '其他')], default=10, help_text="((10, '银行转账'), (20, '现金'), (30, '承兑'), (40, '微信'), (50, '支付宝'), (60, '其他'))", verbose_name='付款方式')),
|
||||
('voucher_no', models.CharField(blank=True, max_length=100, null=True, verbose_name='凭证号')),
|
||||
('remark', models.CharField(blank=True, max_length=200, null=True, verbose_name='备注')),
|
||||
('belong_dept', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_belong_dept', to='system.dept', verbose_name='所属部门')),
|
||||
('contract', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='records', to='pum.pucontract', verbose_name='采购合同')),
|
||||
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
|
||||
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '采购合同付款流水',
|
||||
'verbose_name_plural': '采购合同付款流水',
|
||||
'ordering': ['-record_date', '-create_time'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='pucontract',
|
||||
name='supplier',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contracts', to='pum.supplier', verbose_name='供应商'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='pucontract',
|
||||
name='update_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='puorder',
|
||||
name='contract',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='orders', to='pum.pucontract', verbose_name='采购合同'),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,4 +1,7 @@
|
|||
from decimal import Decimal
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import Sum
|
||||
from apps.utils.models import CommonBModel, BaseModel, CommonBDModel, CommonADModel
|
||||
from apps.mtm.models import Material
|
||||
from apps.wf.models import Ticket
|
||||
|
|
@ -71,6 +74,9 @@ class PuOrder(CommonBModel):
|
|||
number = models.CharField('订单编号', max_length=20, null=True, blank=True)
|
||||
supplier = models.ForeignKey(
|
||||
Supplier, verbose_name='供应商', on_delete=models.CASCADE)
|
||||
contract = models.ForeignKey(
|
||||
'pum.PuContract', verbose_name='采购合同', on_delete=models.SET_NULL,
|
||||
null=True, blank=True, related_name='orders')
|
||||
delivery_date = models.DateField('截止到货日期', null=True, blank=True)
|
||||
submit_time = models.DateTimeField('提交时间', null=True, blank=True)
|
||||
submit_user = models.ForeignKey(
|
||||
|
|
@ -126,3 +132,151 @@ class QuotationApply(CommonADModel):
|
|||
apply_date = models.DateField(verbose_name="申请日期",auto_now_add=True)
|
||||
ticket = models.OneToOneField('wf.ticket', verbose_name='关联工单',
|
||||
on_delete=models.CASCADE, related_name='quo_ticket', null=True, blank=True)
|
||||
|
||||
|
||||
class PuContract(CommonBDModel):
|
||||
"""
|
||||
TN:采购合同
|
||||
"""
|
||||
STATUS_DRAFT = 10
|
||||
STATUS_ACTIVE = 20
|
||||
STATUS_DONE = 30
|
||||
STATUS_TERMINATED = 40
|
||||
STATUS_CHOICES = (
|
||||
(STATUS_DRAFT, '草稿'),
|
||||
(STATUS_ACTIVE, '执行中'),
|
||||
(STATUS_DONE, '已完成'),
|
||||
(STATUS_TERMINATED, '已终止'),
|
||||
)
|
||||
SETTLEMENT_UNPAID = 10
|
||||
SETTLEMENT_PARTIAL = 20
|
||||
SETTLEMENT_FULL = 30
|
||||
SETTLEMENT_CHOICES = (
|
||||
(SETTLEMENT_UNPAID, '未付款'),
|
||||
(SETTLEMENT_PARTIAL, '部分付款'),
|
||||
(SETTLEMENT_FULL, '全部付款'),
|
||||
)
|
||||
name = models.CharField('合同名称', max_length=100)
|
||||
number = models.CharField('合同编号', max_length=100, unique=True)
|
||||
supplier = models.ForeignKey(Supplier, verbose_name='供应商', on_delete=models.CASCADE, related_name='contracts')
|
||||
contract_amount = models.DecimalField('合同金额', max_digits=14, decimal_places=2, default=0)
|
||||
sign_date = models.DateField('签订日期')
|
||||
effective_date = models.DateField('生效日期', null=True, blank=True)
|
||||
end_date = models.DateField('截止日期', null=True, blank=True)
|
||||
status = models.PositiveSmallIntegerField(
|
||||
'合同状态', choices=STATUS_CHOICES, default=STATUS_DRAFT, help_text=str(STATUS_CHOICES))
|
||||
settlement_status = models.PositiveSmallIntegerField(
|
||||
'结算状态', choices=SETTLEMENT_CHOICES, default=SETTLEMENT_UNPAID, help_text=str(SETTLEMENT_CHOICES))
|
||||
paid_amount = models.DecimalField('累计已付款', max_digits=14, decimal_places=2, default=0)
|
||||
unpaid_amount = models.DecimalField('累计未付款', max_digits=14, decimal_places=2, default=0)
|
||||
pay_progress = models.DecimalField('付款进度', max_digits=5, decimal_places=2, default=0)
|
||||
description = models.CharField('描述', max_length=200, blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = '采购合同'
|
||||
verbose_name_plural = verbose_name
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
refresh_settlement = kwargs.pop('refresh_settlement', True)
|
||||
super().save(*args, **kwargs)
|
||||
if refresh_settlement:
|
||||
self.refresh_settlement()
|
||||
|
||||
def refresh_settlement(self):
|
||||
paid_amount = PuContractRecord.objects.filter(contract=self).aggregate(
|
||||
total=Sum('amount')
|
||||
)['total'] or Decimal('0.00')
|
||||
contract_amount = Decimal(str(self.contract_amount or 0)).quantize(Decimal('0.01'))
|
||||
unpaid_amount = contract_amount - paid_amount
|
||||
if unpaid_amount < Decimal('0.00'):
|
||||
unpaid_amount = Decimal('0.00')
|
||||
if contract_amount <= Decimal('0.00'):
|
||||
pay_progress = Decimal('0.00')
|
||||
else:
|
||||
pay_progress = (paid_amount * Decimal('100.00') / contract_amount).quantize(Decimal('0.01'))
|
||||
if pay_progress > Decimal('100.00'):
|
||||
pay_progress = Decimal('100.00')
|
||||
if paid_amount <= Decimal('0.00'):
|
||||
settlement_status = self.SETTLEMENT_UNPAID
|
||||
elif paid_amount >= contract_amount and contract_amount > Decimal('0.00'):
|
||||
settlement_status = self.SETTLEMENT_FULL
|
||||
else:
|
||||
settlement_status = self.SETTLEMENT_PARTIAL
|
||||
status = self.status
|
||||
if status != self.STATUS_TERMINATED:
|
||||
if paid_amount <= Decimal('0.00'):
|
||||
status = self.STATUS_DRAFT
|
||||
elif paid_amount >= contract_amount and contract_amount > Decimal('0.00'):
|
||||
status = self.STATUS_DONE
|
||||
else:
|
||||
status = self.STATUS_ACTIVE
|
||||
type(self).objects.filter(pk=self.pk).update(
|
||||
paid_amount=paid_amount,
|
||||
unpaid_amount=unpaid_amount,
|
||||
pay_progress=pay_progress,
|
||||
settlement_status=settlement_status,
|
||||
status=status,
|
||||
)
|
||||
self.paid_amount = paid_amount
|
||||
self.unpaid_amount = unpaid_amount
|
||||
self.pay_progress = pay_progress
|
||||
self.settlement_status = settlement_status
|
||||
self.status = status
|
||||
|
||||
|
||||
class PuContractRecord(CommonBDModel):
|
||||
"""
|
||||
TN:采购合同付款流水
|
||||
"""
|
||||
STAGE_FIRST = 10
|
||||
STAGE_MIDDLE = 20
|
||||
STAGE_FINAL = 30
|
||||
STAGE_OTHER = 40
|
||||
STAGE_CHOICES = (
|
||||
(STAGE_FIRST, '首款'),
|
||||
(STAGE_MIDDLE, '中间款'),
|
||||
(STAGE_FINAL, '尾款'),
|
||||
(STAGE_OTHER, '其他'),
|
||||
)
|
||||
PAY_BANK = 10
|
||||
PAY_CASH = 20
|
||||
PAY_ACCEPTANCE = 30
|
||||
PAY_WECHAT = 40
|
||||
PAY_ALIPAY = 50
|
||||
PAY_OTHER = 60
|
||||
PAY_METHOD_CHOICES = (
|
||||
(PAY_BANK, '银行转账'),
|
||||
(PAY_CASH, '现金'),
|
||||
(PAY_ACCEPTANCE, '承兑'),
|
||||
(PAY_WECHAT, '微信'),
|
||||
(PAY_ALIPAY, '支付宝'),
|
||||
(PAY_OTHER, '其他'),
|
||||
)
|
||||
contract = models.ForeignKey(
|
||||
PuContract, verbose_name='采购合同', on_delete=models.CASCADE, related_name='records')
|
||||
record_date = models.DateField('付款日期')
|
||||
amount = models.DecimalField('付款金额', max_digits=14, decimal_places=2)
|
||||
stage_type = models.PositiveSmallIntegerField(
|
||||
'阶段类型', choices=STAGE_CHOICES, default=STAGE_OTHER, help_text=str(STAGE_CHOICES))
|
||||
pay_method = models.PositiveSmallIntegerField(
|
||||
'付款方式', choices=PAY_METHOD_CHOICES, default=PAY_BANK, help_text=str(PAY_METHOD_CHOICES))
|
||||
voucher_no = models.CharField('凭证号', max_length=100, null=True, blank=True)
|
||||
remark = models.CharField('备注', max_length=200, null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = '采购合同付款流水'
|
||||
verbose_name_plural = verbose_name
|
||||
ordering = ['-record_date', '-create_time']
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
self.contract.refresh_settlement()
|
||||
|
||||
def delete(self, using=None, *args, **kwargs):
|
||||
contract = self.contract
|
||||
result = super().delete(using=using, *args, **kwargs)
|
||||
contract.refresh_settlement()
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
from decimal import Decimal
|
||||
|
||||
from rest_framework import serializers
|
||||
from apps.utils.serializers import CustomModelSerializer
|
||||
from apps.utils.constants import EXCLUDE_FIELDS_DEPT, EXCLUDE_FIELDS_BASE, EXCLUDE_FIELDS
|
||||
from rest_framework.exceptions import ValidationError, ParseError
|
||||
|
||||
from apps.pum.models import Supplier, PuPlan, PuPlanItem, PuOrder, PuOrderItem, SupplierAudit, QuotationApply
|
||||
from apps.pum.models import Supplier, PuPlan, PuPlanItem, PuOrder, PuOrderItem, SupplierAudit, QuotationApply, PuContract, PuContractRecord
|
||||
from apps.mtm.serializers import MaterialSerializer, MaterialSimpleSerializer
|
||||
from django.db import transaction
|
||||
from .services import PumService
|
||||
|
|
@ -99,6 +101,14 @@ class PuOrderSerializer(CustomModelSerializer):
|
|||
fields = '__all__'
|
||||
read_only_fields = EXCLUDE_FIELDS_DEPT + ['state', 'submit_time', 'total_price']
|
||||
|
||||
def validate(self, attrs):
|
||||
contract = attrs.get('contract', None)
|
||||
if contract:
|
||||
attrs['supplier'] = contract.supplier
|
||||
if attrs.get('supplier', None) is None:
|
||||
raise ValidationError('未选择供应商')
|
||||
return attrs
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
validated_data.pop('supplier')
|
||||
if instance.state != PuOrder.PUORDER_CREATE:
|
||||
|
|
@ -165,3 +175,39 @@ class QuotationApplySerializer(CustomModelSerializer):
|
|||
model = QuotationApply
|
||||
fields = "__all__"
|
||||
read_only_fields = EXCLUDE_FIELDS
|
||||
|
||||
|
||||
class PuContractSerializer(CustomModelSerializer):
|
||||
supplier_name = serializers.CharField(source='supplier.name', read_only=True)
|
||||
create_by_name = serializers.CharField(source='create_by.name', read_only=True)
|
||||
update_by_name = serializers.CharField(source='update_by.name', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = PuContract
|
||||
fields = '__all__'
|
||||
read_only_fields = EXCLUDE_FIELDS + ['belong_dept', 'paid_amount', 'unpaid_amount', 'pay_progress', 'settlement_status']
|
||||
|
||||
|
||||
class PuContractRecordSerializer(CustomModelSerializer):
|
||||
contract_number = serializers.CharField(source='contract.number', read_only=True)
|
||||
supplier_name = serializers.CharField(source='contract.supplier.name', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = PuContractRecord
|
||||
fields = '__all__'
|
||||
read_only_fields = EXCLUDE_FIELDS + ['belong_dept']
|
||||
|
||||
def validate(self, attrs):
|
||||
contract = attrs.get('contract', getattr(self.instance, 'contract', None))
|
||||
amount = attrs.get('amount', getattr(self.instance, 'amount', None))
|
||||
if contract is None or amount is None:
|
||||
return attrs
|
||||
if contract.status == PuContract.STATUS_TERMINATED:
|
||||
raise ValidationError('合同已终止,不可操作付款流水')
|
||||
qs = PuContractRecord.objects.filter(contract=contract)
|
||||
if self.instance is not None:
|
||||
qs = qs.exclude(id=self.instance.id)
|
||||
total = sum((item.amount for item in qs), Decimal('0.00')) + amount
|
||||
if total > contract.contract_amount:
|
||||
raise ValidationError('累计付款金额不可超过合同金额')
|
||||
return attrs
|
||||
|
|
|
|||
|
|
@ -1,3 +1,178 @@
|
|||
from decimal import Decimal
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
from apps.pum.models import Supplier
|
||||
from apps.pum.serializers import PuOrderSerializer
|
||||
from apps.pum.serializers import PuContractRecordSerializer
|
||||
from rest_framework.exceptions import ParseError
|
||||
|
||||
|
||||
class PuContractSettlementTests(TestCase):
|
||||
def test_purchase_contract_record_updates_paid_summary(self):
|
||||
supplier = Supplier.objects.create(name='供应商A')
|
||||
|
||||
from apps.pum.models import PuContract, PuContractRecord
|
||||
|
||||
contract = PuContract.objects.create(
|
||||
name='采购合同A',
|
||||
number='PC-001',
|
||||
contract_amount=Decimal('2000.00'),
|
||||
supplier=supplier,
|
||||
sign_date='2026-04-20',
|
||||
)
|
||||
|
||||
PuContractRecord.objects.create(
|
||||
contract=contract,
|
||||
record_date='2026-04-21',
|
||||
amount=Decimal('800.00'),
|
||||
stage_type=PuContractRecord.STAGE_FIRST,
|
||||
)
|
||||
|
||||
contract.refresh_from_db()
|
||||
self.assertEqual(contract.paid_amount, Decimal('800.00'))
|
||||
self.assertEqual(contract.unpaid_amount, Decimal('1200.00'))
|
||||
self.assertEqual(contract.pay_progress, Decimal('40.00'))
|
||||
self.assertEqual(contract.status, contract.STATUS_ACTIVE)
|
||||
|
||||
def test_purchase_contract_record_delete_refreshes_summary_and_is_physical(self):
|
||||
supplier = Supplier.objects.create(name='供应商C')
|
||||
|
||||
from apps.pum.models import PuContract, PuContractRecord
|
||||
|
||||
contract = PuContract.objects.create(
|
||||
name='采购合同C',
|
||||
number='PC-003',
|
||||
contract_amount=Decimal('2000.00'),
|
||||
supplier=supplier,
|
||||
sign_date='2026-04-20',
|
||||
)
|
||||
|
||||
record = PuContractRecord.objects.create(
|
||||
contract=contract,
|
||||
record_date='2026-04-21',
|
||||
amount=Decimal('800.00'),
|
||||
stage_type=PuContractRecord.STAGE_FIRST,
|
||||
)
|
||||
|
||||
record.delete()
|
||||
contract.refresh_from_db()
|
||||
|
||||
self.assertEqual(contract.paid_amount, Decimal('0.00'))
|
||||
self.assertEqual(contract.unpaid_amount, Decimal('2000.00'))
|
||||
self.assertEqual(contract.pay_progress, Decimal('0.00'))
|
||||
self.assertEqual(contract.status, contract.STATUS_DRAFT)
|
||||
self.assertFalse(PuContractRecord._base_manager.filter(pk=record.pk).exists())
|
||||
|
||||
def test_purchase_contract_delete_is_physical(self):
|
||||
supplier = Supplier.objects.create(name='供应商D')
|
||||
|
||||
from apps.pum.models import PuContract
|
||||
|
||||
contract = PuContract.objects.create(
|
||||
name='采购合同D',
|
||||
number='PC-004',
|
||||
contract_amount=Decimal('500.00'),
|
||||
supplier=supplier,
|
||||
sign_date='2026-04-20',
|
||||
)
|
||||
|
||||
contract.delete()
|
||||
|
||||
self.assertFalse(PuContract._base_manager.filter(pk=contract.pk).exists())
|
||||
|
||||
def test_purchase_contract_status_auto_transitions_by_records(self):
|
||||
supplier = Supplier.objects.create(name='供应商E')
|
||||
|
||||
from apps.pum.models import PuContract, PuContractRecord
|
||||
|
||||
contract = PuContract.objects.create(
|
||||
name='采购合同E',
|
||||
number='PC-005',
|
||||
contract_amount=Decimal('2000.00'),
|
||||
supplier=supplier,
|
||||
sign_date='2026-04-20',
|
||||
)
|
||||
|
||||
self.assertEqual(contract.status, PuContract.STATUS_DRAFT)
|
||||
|
||||
first_record = PuContractRecord.objects.create(
|
||||
contract=contract,
|
||||
record_date='2026-04-21',
|
||||
amount=Decimal('800.00'),
|
||||
stage_type=PuContractRecord.STAGE_FIRST,
|
||||
)
|
||||
contract.refresh_from_db()
|
||||
self.assertEqual(contract.status, PuContract.STATUS_ACTIVE)
|
||||
|
||||
PuContractRecord.objects.create(
|
||||
contract=contract,
|
||||
record_date='2026-04-22',
|
||||
amount=Decimal('1200.00'),
|
||||
stage_type=PuContractRecord.STAGE_FINAL,
|
||||
)
|
||||
contract.refresh_from_db()
|
||||
self.assertEqual(contract.status, PuContract.STATUS_DONE)
|
||||
|
||||
first_record.delete()
|
||||
contract.refresh_from_db()
|
||||
self.assertEqual(contract.status, PuContract.STATUS_ACTIVE)
|
||||
|
||||
def test_purchase_terminated_contract_forbids_record_changes(self):
|
||||
supplier = Supplier.objects.create(name='供应商F')
|
||||
|
||||
from apps.pum.models import PuContract, PuContractRecord
|
||||
|
||||
contract = PuContract.objects.create(
|
||||
name='采购合同F',
|
||||
number='PC-006',
|
||||
contract_amount=Decimal('1000.00'),
|
||||
supplier=supplier,
|
||||
sign_date='2026-04-20',
|
||||
status=PuContract.STATUS_TERMINATED,
|
||||
)
|
||||
|
||||
serializer = PuContractRecordSerializer(data={
|
||||
'contract': contract.id,
|
||||
'record_date': '2026-04-21',
|
||||
'amount': '100.00',
|
||||
'stage_type': 10,
|
||||
'pay_method': 10,
|
||||
})
|
||||
self.assertFalse(serializer.is_valid())
|
||||
self.assertIn('合同已终止,不可操作付款流水', str(serializer.errors))
|
||||
|
||||
contract.status = PuContract.STATUS_DRAFT
|
||||
contract.save(refresh_settlement=False)
|
||||
|
||||
record = PuContractRecord.objects.create(
|
||||
contract=contract,
|
||||
record_date='2026-04-20',
|
||||
amount=Decimal('100.00'),
|
||||
stage_type=PuContractRecord.STAGE_OTHER,
|
||||
)
|
||||
contract.status = PuContract.STATUS_TERMINATED
|
||||
contract.save(refresh_settlement=False)
|
||||
|
||||
with self.assertRaises(ParseError):
|
||||
from apps.pum.views import PuContractRecordViewSet
|
||||
viewset = PuContractRecordViewSet()
|
||||
viewset.request = None
|
||||
viewset.perform_destroy(record)
|
||||
|
||||
def test_purchase_order_serializer_accepts_purchase_contract(self):
|
||||
supplier = Supplier.objects.create(name='供应商B')
|
||||
|
||||
from apps.pum.models import PuContract
|
||||
|
||||
contract = PuContract.objects.create(
|
||||
name='采购合同B',
|
||||
number='PC-002',
|
||||
contract_amount=Decimal('500.00'),
|
||||
supplier=supplier,
|
||||
sign_date='2026-04-20',
|
||||
)
|
||||
|
||||
serializer = PuOrderSerializer(data={'supplier': supplier.id, 'contract': contract.id})
|
||||
|
||||
self.assertTrue(serializer.is_valid(), serializer.errors)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from apps.pum.views import (SupplierViewSet, PuPlanViewSet, PuPlanItemViewSet, PuOrderViewSet, PuOrderItemViewSet, SupplierAuditViewSet, QuotationApplyViewSet)
|
||||
from apps.pum.views import (SupplierViewSet, PuPlanViewSet, PuPlanItemViewSet, PuOrderViewSet, PuOrderItemViewSet, SupplierAuditViewSet, QuotationApplyViewSet, PuContractViewSet, PuContractRecordViewSet)
|
||||
# from apps.pum.views import SupplierViewSet, PuPlanViewSet, PuPlanItemViewSet, PuOrderViewSet, PuOrderItemViewSet
|
||||
|
||||
API_BASE_URL = 'api/pum/'
|
||||
|
|
@ -11,6 +11,8 @@ router.register('supplier', SupplierViewSet, basename='supplier')
|
|||
router.register('supplieraudit', SupplierAuditViewSet, basename='supplieraudit')
|
||||
router.register('pu_plan', PuPlanViewSet, basename='pu_plan')
|
||||
router.register('pu_planitem', PuPlanItemViewSet, basename='pu_planitem')
|
||||
router.register('pu_contract', PuContractViewSet, basename='pu_contract')
|
||||
router.register('pu_contract_record', PuContractRecordViewSet, basename='pu_contract_record')
|
||||
router.register('pu_order', PuOrderViewSet, basename='pu_order')
|
||||
router.register('pu_orderitem', PuOrderItemViewSet, basename='pu_orderitem')
|
||||
router.register('quotation', QuotationApplyViewSet, basename='quotation')
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
from django.shortcuts import render
|
||||
from apps.pum.models import Supplier, PuPlan, PuPlanItem, PuOrder, PuOrderItem, SupplierAudit, QuotationApply
|
||||
from apps.pum.models import Supplier, PuPlan, PuPlanItem, PuOrder, PuOrderItem, SupplierAudit, QuotationApply, PuContract, PuContractRecord
|
||||
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet, EuModelViewSet
|
||||
from apps.pum.serializers import (SupplierSerializer, PuPlanSerializer, PuPlanItemSerializer, QuotationApplySerializer,
|
||||
PuOrderSerializer, PuOrderItemSerializer, AddSerializer, SupplierAuditSerializer)
|
||||
PuOrderSerializer, PuOrderItemSerializer, AddSerializer, SupplierAuditSerializer,
|
||||
PuContractSerializer, PuContractRecordSerializer)
|
||||
from rest_framework.exceptions import ParseError, PermissionDenied
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework import serializers
|
||||
|
|
@ -223,3 +224,50 @@ class QuotationApplyViewSet(TicketMixin, CustomModelViewSet):
|
|||
search_fields = ['product_name', 'customer_name','contact_person']
|
||||
ordering = ['create_time']
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ class RemployeeCreateSerializer(CustomModelSerializer):
|
|||
if Remployee.objects.filter(id_number=validated_data['id_number'], rparty=validated_data['rparty']).exists():
|
||||
raise ValidationError('该成员已存在')
|
||||
with transaction.atomic():
|
||||
if settings.DAHUA_ENABLED:
|
||||
if getattr(settings, 'DAHUA_ENABLED', False):
|
||||
dhClient.request(**dhapis['person_img_upload'], file_path_rela=validated_data['photo'])
|
||||
return super().create(validated_data)
|
||||
|
||||
|
|
@ -161,7 +161,7 @@ class RemployeeUpdateSerializer(CustomModelSerializer):
|
|||
|
||||
def update(self, instance, validated_data):
|
||||
with transaction.atomic():
|
||||
if settings.DAHUA_ENABLED:
|
||||
if getattr(settings, 'DAHUA_ENABLED', False):
|
||||
dhClient.request(**dhapis['person_img_upload'], file_path_rela=validated_data['photo'])
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,122 @@
|
|||
# Generated by Django 4.2.27 on 2026-04-20 06:02
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('system', '0007_alter_dept_create_by_alter_dept_third_info_and_more'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('sam', '0008_alter_orderitem_order'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='contract',
|
||||
name='effective_date',
|
||||
field=models.DateField(blank=True, null=True, verbose_name='生效日期'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='contract',
|
||||
name='end_date',
|
||||
field=models.DateField(blank=True, null=True, verbose_name='截止日期'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='contract',
|
||||
name='receive_progress',
|
||||
field=models.DecimalField(decimal_places=2, default=0, max_digits=5, verbose_name='到款进度'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='contract',
|
||||
name='received_amount',
|
||||
field=models.DecimalField(decimal_places=2, default=0, max_digits=14, verbose_name='累计已到款'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='contract',
|
||||
name='settlement_status',
|
||||
field=models.PositiveSmallIntegerField(choices=[(10, '未到款'), (20, '部分到款'), (30, '全部到款')], default=10, help_text="((10, '未到款'), (20, '部分到款'), (30, '全部到款'))", verbose_name='结算状态'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='contract',
|
||||
name='status',
|
||||
field=models.PositiveSmallIntegerField(choices=[(10, '草稿'), (20, '执行中'), (30, '已完成'), (40, '已终止')], default=10, help_text="((10, '草稿'), (20, '执行中'), (30, '已完成'), (40, '已终止'))", verbose_name='合同状态'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='contract',
|
||||
name='unreceived_amount',
|
||||
field=models.DecimalField(decimal_places=2, default=0, max_digits=14, verbose_name='累计未到款'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='contract',
|
||||
name='belong_dept',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_belong_dept', to='system.dept', verbose_name='所属部门'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='contract',
|
||||
name='create_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='contract',
|
||||
name='update_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='customer',
|
||||
name='belong_dept',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_belong_dept', to='system.dept', verbose_name='所属部门'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='customer',
|
||||
name='create_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='customer',
|
||||
name='update_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='order',
|
||||
name='belong_dept',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_belong_dept', to='system.dept', verbose_name='所属部门'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='order',
|
||||
name='create_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='order',
|
||||
name='update_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ContractRecord',
|
||||
fields=[
|
||||
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
|
||||
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||
('record_date', models.DateField(verbose_name='到款日期')),
|
||||
('amount', models.DecimalField(decimal_places=2, max_digits=14, verbose_name='到款金额')),
|
||||
('stage_type', models.PositiveSmallIntegerField(choices=[(10, '首款'), (20, '中间款'), (30, '尾款'), (40, '其他')], default=40, help_text="((10, '首款'), (20, '中间款'), (30, '尾款'), (40, '其他'))", verbose_name='阶段类型')),
|
||||
('pay_method', models.PositiveSmallIntegerField(choices=[(10, '银行转账'), (20, '现金'), (30, '承兑'), (40, '微信'), (50, '支付宝'), (60, '其他')], default=10, help_text="((10, '银行转账'), (20, '现金'), (30, '承兑'), (40, '微信'), (50, '支付宝'), (60, '其他'))", verbose_name='收款方式')),
|
||||
('voucher_no', models.CharField(blank=True, max_length=100, null=True, verbose_name='凭证号')),
|
||||
('remark', models.CharField(blank=True, max_length=200, null=True, verbose_name='备注')),
|
||||
('belong_dept', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_belong_dept', to='system.dept', verbose_name='所属部门')),
|
||||
('contract', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='records', to='sam.contract', verbose_name='销售合同')),
|
||||
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
|
||||
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '销售合同到款流水',
|
||||
'verbose_name_plural': '销售合同到款流水',
|
||||
'ordering': ['-record_date', '-create_time'],
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -1,4 +1,7 @@
|
|||
from decimal import Decimal
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import Sum
|
||||
from apps.utils.models import CommonBModel, BaseModel, CommonBDModel
|
||||
from apps.mtm.models import Material
|
||||
|
||||
|
|
@ -24,16 +27,43 @@ class Customer(CommonBModel):
|
|||
return self.name
|
||||
|
||||
|
||||
class Contract(CommonBModel):
|
||||
class Contract(CommonBDModel):
|
||||
"""
|
||||
TN:合同信息
|
||||
"""
|
||||
STATUS_DRAFT = 10
|
||||
STATUS_ACTIVE = 20
|
||||
STATUS_DONE = 30
|
||||
STATUS_TERMINATED = 40
|
||||
STATUS_CHOICES = (
|
||||
(STATUS_DRAFT, '草稿'),
|
||||
(STATUS_ACTIVE, '执行中'),
|
||||
(STATUS_DONE, '已完成'),
|
||||
(STATUS_TERMINATED, '已终止'),
|
||||
)
|
||||
SETTLEMENT_UNRECEIVED = 10
|
||||
SETTLEMENT_PARTIAL = 20
|
||||
SETTLEMENT_FULL = 30
|
||||
SETTLEMENT_CHOICES = (
|
||||
(SETTLEMENT_UNRECEIVED, '未到款'),
|
||||
(SETTLEMENT_PARTIAL, '部分到款'),
|
||||
(SETTLEMENT_FULL, '全部到款'),
|
||||
)
|
||||
name = models.CharField('合同名称', max_length=100)
|
||||
number = models.CharField('合同编号', max_length=100, unique=True)
|
||||
amount = models.IntegerField('合同金额', default=0)
|
||||
customer = models.ForeignKey(Customer, verbose_name='关联客户',
|
||||
on_delete=models.CASCADE, related_name='contract_customer')
|
||||
sign_date = models.DateField('签订日期')
|
||||
effective_date = models.DateField('生效日期', null=True, blank=True)
|
||||
end_date = models.DateField('截止日期', null=True, blank=True)
|
||||
status = models.PositiveSmallIntegerField(
|
||||
'合同状态', choices=STATUS_CHOICES, default=STATUS_DRAFT, help_text=str(STATUS_CHOICES))
|
||||
settlement_status = models.PositiveSmallIntegerField(
|
||||
'结算状态', choices=SETTLEMENT_CHOICES, default=SETTLEMENT_UNRECEIVED, help_text=str(SETTLEMENT_CHOICES))
|
||||
received_amount = models.DecimalField('累计已到款', max_digits=14, decimal_places=2, default=0)
|
||||
unreceived_amount = models.DecimalField('累计未到款', max_digits=14, decimal_places=2, default=0)
|
||||
receive_progress = models.DecimalField('到款进度', max_digits=5, decimal_places=2, default=0)
|
||||
description = models.CharField('描述', max_length=200, blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
|
|
@ -43,6 +73,53 @@ class Contract(CommonBModel):
|
|||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
refresh_settlement = kwargs.pop('refresh_settlement', True)
|
||||
super().save(*args, **kwargs)
|
||||
if refresh_settlement:
|
||||
self.refresh_settlement()
|
||||
|
||||
def refresh_settlement(self):
|
||||
received_amount = ContractRecord.objects.filter(contract=self).aggregate(
|
||||
total=Sum('amount')
|
||||
)['total'] or Decimal('0.00')
|
||||
contract_amount = Decimal(str(self.amount or 0)).quantize(Decimal('0.01'))
|
||||
unreceived_amount = contract_amount - received_amount
|
||||
if unreceived_amount < Decimal('0.00'):
|
||||
unreceived_amount = Decimal('0.00')
|
||||
if contract_amount <= Decimal('0.00'):
|
||||
receive_progress = Decimal('0.00')
|
||||
else:
|
||||
receive_progress = (received_amount * Decimal('100.00') / contract_amount).quantize(Decimal('0.01'))
|
||||
if receive_progress > Decimal('100.00'):
|
||||
receive_progress = Decimal('100.00')
|
||||
if received_amount <= Decimal('0.00'):
|
||||
settlement_status = self.SETTLEMENT_UNRECEIVED
|
||||
elif received_amount >= contract_amount and contract_amount > Decimal('0.00'):
|
||||
settlement_status = self.SETTLEMENT_FULL
|
||||
else:
|
||||
settlement_status = self.SETTLEMENT_PARTIAL
|
||||
status = self.status
|
||||
if status != self.STATUS_TERMINATED:
|
||||
if received_amount <= Decimal('0.00'):
|
||||
status = self.STATUS_DRAFT
|
||||
elif received_amount >= contract_amount and contract_amount > Decimal('0.00'):
|
||||
status = self.STATUS_DONE
|
||||
else:
|
||||
status = self.STATUS_ACTIVE
|
||||
type(self).objects.filter(pk=self.pk).update(
|
||||
received_amount=received_amount,
|
||||
unreceived_amount=unreceived_amount,
|
||||
receive_progress=receive_progress,
|
||||
settlement_status=settlement_status,
|
||||
status=status,
|
||||
)
|
||||
self.received_amount = received_amount
|
||||
self.unreceived_amount = unreceived_amount
|
||||
self.receive_progress = receive_progress
|
||||
self.settlement_status = settlement_status
|
||||
self.status = status
|
||||
|
||||
|
||||
class Order(CommonBModel):
|
||||
"""
|
||||
|
|
@ -87,3 +164,58 @@ class OrderItem(BaseModel):
|
|||
delivered_count = models.PositiveIntegerField('已交货数量', default=0)
|
||||
utask = models.ForeignKey('pm.utask', verbose_name='关联生产大任务',
|
||||
on_delete=models.SET_NULL, null=True, blank=True)
|
||||
|
||||
|
||||
class ContractRecord(CommonBDModel):
|
||||
"""
|
||||
TN:销售合同到款流水
|
||||
"""
|
||||
STAGE_FIRST = 10
|
||||
STAGE_MIDDLE = 20
|
||||
STAGE_FINAL = 30
|
||||
STAGE_OTHER = 40
|
||||
STAGE_CHOICES = (
|
||||
(STAGE_FIRST, '首款'),
|
||||
(STAGE_MIDDLE, '中间款'),
|
||||
(STAGE_FINAL, '尾款'),
|
||||
(STAGE_OTHER, '其他'),
|
||||
)
|
||||
PAY_BANK = 10
|
||||
PAY_CASH = 20
|
||||
PAY_ACCEPTANCE = 30
|
||||
PAY_WECHAT = 40
|
||||
PAY_ALIPAY = 50
|
||||
PAY_OTHER = 60
|
||||
PAY_METHOD_CHOICES = (
|
||||
(PAY_BANK, '银行转账'),
|
||||
(PAY_CASH, '现金'),
|
||||
(PAY_ACCEPTANCE, '承兑'),
|
||||
(PAY_WECHAT, '微信'),
|
||||
(PAY_ALIPAY, '支付宝'),
|
||||
(PAY_OTHER, '其他'),
|
||||
)
|
||||
contract = models.ForeignKey(
|
||||
Contract, verbose_name='销售合同', on_delete=models.CASCADE, related_name='records')
|
||||
record_date = models.DateField('到款日期')
|
||||
amount = models.DecimalField('到款金额', max_digits=14, decimal_places=2)
|
||||
stage_type = models.PositiveSmallIntegerField(
|
||||
'阶段类型', choices=STAGE_CHOICES, default=STAGE_OTHER, help_text=str(STAGE_CHOICES))
|
||||
pay_method = models.PositiveSmallIntegerField(
|
||||
'收款方式', choices=PAY_METHOD_CHOICES, default=PAY_BANK, help_text=str(PAY_METHOD_CHOICES))
|
||||
voucher_no = models.CharField('凭证号', max_length=100, null=True, blank=True)
|
||||
remark = models.CharField('备注', max_length=200, null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = '销售合同到款流水'
|
||||
verbose_name_plural = verbose_name
|
||||
ordering = ['-record_date', '-create_time']
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
self.contract.refresh_settlement()
|
||||
|
||||
def delete(self, using=None, *args, **kwargs):
|
||||
contract = self.contract
|
||||
result = super().delete(using=using, *args, **kwargs)
|
||||
contract.refresh_settlement()
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
from decimal import Decimal
|
||||
|
||||
from rest_framework import serializers
|
||||
from apps.utils.serializers import CustomModelSerializer
|
||||
from apps.sam.models import Customer, Contract, Order, OrderItem
|
||||
from apps.sam.models import Customer, Contract, Order, OrderItem, ContractRecord
|
||||
from apps.utils.constants import EXCLUDE_FIELDS, EXCLUDE_FIELDS_BASE
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from apps.mtm.serializers import MaterialSerializer
|
||||
|
|
@ -81,3 +83,29 @@ class OrderItemSerializer(CustomModelSerializer):
|
|||
validated_data.pop('product', None)
|
||||
validated_data.pop('order', None)
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
class ContractRecordSerializer(CustomModelSerializer):
|
||||
contract_number = serializers.CharField(source='contract.number', read_only=True)
|
||||
customer_name = serializers.CharField(source='contract.customer.name', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ContractRecord
|
||||
fields = '__all__'
|
||||
read_only_fields = EXCLUDE_FIELDS + ['belong_dept']
|
||||
|
||||
def validate(self, attrs):
|
||||
contract = attrs.get('contract', getattr(self.instance, 'contract', None))
|
||||
amount = attrs.get('amount', getattr(self.instance, 'amount', None))
|
||||
if contract is None or amount is None:
|
||||
return attrs
|
||||
if contract.status == Contract.STATUS_TERMINATED:
|
||||
raise ValidationError('合同已终止,不可操作到款流水')
|
||||
qs = ContractRecord.objects.filter(contract=contract)
|
||||
if self.instance is not None:
|
||||
qs = qs.exclude(id=self.instance.id)
|
||||
total = sum((item.amount for item in qs), Decimal('0.00')) + amount
|
||||
contract_amount = Decimal(str(contract.amount or 0))
|
||||
if total > contract_amount:
|
||||
raise ValidationError('累计到款金额不可超过合同金额')
|
||||
return attrs
|
||||
|
|
|
|||
|
|
@ -1,3 +1,178 @@
|
|||
from decimal import Decimal
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
from apps.sam.models import Contract, Customer
|
||||
from apps.sam.serializers import ContractRecordSerializer
|
||||
from rest_framework.exceptions import ParseError
|
||||
|
||||
|
||||
class ContractSettlementTests(TestCase):
|
||||
def test_sales_contract_record_updates_received_summary(self):
|
||||
customer = Customer.objects.create(
|
||||
name='客户A',
|
||||
contact='张三',
|
||||
contact_phone='13800000001',
|
||||
)
|
||||
contract = Contract.objects.create(
|
||||
name='销售合同A',
|
||||
number='SC-001',
|
||||
amount=1000,
|
||||
customer=customer,
|
||||
sign_date='2026-04-20',
|
||||
)
|
||||
|
||||
from apps.sam.models import ContractRecord
|
||||
|
||||
ContractRecord.objects.create(
|
||||
contract=contract,
|
||||
record_date='2026-04-21',
|
||||
amount=Decimal('300.00'),
|
||||
stage_type=ContractRecord.STAGE_FIRST,
|
||||
)
|
||||
ContractRecord.objects.create(
|
||||
contract=contract,
|
||||
record_date='2026-04-22',
|
||||
amount=Decimal('200.00'),
|
||||
stage_type=ContractRecord.STAGE_MIDDLE,
|
||||
)
|
||||
|
||||
contract.refresh_from_db()
|
||||
self.assertEqual(contract.received_amount, Decimal('500.00'))
|
||||
self.assertEqual(contract.unreceived_amount, Decimal('500.00'))
|
||||
self.assertEqual(contract.receive_progress, Decimal('50.00'))
|
||||
self.assertEqual(contract.status, Contract.STATUS_ACTIVE)
|
||||
|
||||
def test_sales_contract_record_delete_refreshes_summary_and_is_physical(self):
|
||||
customer = Customer.objects.create(
|
||||
name='客户B',
|
||||
contact='李四',
|
||||
contact_phone='13800000002',
|
||||
)
|
||||
contract = Contract.objects.create(
|
||||
name='销售合同B',
|
||||
number='SC-002',
|
||||
amount=1000,
|
||||
customer=customer,
|
||||
sign_date='2026-04-20',
|
||||
)
|
||||
|
||||
from apps.sam.models import ContractRecord
|
||||
|
||||
record = ContractRecord.objects.create(
|
||||
contract=contract,
|
||||
record_date='2026-04-21',
|
||||
amount=Decimal('300.00'),
|
||||
stage_type=ContractRecord.STAGE_FIRST,
|
||||
)
|
||||
|
||||
record.delete()
|
||||
contract.refresh_from_db()
|
||||
|
||||
self.assertEqual(contract.received_amount, Decimal('0.00'))
|
||||
self.assertEqual(contract.unreceived_amount, Decimal('1000.00'))
|
||||
self.assertEqual(contract.receive_progress, Decimal('0.00'))
|
||||
self.assertEqual(contract.status, Contract.STATUS_DRAFT)
|
||||
self.assertFalse(ContractRecord._base_manager.filter(pk=record.pk).exists())
|
||||
|
||||
def test_sales_contract_delete_is_physical(self):
|
||||
customer = Customer.objects.create(
|
||||
name='客户C',
|
||||
contact='王五',
|
||||
contact_phone='13800000003',
|
||||
)
|
||||
contract = Contract.objects.create(
|
||||
name='销售合同C',
|
||||
number='SC-003',
|
||||
amount=500,
|
||||
customer=customer,
|
||||
sign_date='2026-04-20',
|
||||
)
|
||||
|
||||
contract.delete()
|
||||
|
||||
self.assertFalse(Contract._base_manager.filter(pk=contract.pk).exists())
|
||||
|
||||
def test_sales_contract_status_auto_transitions_by_records(self):
|
||||
customer = Customer.objects.create(
|
||||
name='客户D',
|
||||
contact='赵六',
|
||||
contact_phone='13800000004',
|
||||
)
|
||||
contract = Contract.objects.create(
|
||||
name='销售合同D',
|
||||
number='SC-004',
|
||||
amount=1000,
|
||||
customer=customer,
|
||||
sign_date='2026-04-20',
|
||||
)
|
||||
|
||||
from apps.sam.models import ContractRecord
|
||||
|
||||
self.assertEqual(contract.status, Contract.STATUS_DRAFT)
|
||||
|
||||
first_record = ContractRecord.objects.create(
|
||||
contract=contract,
|
||||
record_date='2026-04-21',
|
||||
amount=Decimal('300.00'),
|
||||
stage_type=ContractRecord.STAGE_FIRST,
|
||||
)
|
||||
contract.refresh_from_db()
|
||||
self.assertEqual(contract.status, Contract.STATUS_ACTIVE)
|
||||
|
||||
ContractRecord.objects.create(
|
||||
contract=contract,
|
||||
record_date='2026-04-22',
|
||||
amount=Decimal('700.00'),
|
||||
stage_type=ContractRecord.STAGE_FINAL,
|
||||
)
|
||||
contract.refresh_from_db()
|
||||
self.assertEqual(contract.status, Contract.STATUS_DONE)
|
||||
|
||||
first_record.delete()
|
||||
contract.refresh_from_db()
|
||||
self.assertEqual(contract.status, Contract.STATUS_ACTIVE)
|
||||
|
||||
def test_sales_terminated_contract_forbids_record_changes(self):
|
||||
customer = Customer.objects.create(
|
||||
name='客户E',
|
||||
contact='孙七',
|
||||
contact_phone='13800000005',
|
||||
)
|
||||
contract = Contract.objects.create(
|
||||
name='销售合同E',
|
||||
number='SC-005',
|
||||
amount=1000,
|
||||
customer=customer,
|
||||
sign_date='2026-04-20',
|
||||
status=Contract.STATUS_TERMINATED,
|
||||
)
|
||||
|
||||
serializer = ContractRecordSerializer(data={
|
||||
'contract': contract.id,
|
||||
'record_date': '2026-04-21',
|
||||
'amount': '100.00',
|
||||
'stage_type': 10,
|
||||
'pay_method': 10,
|
||||
})
|
||||
self.assertFalse(serializer.is_valid())
|
||||
self.assertIn('合同已终止,不可操作到款流水', str(serializer.errors))
|
||||
|
||||
contract.status = Contract.STATUS_DRAFT
|
||||
contract.save(refresh_settlement=False)
|
||||
|
||||
from apps.sam.models import ContractRecord
|
||||
record = ContractRecord.objects.create(
|
||||
contract=contract,
|
||||
record_date='2026-04-20',
|
||||
amount=Decimal('100.00'),
|
||||
stage_type=ContractRecord.STAGE_OTHER,
|
||||
)
|
||||
contract.status = Contract.STATUS_TERMINATED
|
||||
contract.save(refresh_settlement=False)
|
||||
|
||||
with self.assertRaises(ParseError):
|
||||
from apps.sam.views import ContractRecordViewSet
|
||||
viewset = ContractRecordViewSet()
|
||||
viewset.request = None
|
||||
viewset.perform_destroy(record)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from apps.sam.views import (CustomerViewSet, ContractViewSet, OrderViewSet, OrderItemViewSet)
|
||||
from apps.sam.views import (CustomerViewSet, ContractViewSet, OrderViewSet, OrderItemViewSet, ContractRecordViewSet)
|
||||
|
||||
API_BASE_URL = 'api/sam/'
|
||||
HTML_BASE_URL = 'dhtml/sam/'
|
||||
|
|
@ -8,6 +8,7 @@ HTML_BASE_URL = 'dhtml/sam/'
|
|||
router = DefaultRouter()
|
||||
router.register('customer', CustomerViewSet, basename='customer')
|
||||
router.register('contract', ContractViewSet, basename='contract')
|
||||
router.register('contract_record', ContractRecordViewSet, basename='contract_record')
|
||||
router.register('order', OrderViewSet, basename='order')
|
||||
router.register('orderitem', OrderItemViewSet, basename='orderitem')
|
||||
urlpatterns = [
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from django.shortcuts import render
|
||||
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet
|
||||
from apps.sam.models import Customer, Contract, Order, OrderItem
|
||||
from apps.sam.serializers import CustomerSerializer, ContractSerializer, OrderSerializer, OrderItemSerializer
|
||||
from apps.sam.models import Customer, Contract, Order, OrderItem, ContractRecord
|
||||
from apps.sam.serializers import CustomerSerializer, ContractSerializer, OrderSerializer, OrderItemSerializer, ContractRecordSerializer
|
||||
from rest_framework.exceptions import ParseError
|
||||
from rest_framework.mixins import ListModelMixin, CreateModelMixin, DestroyModelMixin
|
||||
from apps.utils.mixins import BulkCreateModelMixin
|
||||
|
|
@ -46,6 +46,7 @@ class ContractViewSet(CustomModelViewSet):
|
|||
def perform_destroy(self, instance):
|
||||
if Order.objects.filter(contract=instance).exists():
|
||||
raise ParseError('该合同存在订单不可删除')
|
||||
instance.delete()
|
||||
|
||||
|
||||
class OrderViewSet(CustomModelViewSet):
|
||||
|
|
@ -106,3 +107,32 @@ class OrderItemViewSet(ListModelMixin, CreateModelMixin, DestroyModelMixin, Cust
|
|||
if instance.order.state != Order.ORDER_CREATE:
|
||||
raise ParseError('该订单状态下不可删除')
|
||||
return super().perform_destroy(instance)
|
||||
|
||||
|
||||
class ContractRecordViewSet(CustomModelViewSet):
|
||||
"""
|
||||
list: 销售合同到款流水
|
||||
|
||||
销售合同到款流水
|
||||
"""
|
||||
perms_map = {
|
||||
'get': '*',
|
||||
'post': 'contract.update',
|
||||
'put': 'contract.update',
|
||||
'patch': 'contract.update',
|
||||
'delete': 'contract.update',
|
||||
}
|
||||
queryset = ContractRecord.objects.all()
|
||||
serializer_class = ContractRecordSerializer
|
||||
search_fields = ['contract__number', 'contract__name', 'voucher_no', 'remark']
|
||||
select_related_fields = ['contract', 'contract__customer', 'create_by', 'update_by']
|
||||
filterset_fields = {
|
||||
'contract': ['exact'],
|
||||
'stage_type': ['exact', 'in'],
|
||||
'pay_method': ['exact', 'in'],
|
||||
}
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
if instance.contract.status == Contract.STATUS_TERMINATED:
|
||||
raise ParseError('合同已终止,不可删除到款流水')
|
||||
instance.delete()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
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="是否继承"),
|
||||
),
|
||||
]
|
||||
|
|
@ -510,7 +510,7 @@ class Mlogb(BaseModel):
|
|||
|
||||
@property
|
||||
def mlogbdefect(self):
|
||||
return MlogbDefect.objects.filter(mlogb=self)
|
||||
return self.mlogbdefect_set.all()
|
||||
|
||||
def cal_count_pn_jgqbl(self, cal_mlog=False):
|
||||
mqs = MlogbDefect.get_defect_qs_from_mlogb(self, ftype="in")
|
||||
|
|
@ -535,12 +535,58 @@ class Mlogb(BaseModel):
|
|||
if mlog and cal_mlog:
|
||||
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.mlog_id and self.mlog and self.mlog.is_fix:
|
||||
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)
|
||||
had_inherited = inherited_qs.exists()
|
||||
if MlogbDefect.objects.filter(mlogb=self, is_inherited=False).exists() or self.has_legacy_defect_count():
|
||||
inherited_qs.delete()
|
||||
if had_inherited and cal_count:
|
||||
self.cal_count_notok(cal_mlog=False)
|
||||
return
|
||||
|
||||
defect = self.get_default_inherited_defect()
|
||||
if defect is None:
|
||||
inherited_qs.delete()
|
||||
if had_inherited and cal_count:
|
||||
self.cal_count_notok(cal_mlog=False)
|
||||
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):
|
||||
"""TN: 生成记录的缺陷记录"""
|
||||
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)
|
||||
count = 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
|
||||
def get_defect_qs(cls, ftype="all"):
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from apps.wpm.models import BatchSt
|
||||
import logging
|
||||
from apps.wpm.models import Mlogb, Mlogbw, MlogbDefect
|
||||
from apps.wpm.models import Mlogb, Mlogbw, MlogbDefect, MlogUser
|
||||
from apps.mtm.models import Mgroup
|
||||
import decimal
|
||||
from django.db.models import Sum
|
||||
|
|
@ -27,7 +27,7 @@ def main(batch: str, mgroup_obj:Mgroup=None):
|
|||
mgroup_name = mgroup.name
|
||||
mlogb1_qs = Mlogb.objects.filter(mlog__submit_time__isnull=False,
|
||||
material_out__isnull=False, mlog__mgroup=mgroup,
|
||||
mlog__is_fix=False, batch=batch, need_inout=True)
|
||||
mlog__is_fix=False, batch=batch, need_inout=True).order_by("mlog__submit_time")
|
||||
if mlogb1_qs.exists():
|
||||
data[f"{mgroup_name}_日期"] = []
|
||||
data[f"{mgroup_name}_操作人"] = []
|
||||
|
|
@ -38,6 +38,7 @@ def main(batch: str, mgroup_obj:Mgroup=None):
|
|||
data[f"{mgroup_name}_count_ok_full"] = 0
|
||||
data[f"{mgroup_name}_count_pn_jgqbl"] = 0
|
||||
mlogb_q_ids = []
|
||||
cal_mlog = []
|
||||
for item in mlogb1_qs:
|
||||
# 找到对应的输入
|
||||
mlogb_from:Mlogb = item.mlogb_from
|
||||
|
|
@ -51,6 +52,13 @@ def main(batch: str, mgroup_obj:Mgroup=None):
|
|||
data[f"{mgroup_name}_count_pn_jgqbl"] += 0
|
||||
if item.mlog.handle_user:
|
||||
data[f"{mgroup_name}_操作人"].append(item.mlog.handle_user)
|
||||
# 子工序操作人
|
||||
if item.mlog not in cal_mlog:
|
||||
mlog_users_qs = MlogUser.objects.filter(mlog=item.mlog)
|
||||
if mlog_users_qs.exists():
|
||||
for mlog_user in mlog_users_qs:
|
||||
data[f"{mgroup_name}_{mlog_user.process.name}_操作人"] = mlog_user.handle_user.name
|
||||
cal_mlog.append(item.mlog)
|
||||
if item.mlog.handle_date:
|
||||
data[f"{mgroup_name}_日期"].append(item.mlog.handle_date)
|
||||
data[f"{mgroup_name}_count_real"] += item.count_real
|
||||
|
|
@ -60,12 +68,12 @@ def main(batch: str, mgroup_obj:Mgroup=None):
|
|||
|
||||
try:
|
||||
data[f"{mgroup_name}_完全合格率"] = round((data[f"{mgroup_name}_count_ok_full"] / data[f"{mgroup_name}_count_real"])*100, 2)
|
||||
except decimal.InvalidOperation:
|
||||
except (decimal.InvalidOperation, ZeroDivisionError):
|
||||
data[f"{mgroup_name}_完全合格率"] = 0
|
||||
|
||||
try:
|
||||
data[f"{mgroup_name}_合格率"] = round((data[f"{mgroup_name}_count_ok"] / data[f"{mgroup_name}_count_real"])*100, 2)
|
||||
except decimal.InvalidOperation:
|
||||
except (decimal.InvalidOperation, ZeroDivisionError):
|
||||
data[f"{mgroup_name}_合格率"] = 0
|
||||
|
||||
mlogbd1_qs = MlogbDefect.objects.filter(mlogb__in=mlogb1_qs, count__gt=0).values("defect__name").annotate(total=Sum("count"))
|
||||
|
|
@ -85,8 +93,8 @@ def main(batch: str, mgroup_obj:Mgroup=None):
|
|||
data[f"{mgroup_name}_含缺陷_{item['defect__name']}_比例"] = round((item["total"] / data[f"{mgroup_name}_count_real"])*100, 2)
|
||||
|
||||
data[f"{mgroup_name}_日期"] = list(set(data[f"{mgroup_name}_日期"]))
|
||||
data[f"{mgroup_name}_小日期"] = max(data[f"{mgroup_name}_日期"]).strftime("%Y-%m-%d")
|
||||
data[f"{mgroup_name}_大日期"] = min(data[f"{mgroup_name}_日期"]).strftime("%Y-%m-%d")
|
||||
data[f"{mgroup_name}_小日期"] = min(data[f"{mgroup_name}_日期"]).strftime("%Y-%m-%d")
|
||||
data[f"{mgroup_name}_大日期"] = max(data[f"{mgroup_name}_日期"]).strftime("%Y-%m-%d")
|
||||
data[f"{mgroup_name}_日期"] = ";".join([item.strftime("%Y-%m-%d") for item in data[f"{mgroup_name}_日期"]])
|
||||
data[f"{mgroup_name}_操作人"] = list(set(data[f"{mgroup_name}_操作人"]))
|
||||
data[f"{mgroup_name}_操作人"] = ";".join([item.name for item in data[f"{mgroup_name}_操作人"]])
|
||||
|
|
|
|||
|
|
@ -0,0 +1,216 @@
|
|||
# batch_gxerp.py 批次统计计算规则
|
||||
|
||||
`apps/wpm/scripts/batch_gxerp.py` 用于按 **批次号 (batch)** 汇总该批次在各工序组下的生产/检验数据,写入 `BatchSt.data` (JSON),同时维护 `first_time` / `last_time`。
|
||||
|
||||
---
|
||||
|
||||
## 一、入参与前置条件
|
||||
|
||||
| 参数 | 说明 |
|
||||
| --- | --- |
|
||||
| `batch` | 批次号 |
|
||||
| `mgroup_obj` | 工序组对象(当前实现未直接使用,仅作为入口签名) |
|
||||
|
||||
- 必须存在 `BatchSt.objects.get(batch=batch, version=1)`,否则报错并退出。
|
||||
- 遍历 `Mgroup.objects.all().order_by("sort")`,对每个工序组分别统计。
|
||||
- 仅统计已提交且已出库的报工单:`mlog.submit_time__isnull=False` 且 `material_out__isnull=False`,并要求 `need_inout=True`。
|
||||
|
||||
---
|
||||
|
||||
## 二、字段命名约定
|
||||
|
||||
输出 JSON `data` 的 key 形式如下(`<G>` 代表工序组名):
|
||||
|
||||
| Key 模式 | 含义 |
|
||||
| --- | --- |
|
||||
| `批次号` | 批次号 |
|
||||
| `<G>_日期` | 该工序组所有报工日期,去重后 `;` 拼接 |
|
||||
| `<G>_小日期` / `<G>_大日期` | 该工序组最早 / 最晚日期(`YYYY-MM-DD`) |
|
||||
| `<G>_操作人` | 操作人姓名去重后 `;` 拼接 |
|
||||
| `<G>_班次` | 班次名去重后 `;` 拼接 |
|
||||
| `<G>_count_use` | 领用数(来自上游 `mlogb_from`) |
|
||||
| `<G>_count_real` | 实际生产数 |
|
||||
| `<G>_count_ok` | 合格数 |
|
||||
| `<G>_count_notok` | 不合格数 |
|
||||
| `<G>_count_ok_full` | 完全合格数 |
|
||||
| `<G>_count_pn_jgqbl` | 加工前不良数(来自上游 `mlogb_from`) |
|
||||
| `<G>_合格率` | 合格率(%) |
|
||||
| `<G>_完全合格率` | 完全合格率(%) |
|
||||
| `<G>_缺陷_<缺陷名>` | 该工序组的缺陷数量(按缺陷名汇总) |
|
||||
| `<G>_缺陷_<缺陷名>_比例` | 缺陷数 / `count_real` × 100,单位 % |
|
||||
| `<G>_加工前_缺陷_<缺陷名>` | 上游 `mlogb_from` 上的缺陷数量 |
|
||||
| `<G>_加工前_缺陷_<缺陷名>_比例` | 加工前缺陷数 / `count_use` × 100,单位 % |
|
||||
|
||||
---
|
||||
|
||||
## 三、主流程:按工序组汇总(普通报工,`is_fix=False`)
|
||||
|
||||
数据源 `mlogb1_qs`:
|
||||
|
||||
```
|
||||
Mlogb where mlog.submit_time IS NOT NULL
|
||||
and material_out IS NOT NULL
|
||||
and mlog.mgroup = <G>
|
||||
and mlog.is_fix = False
|
||||
and batch = <batch>
|
||||
and need_inout = True
|
||||
```
|
||||
|
||||
### 1. 累加规则
|
||||
|
||||
对每条 `Mlogb`:
|
||||
|
||||
| 字段 | 累加来源 |
|
||||
| --- | --- |
|
||||
| `count_use` | `mlogb_from.count_use`(上游报工子项;若无 `mlogb_from` 则跳过) |
|
||||
| `count_pn_jgqbl` | `mlogb_from.count_pn_jgqbl` |
|
||||
| `count_real` | 当前 `item.count_real` |
|
||||
| `count_ok` | 当前 `item.count_ok` |
|
||||
| `count_ok_full` | 当前 `item.count_ok_full or 0` |
|
||||
| `count_notok` | 当前 `item.count_notok or 0` |
|
||||
| `操作人` | `mlog.handle_user`(最终去重,按 `name` 拼接) |
|
||||
| `日期` | `mlog.handle_date`(最终去重排序) |
|
||||
| `班次` | `mlog.shift.name` |
|
||||
|
||||
并记录所有上游 `mlogb_from.id` 到 `mlogb_q_ids`,用于"加工前缺陷"统计。
|
||||
|
||||
### 2. 合格率计算
|
||||
|
||||
```
|
||||
完全合格率 = round(count_ok_full / count_real * 100, 2)
|
||||
合格率 = round(count_ok / count_real * 100, 2)
|
||||
```
|
||||
|
||||
`decimal.InvalidOperation` 异常时(如分母为 0)记 `0`。
|
||||
|
||||
### 3. 缺陷统计
|
||||
|
||||
- 当前工序缺陷:`MlogbDefect.filter(mlogb__in=mlogb1_qs, count__gt=0).values('defect__name').annotate(total=Sum('count'))`
|
||||
- `<G>_缺陷_<name>` = total
|
||||
- `<G>_缺陷_<name>_比例` = `round(total / count_real * 100, 2)`
|
||||
- 加工前(上游)缺陷:`MlogbDefect.filter(mlogb__id__in=mlogb_q_ids, count__gt=0)...`
|
||||
- `<G>_加工前_缺陷_<name>` = total
|
||||
- `<G>_加工前_缺陷_<name>_比例` = `round(total / count_use * 100, 2)`
|
||||
|
||||
### 4. 日期字段
|
||||
|
||||
```python
|
||||
data[f"{G}_小日期"] = min(日期列表).strftime("%Y-%m-%d") # 最早日期
|
||||
data[f"{G}_大日期"] = max(日期列表).strftime("%Y-%m-%d") # 最晚日期
|
||||
```
|
||||
|
||||
最终 `<G>_日期` 被覆写为去重排序后的字符串(`;` 拼接)。
|
||||
|
||||
---
|
||||
|
||||
## 四、外观检验返修(`is_fix=True`)
|
||||
|
||||
数据源 `mlogb2_qs`:
|
||||
|
||||
```
|
||||
Mlogb where mlog.submit_time IS NOT NULL
|
||||
and material_out IS NOT NULL
|
||||
and mlog.mgroup.name = '外观检验'
|
||||
and mlog.is_fix = True
|
||||
and batch = <batch>
|
||||
and need_inout = True
|
||||
```
|
||||
|
||||
输出字段:
|
||||
|
||||
| Key | 计算 |
|
||||
| --- | --- |
|
||||
| `外观检验_返修_日期` | 报工日期,去重 `;` 拼接 |
|
||||
| `外观检验_返修_操作人` | 操作人姓名,去重 `;` 拼接 |
|
||||
| `外观检验_返修_count_real` | Σ `count_real` |
|
||||
| `外观检验_返修_count_ok` | Σ `count_ok` |
|
||||
| `外观检验_返修_count_ok_full` | Σ `count_ok_full or 0` |
|
||||
| `外观检验_返修_缺陷_<name>` | `MlogbDefect` 按缺陷名汇总 |
|
||||
| `外观检验_返修_缺陷_<name>_比例` | `round(total / 外观检验_返修_count_real * 100, 2)` |
|
||||
|
||||
---
|
||||
|
||||
## 五、外观检验车间库存抽检
|
||||
|
||||
数据源 `ft_qs`:
|
||||
|
||||
```
|
||||
FtestWork where type2 = TYPE2_SOME
|
||||
and wm.mgroup.name = '外观检验'
|
||||
and batch = <batch>
|
||||
and submit_time IS NOT NULL
|
||||
```
|
||||
|
||||
输出字段:
|
||||
|
||||
| Key | 计算 |
|
||||
| --- | --- |
|
||||
| `外观检验_车间库存抽检_日期` | `test_date` 去重 `;` 拼接 |
|
||||
| `外观检验_车间库存抽检_操作人` | `test_user.name` 去重 `;` 拼接 |
|
||||
| `外观检验_车间库存抽检_count_notok` | Σ `count_notok or 0` |
|
||||
| `外观检验_车间库存抽检_缺陷_<name>` | `FtestworkDefect` 按缺陷名汇总(无比例字段) |
|
||||
|
||||
---
|
||||
|
||||
## 六、外观检验汇总指标(仅当存在 `外观检验_count_ok` 时计算)
|
||||
|
||||
| Key | 公式 |
|
||||
| --- | --- |
|
||||
| `外观检验_总合格数` | `外观检验_count_ok + 外观检验_返修_count_ok(默认0)` |
|
||||
| `外观检验_总合格率` | `round(外观检验_总合格数 / 外观检验_count_real * 100, 2)` |
|
||||
| `外观检验_完全总合格数` | `外观检验_count_ok_full + 外观检验_返修_count_ok_full(默认0)` |
|
||||
| `外观检验_完全总合格率` | `round(外观检验_完全总合格数 / 外观检验_count_real * 100, 2)` |
|
||||
| `外观检验_直通合格数` | `外观检验_总合格数 - 外观检验_车间库存抽检_count_notok(默认0)` |
|
||||
|
||||
### 直通合格率(依赖尺寸检验)
|
||||
|
||||
仅当 `尺寸检验_合格率` 存在时:
|
||||
|
||||
```
|
||||
外观检验_直通合格率 = round(外观检验_总合格率 * 尺寸检验_合格率 / 100, 2)
|
||||
外观检验_直通合格率2 = round(外观检验_直通合格数 / 尺寸检验_count_use * 100, 2)
|
||||
```
|
||||
|
||||
仅当 `尺寸检验_完全合格率` 存在时:
|
||||
|
||||
```
|
||||
外观检验_完全直通合格率 = round(外观检验_完全总合格率 * 尺寸检验_完全合格率 / 100, 2)
|
||||
```
|
||||
|
||||
异常(`InvalidOperation` / `ZeroDivisionError`)回退为 `0`。
|
||||
|
||||
---
|
||||
|
||||
## 七、写回 BatchSt
|
||||
|
||||
1. `data` 经 `MyJSONEncoder` 序列化后回填 `batchst.data`。
|
||||
2. 调 `get_f_l_date(data)`:扫描所有以 `_日期` 结尾的字段,按 `;` 拆开后逐个 `datetime.strptime("%Y-%m-%d")` 解析为 `date` 对象,再取最小/最大并转成上海时区的 `00:00:00` / `23:59:59`,得到 `first_time` / `last_time`。无法解析的片段会写日志并跳过。
|
||||
3. 仅当现有 `first_time` 为空或新值更早时更新;`last_time` 反之。
|
||||
4. 调用 `batchst.save()` 持久化。
|
||||
|
||||
---
|
||||
|
||||
## 八、关键依赖与字段含义速查
|
||||
|
||||
| 模型字段 | 中文释义 | 来源 |
|
||||
| --- | --- | --- |
|
||||
| `Mlogb.count_use` | 领用数 | 报工子项(来自 `mlogb_from`) |
|
||||
| `Mlogb.count_real` | 实际生产数 | 报工子项 |
|
||||
| `Mlogb.count_ok` | 合格数 | `count_real - count_notok` |
|
||||
| `Mlogb.count_ok_full` | 完全合格数 | `count_real - count_notok_full` |
|
||||
| `Mlogb.count_notok` | 不合格数 | 缺陷 `okcate=30` 求和 |
|
||||
| `Mlogb.count_pn_jgqbl` | 加工前不良 | `MlogbDefect` 中加工前不良求和 |
|
||||
| `Mlogb.need_inout` | 是否需出入库 | 仅 `True` 参与统计 |
|
||||
| `Mlogb.mlogb_from` | 上游报工子项 | 用于追溯领用与加工前缺陷 |
|
||||
|
||||
---
|
||||
|
||||
## 九、已知潜在问题
|
||||
|
||||
1. **`mgroup_obj` 入参未使用**:当前实现忽略该参数,对所有 `Mgroup` 全量遍历。
|
||||
|
||||
## 十、修订记录
|
||||
|
||||
- 修复 `小日期 / 大日期` 命名与含义反向,`小日期` 取最早日期、`大日期` 取最晚日期;同步修正 `batch_bxerp.py`、`batch_gzerp.py` 中所有同类字段。
|
||||
- `get_f_l_date` 改为先 `datetime.strptime` 解析为 `date` 对象再比较,移除对字符串字典序的隐式依赖;非法日期片段记录日志后跳过。
|
||||
- 所有合格率/直通率计算的兜底 `except` 同时捕获 `decimal.InvalidOperation` 与 `ZeroDivisionError`,以兼容 `Decimal` 与原生数值的除零场景;`batch_bxerp.py` 与 `batch_gzerp.py` 中仅捕获 `decimal.InvalidOperation` 的合格率分支也同步扩展。
|
||||
|
|
@ -57,12 +57,12 @@ def main(batch: str, mgroup_obj):
|
|||
|
||||
try:
|
||||
data[f"{mgroup_name}_完全合格率"] = round((data[f"{mgroup_name}_count_ok_full"] / data[f"{mgroup_name}_count_real"])*100, 2)
|
||||
except decimal.InvalidOperation:
|
||||
except (decimal.InvalidOperation, ZeroDivisionError):
|
||||
data[f"{mgroup_name}_完全合格率"] = 0
|
||||
|
||||
try:
|
||||
data[f"{mgroup_name}_合格率"] = round((data[f"{mgroup_name}_count_ok"] / data[f"{mgroup_name}_count_real"])*100, 2)
|
||||
except decimal.InvalidOperation:
|
||||
except (decimal.InvalidOperation, ZeroDivisionError):
|
||||
data[f"{mgroup_name}_合格率"] = 0
|
||||
|
||||
mlogbd1_qs = MlogbDefect.objects.filter(mlogb__in=mlogb1_qs, count__gt=0).values("defect__name").annotate(total=Sum("count"))
|
||||
|
|
@ -78,8 +78,8 @@ def main(batch: str, mgroup_obj):
|
|||
|
||||
data[f"{mgroup_name}_日期"] = list(set(data[f"{mgroup_name}_日期"]))
|
||||
data[f"{mgroup_name}_日期"].sort()
|
||||
data[f"{mgroup_name}_小日期"] = max(data[f"{mgroup_name}_日期"]).strftime("%Y-%m-%d")
|
||||
data[f"{mgroup_name}_大日期"] = min(data[f"{mgroup_name}_日期"]).strftime("%Y-%m-%d")
|
||||
data[f"{mgroup_name}_小日期"] = min(data[f"{mgroup_name}_日期"]).strftime("%Y-%m-%d")
|
||||
data[f"{mgroup_name}_大日期"] = max(data[f"{mgroup_name}_日期"]).strftime("%Y-%m-%d")
|
||||
data[f"{mgroup_name}_日期"] = ";".join([item.strftime("%Y-%m-%d") for item in data[f"{mgroup_name}_日期"]])
|
||||
data[f"{mgroup_name}_操作人"] = list(set(data[f"{mgroup_name}_操作人"]))
|
||||
data[f"{mgroup_name}_操作人"] = ";".join([item.name for item in data[f"{mgroup_name}_操作人"]])
|
||||
|
|
@ -144,20 +144,20 @@ def main(batch: str, mgroup_obj):
|
|||
data["外观检验_总合格数"] = data["外观检验_count_ok"] + data.get("外观检验_返修_count_ok", 0)
|
||||
try:
|
||||
data["外观检验_总合格率"] = round((data["外观检验_总合格数"] / data["外观检验_count_real"])*100, 2)
|
||||
except decimal.InvalidOperation:
|
||||
except (decimal.InvalidOperation, ZeroDivisionError):
|
||||
data["外观检验_总合格率"] = 0
|
||||
|
||||
data["外观检验_完全总合格数"] = data["外观检验_count_ok_full"] + data.get("外观检验_返修_count_ok_full", 0)
|
||||
try:
|
||||
data["外观检验_完全总合格率"] = round((data["外观检验_完全总合格数"] / data["外观检验_count_real"])*100, 2)
|
||||
except decimal.InvalidOperation:
|
||||
except (decimal.InvalidOperation, ZeroDivisionError):
|
||||
data["外观检验_完全总合格率"] = 0
|
||||
|
||||
data["外观检验_直通合格数"] = data["外观检验_总合格数"] - data.get("外观检验_车间库存抽检_count_notok", 0)
|
||||
if "尺寸检验_合格率" in data:
|
||||
try:
|
||||
data["外观检验_直通合格率"] = round((data["外观检验_总合格率"]* data["尺寸检验_合格率"])/100, 2)
|
||||
except decimal.InvalidOperation:
|
||||
except (decimal.InvalidOperation, ZeroDivisionError):
|
||||
data["外观检验_直通合格率"] = 0
|
||||
|
||||
try:
|
||||
|
|
@ -168,7 +168,7 @@ def main(batch: str, mgroup_obj):
|
|||
if "尺寸检验_完全合格率" in data:
|
||||
try:
|
||||
data["外观检验_完全直通合格率"] = round((data["外观检验_完全总合格率"]* data["尺寸检验_完全合格率"])/100, 2)
|
||||
except decimal.InvalidOperation:
|
||||
except (decimal.InvalidOperation, ZeroDivisionError):
|
||||
data["外观检验_完全直通合格率"] = 0
|
||||
|
||||
res = get_f_l_date(data)
|
||||
|
|
|
|||
|
|
@ -59,8 +59,8 @@ def main(batch: str, mgroup_obj=None):
|
|||
data["棒料成型_切料人"] = ";".join([item.name for item in data["棒料成型_切料人"]])
|
||||
data["棒料成型_日期"] = list(set(data["棒料成型_日期"]))
|
||||
data["棒料成型_日期"].sort()
|
||||
data["棒料成型_小日期"] = max(data["棒料成型_日期"]).strftime("%Y-%m-%d")
|
||||
data["棒料成型_大日期"] = min(data["棒料成型_日期"]).strftime("%Y-%m-%d")
|
||||
data["棒料成型_小日期"] = min(data["棒料成型_日期"]).strftime("%Y-%m-%d")
|
||||
data["棒料成型_大日期"] = max(data["棒料成型_日期"]).strftime("%Y-%m-%d")
|
||||
data["棒料成型_日期"] = ";".join([item.strftime("%Y-%m-%d") for item in data["棒料成型_日期"]])
|
||||
try:
|
||||
data["棒料成型_合格率"] = round((data["棒料成型_count_ok"] * 100/ data["棒料成型_count_real"]), 1)
|
||||
|
|
@ -95,8 +95,8 @@ def main(batch: str, mgroup_obj=None):
|
|||
data["管料成型_合格率"] = round((data["管料成型_count_ok"] * 100 / data["管料成型_count_real"]), 1)
|
||||
data["管料成型_日期"] = list(set(data["管料成型_日期"]))
|
||||
data["管料成型_日期"].sort()
|
||||
data["管料成型_小日期"] = max(data["管料成型_日期"]).strftime("%Y-%m-%d")
|
||||
data["管料成型_大日期"] = min(data["管料成型_日期"]).strftime("%Y-%m-%d")
|
||||
data["管料成型_小日期"] = min(data["管料成型_日期"]).strftime("%Y-%m-%d")
|
||||
data["管料成型_大日期"] = max(data["管料成型_日期"]).strftime("%Y-%m-%d")
|
||||
data["管料成型_日期"] = ";".join([item.strftime("%Y-%m-%d") for item in data["管料成型_日期"]])
|
||||
|
||||
# 7车间生产入库数据/ 8车间中检数据
|
||||
|
|
@ -127,8 +127,8 @@ def main(batch: str, mgroup_obj=None):
|
|||
data["七车间入库_合格率"] = round((data["七车间入库_count"] - data["七车间入库_count_notok"]) * 100/ data["七车间入库_count"], 1)
|
||||
data["七车间入库_日期"] = list(set(data["七车间入库_日期"]))
|
||||
data["七车间入库_日期"].sort()
|
||||
data["七车间入库_小日期"] = max(data["七车间入库_日期"]).strftime("%Y-%m-%d")
|
||||
data["七车间入库_大日期"] = min(data["七车间入库_日期"]).strftime("%Y-%m-%d")
|
||||
data["七车间入库_小日期"] = min(data["七车间入库_日期"]).strftime("%Y-%m-%d")
|
||||
data["七车间入库_大日期"] = max(data["七车间入库_日期"]).strftime("%Y-%m-%d")
|
||||
data["七车间入库_日期"] = ";".join([item.strftime("%Y-%m-%d") for item in data["七车间入库_日期"]])
|
||||
data["七车间入库_车间执行人"] = list(set(data["七车间入库_车间执行人"]))
|
||||
data["七车间入库_车间执行人"] = ";".join([item.name for item in data["七车间入库_车间执行人"]])
|
||||
|
|
@ -168,8 +168,8 @@ def main(batch: str, mgroup_obj=None):
|
|||
data["十车间入库_仓库执行人"] = ";".join([item.name for item in data["十车间入库_仓库执行人"]])
|
||||
data["十车间入库_日期"] = list(set(data["十车间入库_日期"]))
|
||||
data["十车间入库_日期"].sort()
|
||||
data["十车间入库_小日期"] = max(data["十车间入库_日期"]).strftime("%Y-%m-%d")
|
||||
data["十车间入库_大日期"] = min(data["十车间入库_日期"]).strftime("%Y-%m-%d")
|
||||
data["十车间入库_小日期"] = min(data["十车间入库_日期"]).strftime("%Y-%m-%d")
|
||||
data["十车间入库_大日期"] = max(data["十车间入库_日期"]).strftime("%Y-%m-%d")
|
||||
data["十车间入库_日期"] = ";".join([item.strftime("%Y-%m-%d") for item in data["十车间入库_日期"]])
|
||||
data["十车间入库_合格数"] = data["十车间入库_count"] - data["十车间入库_count_notok"]
|
||||
data["十车间入库_合格率"] = round((data["十车间入库_count"] - data["十车间入库_count_notok"]) * 100/ data["十车间入库_count"], 1)
|
||||
|
|
@ -222,8 +222,8 @@ def main(batch: str, mgroup_obj=None):
|
|||
data[f'管料退火_{field}'] += getattr(item, field)
|
||||
data["管料退火_日期"] = list(set(data["管料退火_日期"]))
|
||||
data["管料退火_日期"].sort()
|
||||
data["管料退火_小日期"] = max(data["管料退火_日期"]).strftime("%Y-%m-%d")
|
||||
data["管料退火_大日期"] = min(data["管料退火_日期"]).strftime("%Y-%m-%d")
|
||||
data["管料退火_小日期"] = min(data["管料退火_日期"]).strftime("%Y-%m-%d")
|
||||
data["管料退火_大日期"] = max(data["管料退火_日期"]).strftime("%Y-%m-%d")
|
||||
data["管料退火_日期"] = ";".join([item.strftime("%Y-%m-%d") for item in data["管料退火_日期"]])
|
||||
data["管料退火_操作人"] = list(set(data["管料退火_操作人"]))
|
||||
data["管料退火_操作人"] = ";".join([item.name for item in data["管料退火_操作人"]])
|
||||
|
|
@ -257,8 +257,8 @@ def main(batch: str, mgroup_obj=None):
|
|||
data[f'六车间领料_{field}'] += getattr(item, field)
|
||||
data["六车间领料_日期"] = list(set(data["六车间领料_日期"]))
|
||||
data["六车间领料_日期"].sort()
|
||||
data["六车间领料_小日期"] = max(data["六车间领料_日期"]).strftime("%Y-%m-%d")
|
||||
data["六车间领料_大日期"] = min(data["六车间领料_日期"]).strftime("%Y-%m-%d")
|
||||
data["六车间领料_小日期"] = min(data["六车间领料_日期"]).strftime("%Y-%m-%d")
|
||||
data["六车间领料_大日期"] = max(data["六车间领料_日期"]).strftime("%Y-%m-%d")
|
||||
data["六车间领料_日期"] = ";".join([item.strftime("%Y-%m-%d") for item in data["六车间领料_日期"]])
|
||||
data["六车间领料_仓库执行人"] = list(set(data["六车间领料_仓库执行人"]))
|
||||
data["六车间领料_仓库执行人"] = ";".join([item.name for item in data["六车间领料_仓库执行人"]])
|
||||
|
|
@ -283,8 +283,8 @@ def main(batch: str, mgroup_obj=None):
|
|||
data["六车间交接领料_接料人"].append(item.recive_user)
|
||||
data["六车间交接领料_日期"] = list(set(data["六车间交接领料_日期"]))
|
||||
data["六车间交接领料_日期"].sort()
|
||||
data["六车间交接领料_小日期"] = max(data["六车间交接领料_日期"]).strftime("%Y-%m-%d")
|
||||
data["六车间交接领料_大日期"] = min(data["六车间交接领料_日期"]).strftime("%Y-%m-%d")
|
||||
data["六车间交接领料_小日期"] = min(data["六车间交接领料_日期"]).strftime("%Y-%m-%d")
|
||||
data["六车间交接领料_大日期"] = max(data["六车间交接领料_日期"]).strftime("%Y-%m-%d")
|
||||
data["六车间交接领料_日期"] = ";".join([item.strftime("%Y-%m-%d") for item in data["六车间交接领料_日期"]])
|
||||
data["六车间交接领料_送料人"] = list(set(data["六车间交接领料_送料人"]))
|
||||
data["六车间交接领料_送料人"] = ";".join([item.name for item in data["六车间交接领料_送料人"]])
|
||||
|
|
@ -320,14 +320,14 @@ def main(batch: str, mgroup_obj=None):
|
|||
data[f'六车间_{mgroup_name}_{field}'] += getattr(item, field)
|
||||
data[f'六车间_{mgroup_name}_日期'] = list(set(data[f'六车间_{mgroup_name}_日期']))
|
||||
data[f'六车间_{mgroup_name}_日期'].sort()
|
||||
data[f'六车间_{mgroup_name}_小日期'] = max(data[f'六车间_{mgroup_name}_日期']).strftime("%Y-%m-%d")
|
||||
data[f'六车间_{mgroup_name}_大日期'] = min(data[f'六车间_{mgroup_name}_日期']).strftime("%Y-%m-%d")
|
||||
data[f'六车间_{mgroup_name}_小日期'] = min(data[f'六车间_{mgroup_name}_日期']).strftime("%Y-%m-%d")
|
||||
data[f'六车间_{mgroup_name}_大日期'] = max(data[f'六车间_{mgroup_name}_日期']).strftime("%Y-%m-%d")
|
||||
data[f'六车间_{mgroup_name}_日期'] = ";".join([item.strftime("%Y-%m-%d") for item in data[f'六车间_{mgroup_name}_日期']])
|
||||
data[f'六车间_{mgroup_name}_操作人'] = list(set(data[f'六车间_{mgroup_name}_操作人']))
|
||||
data[f'六车间_{mgroup_name}_操作人'] = ";".join([item.name for item in data[f'六车间_{mgroup_name}_操作人']])
|
||||
try:
|
||||
data[f'六车间_{mgroup_name}_合格率'] = round(data[f'六车间_{mgroup_name}_count_ok'] * 100/ data[f'六车间_{mgroup_name}_count_real'], 1)
|
||||
except decimal.InvalidOperation:
|
||||
except (decimal.InvalidOperation, ZeroDivisionError):
|
||||
# myLogger.error(f"六车间_{mgroup_name}_合格率decimal.InvalidOperation-{data}")
|
||||
data[f'六车间_{mgroup_name}_合格率'] = 0
|
||||
|
||||
|
|
@ -359,8 +359,8 @@ def main(batch: str, mgroup_obj=None):
|
|||
data[f'六车间中检_{field}'] += getattr(item, field)
|
||||
data["六车间中检_日期"] = list(set(data["六车间中检_日期"]))
|
||||
data["六车间中检_日期"].sort()
|
||||
data["六车间中检_小日期"] = max(data["六车间中检_日期"]).strftime("%Y-%m-%d")
|
||||
data["六车间中检_大日期"] = min(data["六车间中检_日期"]).strftime("%Y-%m-%d")
|
||||
data["六车间中检_小日期"] = min(data["六车间中检_日期"]).strftime("%Y-%m-%d")
|
||||
data["六车间中检_大日期"] = max(data["六车间中检_日期"]).strftime("%Y-%m-%d")
|
||||
data["六车间中检_日期"] = ";".join([item.strftime("%Y-%m-%d") for item in data["六车间中检_日期"]])
|
||||
data['六车间中检_检验人'] = list(set(data['六车间中检_检验人']))
|
||||
data['六车间中检_检验人'] = ";".join([item.name for item in data['六车间中检_检验人']])
|
||||
|
|
@ -395,7 +395,7 @@ def main(batch: str, mgroup_obj=None):
|
|||
data['六车间生产入库_检验人'] = ";".join([item.name for item in data['六车间生产入库_检验人']])
|
||||
try:
|
||||
data['六车间生产入库_合格率'] = round((data['六车间生产入库_count'] - data['六车间生产入库_count_notok']) * 100/ data['六车间生产入库_count'], 1)
|
||||
except decimal.InvalidOperation:
|
||||
except (decimal.InvalidOperation, ZeroDivisionError):
|
||||
# myLogger.error("六车间生产入库_合格率decimal.InvalidOperation-{data}")
|
||||
data['六车间生产入库_合格率'] = 0
|
||||
|
||||
|
|
@ -419,8 +419,8 @@ def main(batch: str, mgroup_obj=None):
|
|||
if "六车间生产入库_日期" in data:
|
||||
data["六车间生产入库_日期"] = list(set(data["六车间生产入库_日期"]))
|
||||
data["六车间生产入库_日期"].sort()
|
||||
data["六车间生产入库_小日期"] = max(data["六车间生产入库_日期"]).strftime("%Y-%m-%d")
|
||||
data["六车间生产入库_大日期"] = min(data["六车间生产入库_日期"]).strftime("%Y-%m-%d")
|
||||
data["六车间生产入库_小日期"] = min(data["六车间生产入库_日期"]).strftime("%Y-%m-%d")
|
||||
data["六车间生产入库_大日期"] = max(data["六车间生产入库_日期"]).strftime("%Y-%m-%d")
|
||||
data["六车间生产入库_日期"] = ";".join([item.strftime("%Y-%m-%d") for item in data["六车间生产入库_日期"]])
|
||||
|
||||
# 成品检验数据
|
||||
|
|
@ -497,8 +497,8 @@ def main(batch: str, mgroup_obj=None):
|
|||
data['销售发货_仓库执行人'] = ";".join([item.name for item in data['销售发货_仓库执行人']])
|
||||
data["销售发货_日期"] = list(set(data["销售发货_日期"]))
|
||||
data["销售发货_日期"].sort()
|
||||
data["销售发货_小日期"] = max(data["销售发货_日期"]).strftime("%Y-%m-%d")
|
||||
data["销售发货_大日期"] = min(data["销售发货_日期"]).strftime("%Y-%m-%d")
|
||||
data["销售发货_小日期"] = min(data["销售发货_日期"]).strftime("%Y-%m-%d")
|
||||
data["销售发货_大日期"] = max(data["销售发货_日期"]).strftime("%Y-%m-%d")
|
||||
data["销售发货_日期"] = ";".join([item.strftime("%Y-%m-%d") for item in data["销售发货_日期"]])
|
||||
|
||||
# if data.get("六车间领料_count", 0) > 0 or data.get("六车间交接领料_count", 0) > 0:
|
||||
|
|
|
|||
|
|
@ -228,10 +228,11 @@ class MlogbDefectSerializer(CustomModelSerializer):
|
|||
defect_okcate = serializers.CharField(source="defect.okcate", read_only=True)
|
||||
class Meta:
|
||||
model = MlogbDefect
|
||||
fields = ["id", "defect_name", "count", "mlogb", "defect", "defect_okcate", "count_has"]
|
||||
fields = ["id", "defect_name", "count", "mlogb", "defect", "defect_okcate", "count_has", "is_inherited"]
|
||||
read_only_fields = EXCLUDE_FIELDS_BASE + ["mlogb"]
|
||||
extra_kwargs = {
|
||||
'count_has': {'required': False},
|
||||
'is_inherited': {'required': False},
|
||||
}
|
||||
|
||||
def validate(self, attrs):
|
||||
|
|
@ -300,6 +301,9 @@ class MlogListSerializer(CustomModelSerializer):
|
|||
|
||||
handle_users_ = UserSimpleSerializer(
|
||||
source='handle_users', many=True, read_only=True)
|
||||
wm_in_defect = serializers.PrimaryKeyRelatedField(source='wm_in.defect', read_only=True)
|
||||
wm_in_defect_name = serializers.CharField(source='wm_in.defect.name', read_only=True)
|
||||
wm_in_state = serializers.IntegerField(source='wm_in.state', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Mlog
|
||||
|
|
@ -314,6 +318,9 @@ class MlogbDetailSerializer(CustomModelSerializer):
|
|||
material_out_tracking = serializers.IntegerField(source="material_out.tracking", read_only=True)
|
||||
mlogbdefect = MlogbDefectSerializer(many=True, read_only=True)
|
||||
test_user_name = serializers.CharField(source='test_user.name', read_only=True)
|
||||
wm_in_defect = serializers.PrimaryKeyRelatedField(source='wm_in.defect', read_only=True)
|
||||
wm_in_defect_name = serializers.CharField(source='wm_in.defect.name', read_only=True)
|
||||
wm_in_state = serializers.IntegerField(source='wm_in.state', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Mlogb
|
||||
|
|
@ -367,6 +374,9 @@ class MlogSerializer(CustomModelSerializer):
|
|||
|
||||
mlogdefect = MlogbDefectSerializer(many=True, required=False)
|
||||
mlogindefect = MlogbDefectSerializer(many=True, label="前道不良", required=False)
|
||||
wm_in_defect = serializers.PrimaryKeyRelatedField(source='wm_in.defect', read_only=True)
|
||||
wm_in_defect_name = serializers.CharField(source='wm_in.defect.name', read_only=True)
|
||||
wm_in_state = serializers.IntegerField(source='wm_in.state', read_only=True)
|
||||
class Meta:
|
||||
model = Mlog
|
||||
fields = '__all__'
|
||||
|
|
@ -477,6 +487,7 @@ class MlogSerializer(CustomModelSerializer):
|
|||
if mlogb_defect_objects:
|
||||
MlogbDefect.objects.bulk_create(mlogb_defect_objects)
|
||||
mlogb.cal_count_notok(cal_mlog=False)
|
||||
mlogb.sync_inherited_defect(cal_count=True)
|
||||
instance.cal_mlog_count_from_mlogb()
|
||||
return instance
|
||||
|
||||
|
|
@ -567,7 +578,7 @@ class MlogSerializer(CustomModelSerializer):
|
|||
mox.save()
|
||||
Mlogb.objects.filter(mlog=instance, material_out__isnull=False).exclude(id=mox.id).delete()
|
||||
if need_mdefect:
|
||||
MlogbDefect.objects.filter(mlogb__mlog=instance).delete()
|
||||
MlogbDefect.objects.filter(mlogb__mlog=instance, is_inherited=False).delete()
|
||||
mlogb_defect_objects = [
|
||||
MlogbDefect(**{**item, "mlogb": mox, "id": idWorker.get_id()})
|
||||
for item in mlogdefect if item["count"] > 0
|
||||
|
|
@ -575,6 +586,7 @@ class MlogSerializer(CustomModelSerializer):
|
|||
if mlogb_defect_objects:
|
||||
MlogbDefect.objects.bulk_create(mlogb_defect_objects)
|
||||
mox.cal_count_notok(cal_mlog=False)
|
||||
mox.sync_inherited_defect(cal_count=True)
|
||||
instance.cal_mlog_count_from_mlogb()
|
||||
return instance
|
||||
|
||||
|
|
@ -1111,6 +1123,8 @@ class MlogbOutUpdateSerializer(CustomModelSerializer):
|
|||
if mlogb_defect_objects:
|
||||
MlogbDefect.objects.bulk_create(mlogb_defect_objects)
|
||||
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
|
||||
|
||||
def validate(self, attrs):
|
||||
|
|
|
|||
|
|
@ -610,6 +610,18 @@ def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
|
|||
mlog.submit_user = None
|
||||
mlog.save()
|
||||
|
||||
# 兜底清理历史 is_fix 错误带入的继承缺陷
|
||||
if is_fix:
|
||||
legacy_mlogbouts = list(Mlogb.objects.filter(
|
||||
mlog=mlog,
|
||||
material_out__isnull=False,
|
||||
mlogbdefect__is_inherited=True,
|
||||
).distinct())
|
||||
if legacy_mlogbouts:
|
||||
for mlogbout in legacy_mlogbouts:
|
||||
mlogbout.sync_inherited_defect(cal_count=True)
|
||||
mlog.cal_mlog_count_from_mlogb()
|
||||
|
||||
# mtask变更状态
|
||||
update_mtaskIds = []
|
||||
if mlog.mtask:
|
||||
|
|
|
|||
|
|
@ -65,16 +65,25 @@ def get_f_l_date(data):
|
|||
if v:
|
||||
if isinstance(v, list):
|
||||
myLogger.error(f"get_f_l_date {k} {v}")
|
||||
v = v.split(";")
|
||||
if first_date is None:
|
||||
first_date = min(v)
|
||||
else:
|
||||
first_date = min(first_date, min(v))
|
||||
if last_date is None:
|
||||
last_date = max(v)
|
||||
else:
|
||||
last_date = max(last_date, max(v))
|
||||
return {"first_date": first_date,
|
||||
"last_date": last_date,
|
||||
"first_time": datetime.strptime(f"{first_date} 00:00:00", "%Y-%m-%d %H:%M:%S").replace(tzinfo=tz_shanghai) if first_date else None,
|
||||
"last_time": datetime.strptime(f"{last_date} 23:59:59", "%Y-%m-%d %H:%M:%S").replace(tzinfo=tz_shanghai) if last_date else None}
|
||||
continue
|
||||
dates = []
|
||||
for s in v.split(";"):
|
||||
s = s.strip()
|
||||
if not s:
|
||||
continue
|
||||
try:
|
||||
dates.append(datetime.strptime(s, "%Y-%m-%d").date())
|
||||
except ValueError:
|
||||
myLogger.error(f"get_f_l_date invalid date {k} {s}")
|
||||
if not dates:
|
||||
continue
|
||||
cur_min = min(dates)
|
||||
cur_max = max(dates)
|
||||
if first_date is None or cur_min < first_date:
|
||||
first_date = cur_min
|
||||
if last_date is None or cur_max > last_date:
|
||||
last_date = cur_max
|
||||
return {"first_date": first_date.strftime("%Y-%m-%d") if first_date else None,
|
||||
"last_date": last_date.strftime("%Y-%m-%d") if last_date else None,
|
||||
"first_time": datetime.combine(first_date, datetime.min.time()).replace(tzinfo=tz_shanghai) if first_date else None,
|
||||
"last_time": datetime.combine(last_date, datetime.max.time().replace(microsecond=0)).replace(tzinfo=tz_shanghai) if last_date else None}
|
||||
|
|
|
|||
|
|
@ -274,6 +274,7 @@ class MlogViewSet(CustomModelViewSet):
|
|||
"material_out__process",
|
||||
"mgroup__process",
|
||||
"submit_user",
|
||||
"wm_in__defect",
|
||||
]
|
||||
# select_related_fields = ['create_by', 'update_by', 'mtask', 'mtaskb', 'mgroup',
|
||||
# 'handle_user', 'handle_user_2', 'equipment', 'mgroup__belong_dept',
|
||||
|
|
@ -574,7 +575,7 @@ class HandoverViewSet(CustomModelViewSet):
|
|||
serializer_class = HandoverSerializer
|
||||
select_related_fields = ["send_user", "send_mgroup", "send_dept", "recive_user", "recive_mgroup", "recive_dept", "wm", "material_changed", "material"]
|
||||
filterset_class = HandoverFilter
|
||||
search_fields = ["id", "material__name", "material__number", "material__specification", "batch", "material__model", "b_handover__batch", "new_batch", "wm__batch"]
|
||||
search_fields = ["material__name", "material__number", "material__specification", "batch", "material__model", "b_handover__batch", "new_batch", "wm__batch"]
|
||||
prefetch_related_fields = [Prefetch("b_handover", queryset=Handoverb.objects.select_related("wm__defect"))]
|
||||
|
||||
def perform_destroy(self, instance: Handover):
|
||||
|
|
@ -764,7 +765,10 @@ class MlogbViewSet(CustomListModelMixin, CustomGenericViewSet):
|
|||
perms_map = {"get": "*"}
|
||||
queryset = Mlogb.objects.all()
|
||||
serializer_class = MlogbDetailSerializer
|
||||
select_related_fields = ["material_out", "material_in", "test_user"]
|
||||
select_related_fields = ["material_out", "material_in", "test_user", "wm_in__defect"]
|
||||
prefetch_related_fields = [
|
||||
Prefetch("mlogbdefect_set", queryset=MlogbDefect.objects.select_related("defect")),
|
||||
]
|
||||
filterset_class = MlogbFilter
|
||||
ordering = ["create_time"]
|
||||
|
||||
|
|
@ -977,6 +981,8 @@ class MlogbInViewSet(BulkCreateModelMixin, BulkUpdateModelMixin, BulkDestroyMode
|
|||
Mlogbw.objects.get_or_create(number=numberx, mlogb=mlogbout)
|
||||
else:
|
||||
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()
|
||||
|
||||
def perform_create(self, serializer):
|
||||
|
|
|
|||
16
changelog.md
16
changelog.md
|
|
@ -1,3 +1,19 @@
|
|||
## 3.1.2026042912
|
||||
|
||||
- feat: 新增功能
|
||||
- mio 列表接口新增 total_price 返回明细金额合计 [caoqianming]
|
||||
- handover移除search包含id [caoqianming]
|
||||
- shift 视图改为完整 CRUD 以支持班次管理页 [caoqianming]
|
||||
- enm 修改重跑能源计算 不用从mplogx 开始计算 [TianyangZhang]
|
||||
- 光芯科技 主要修改采购功能 [TianyangZhang]
|
||||
- add contract settlement workflows [caoqianming]
|
||||
- batch_bxerp添加子工序操作人 [caoqianming]
|
||||
- 修改 hrm & rpm 代码 [TianyangZhang]
|
||||
- 修改光芯人员导入功能 [TianyangZhang]
|
||||
- fix: 问题修复
|
||||
- 批次统计修复小/大日期反向并增强除零兜底 [caoqianming]
|
||||
- mlogdefect 空列表时仍同步继承缺陷 [caoqianming]
|
||||
- 重建wpr [caoqianming]
|
||||
## 3.1.2026033008
|
||||
- feat: 新增功能
|
||||
- WMaterialViewSet 添加手动来料创建和删除接口 [caoqianming]
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ sys.path.insert(0, os.path.join(BASE_DIR, 'apps'))
|
|||
ALLOWED_HOSTS = ['*']
|
||||
|
||||
SYS_NAME = '星途工厂综合管理系统'
|
||||
SYS_VERSION = '3.1.2026033008'
|
||||
SYS_VERSION = '3.1.2026042912'
|
||||
X_FRAME_OPTIONS = 'SAMEORIGIN'
|
||||
|
||||
# Application definition
|
||||
|
|
|
|||
Loading…
Reference in New Issue