Compare commits
4 Commits
e1e0bc1104
...
d4bf7c5e99
| Author | SHA1 | Date | |
|---|---|---|---|
| d4bf7c5e99 | |||
| 8356774a8a | |||
| 9a96fad3cf | |||
| 5ef98ef5b3 |
@ -187,7 +187,9 @@ class MaterialBaseService:
|
|||||||
inner_sub.c.total_avail,
|
inner_sub.c.total_avail,
|
||||||
MaterialWarningSetting.is_enabled.label('warning_enabled'),
|
MaterialWarningSetting.is_enabled.label('warning_enabled'),
|
||||||
MaterialWarningSetting.yellow_threshold.label('warning_yellow'),
|
MaterialWarningSetting.yellow_threshold.label('warning_yellow'),
|
||||||
MaterialWarningSetting.red_threshold.label('warning_red')
|
MaterialWarningSetting.red_threshold.label('warning_red'),
|
||||||
|
MaterialWarningSetting.red_emails.label('warning_red_emails'),
|
||||||
|
MaterialWarningSetting.yellow_emails.label('warning_yellow_emails')
|
||||||
).outerjoin(inner_sub, MaterialBase.id == inner_sub.c.base_id) \
|
).outerjoin(inner_sub, MaterialBase.id == inner_sub.c.base_id) \
|
||||||
.outerjoin(MaterialWarningSetting, MaterialBase.id == MaterialWarningSetting.base_id)
|
.outerjoin(MaterialWarningSetting, MaterialBase.id == MaterialWarningSetting.base_id)
|
||||||
|
|
||||||
@ -441,6 +443,8 @@ class MaterialBaseService:
|
|||||||
warning_enabled = row[3] if len(row) > 3 else False
|
warning_enabled = row[3] if len(row) > 3 else False
|
||||||
warning_yellow = row[4] if len(row) > 4 else 0
|
warning_yellow = row[4] if len(row) > 4 else 0
|
||||||
warning_red = row[5] if len(row) > 5 else 0
|
warning_red = row[5] if len(row) > 5 else 0
|
||||||
|
warning_red_emails = row[6] if len(row) > 6 else None
|
||||||
|
warning_yellow_emails = row[7] if len(row) > 7 else None
|
||||||
|
|
||||||
# 安全兜底
|
# 安全兜底
|
||||||
if not hasattr(item, 'to_dict'):
|
if not hasattr(item, 'to_dict'):
|
||||||
@ -455,6 +459,8 @@ class MaterialBaseService:
|
|||||||
item_dict['warningEnabled'] = bool(warning_enabled) if warning_enabled is not None else False
|
item_dict['warningEnabled'] = bool(warning_enabled) if warning_enabled is not None else False
|
||||||
item_dict['warningYellow'] = float(warning_yellow) if warning_yellow is not None else None
|
item_dict['warningYellow'] = float(warning_yellow) if warning_yellow is not None else None
|
||||||
item_dict['warningRed'] = float(warning_red) if warning_red is not None else None
|
item_dict['warningRed'] = float(warning_red) if warning_red is not None else None
|
||||||
|
item_dict['warningRedEmails'] = warning_red_emails or ''
|
||||||
|
item_dict['warningYellowEmails'] = warning_yellow_emails or ''
|
||||||
|
|
||||||
# 计算预警状态
|
# 计算预警状态
|
||||||
if warning_enabled:
|
if warning_enabled:
|
||||||
|
|||||||
@ -101,6 +101,8 @@ class InventoryWarningService:
|
|||||||
|
|
||||||
total_red = 0
|
total_red = 0
|
||||||
total_yellow = 0
|
total_yellow = 0
|
||||||
|
total_red_cascaded = 0 # 红色顺延到黄色
|
||||||
|
total_yellow_cascaded = 0 # 黄色顺延到红色
|
||||||
sent_red = False
|
sent_red = False
|
||||||
sent_yellow = False
|
sent_yellow = False
|
||||||
processed_settings = []
|
processed_settings = []
|
||||||
@ -121,7 +123,18 @@ class InventoryWarningService:
|
|||||||
if red_th is not None and inv <= red_th:
|
if red_th is not None and inv <= red_th:
|
||||||
total_red += 1
|
total_red += 1
|
||||||
red_emails = InventoryWarningService._parse_emails(setting.red_emails)
|
red_emails = InventoryWarningService._parse_emails(setting.red_emails)
|
||||||
if red_emails:
|
emails_to_use = red_emails
|
||||||
|
use_yellow_channel = False # 是否走黄色通道(顺延时为 True)
|
||||||
|
if not emails_to_use:
|
||||||
|
# ★ 红色预警但无 red_emails,顺延使用 yellow_emails ★
|
||||||
|
emails_to_use = InventoryWarningService._parse_emails(setting.yellow_emails)
|
||||||
|
if emails_to_use:
|
||||||
|
total_yellow += 1
|
||||||
|
total_red_cascaded += 1
|
||||||
|
use_yellow_channel = True
|
||||||
|
print(f"[InventoryWarning] 物料「{name}」红色预警触发,但 red_emails 为空,顺延使用 yellow_emails 发黄色预警")
|
||||||
|
|
||||||
|
if emails_to_use:
|
||||||
processed_settings.append(setting)
|
processed_settings.append(setting)
|
||||||
row = {
|
row = {
|
||||||
'name': name,
|
'name': name,
|
||||||
@ -129,10 +142,14 @@ class InventoryWarningService:
|
|||||||
'qty': round(inv, 2),
|
'qty': round(inv, 2),
|
||||||
'threshold': round(red_th, 2),
|
'threshold': round(red_th, 2),
|
||||||
}
|
}
|
||||||
for email in red_emails:
|
if use_yellow_channel:
|
||||||
red_rows_by_email[email].append(row)
|
for email in emails_to_use:
|
||||||
|
yellow_rows_by_email[email].append(row)
|
||||||
|
else:
|
||||||
|
for email in emails_to_use:
|
||||||
|
red_rows_by_email[email].append(row)
|
||||||
else:
|
else:
|
||||||
print(f"[InventoryWarning] 物料「{name}」红单跳过:无 red_emails 配置")
|
print(f"[InventoryWarning] 物料「{name}」红单跳过:无 red_emails 且 yellow_emails 也为空")
|
||||||
|
|
||||||
# ★ 黄色预警:red_threshold < 库存 <= yellow_threshold,走 setting.yellow_emails ★
|
# ★ 黄色预警:red_threshold < 库存 <= yellow_threshold,走 setting.yellow_emails ★
|
||||||
elif (
|
elif (
|
||||||
@ -141,7 +158,18 @@ class InventoryWarningService:
|
|||||||
):
|
):
|
||||||
total_yellow += 1
|
total_yellow += 1
|
||||||
yellow_emails = InventoryWarningService._parse_emails(setting.yellow_emails)
|
yellow_emails = InventoryWarningService._parse_emails(setting.yellow_emails)
|
||||||
if yellow_emails:
|
emails_to_use = yellow_emails
|
||||||
|
use_red_channel = False # 是否走红色通道(顺延时为 True)
|
||||||
|
if not emails_to_use:
|
||||||
|
# ★ 黄色预警但无 yellow_emails,顺延使用 red_emails ★
|
||||||
|
emails_to_use = InventoryWarningService._parse_emails(setting.red_emails)
|
||||||
|
if emails_to_use:
|
||||||
|
total_red += 1
|
||||||
|
total_yellow_cascaded += 1
|
||||||
|
use_red_channel = True
|
||||||
|
print(f"[InventoryWarning] 物料「{name}」黄色预警触发,但 yellow_emails 为空,顺延使用 red_emails 发红色预警")
|
||||||
|
|
||||||
|
if emails_to_use:
|
||||||
processed_settings.append(setting)
|
processed_settings.append(setting)
|
||||||
row = {
|
row = {
|
||||||
'name': name,
|
'name': name,
|
||||||
@ -149,10 +177,14 @@ class InventoryWarningService:
|
|||||||
'qty': round(inv, 2),
|
'qty': round(inv, 2),
|
||||||
'threshold': round(yellow_th, 2),
|
'threshold': round(yellow_th, 2),
|
||||||
}
|
}
|
||||||
for email in yellow_emails:
|
if use_red_channel:
|
||||||
yellow_rows_by_email[email].append(row)
|
for email in emails_to_use:
|
||||||
|
red_rows_by_email[email].append(row)
|
||||||
|
else:
|
||||||
|
for email in emails_to_use:
|
||||||
|
yellow_rows_by_email[email].append(row)
|
||||||
else:
|
else:
|
||||||
print(f"[InventoryWarning] 物料「{name}」黄单跳过:无 yellow_emails 配置")
|
print(f"[InventoryWarning] 物料「{name}」黄单跳过:无 yellow_emails 且 red_emails 也为空")
|
||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -193,6 +225,8 @@ class InventoryWarningService:
|
|||||||
return {
|
return {
|
||||||
'red_count': total_red,
|
'red_count': total_red,
|
||||||
'yellow_count': total_yellow,
|
'yellow_count': total_yellow,
|
||||||
|
'red_cascaded_count': total_red_cascaded,
|
||||||
|
'yellow_cascaded_count': total_yellow_cascaded,
|
||||||
'red_sent': sent_red,
|
'red_sent': sent_red,
|
||||||
'yellow_sent': sent_yellow,
|
'yellow_sent': sent_yellow,
|
||||||
'timestamp': now.strftime('%Y-%m-%d %H:%M:%S')
|
'timestamp': now.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
|||||||
@ -17,4 +17,8 @@ qrcode[pil]>=7.4.2
|
|||||||
# [新增] 必须添加,用于处理 token 登录
|
# [新增] 必须添加,用于处理 token 登录
|
||||||
Flask-JWT-Extended==4.6.0
|
Flask-JWT-Extended==4.6.0
|
||||||
# [新增] Excel 处理库 (解决 No module named 'openpyxl' 报错)
|
# [新增] Excel 处理库 (解决 No module named 'openpyxl' 报错)
|
||||||
openpyxl>=3.1.2
|
openpyxl>=3.1.2
|
||||||
|
# [新增] 定时任务调度器 (库存预警每日邮件)
|
||||||
|
APScheduler==3.10.4
|
||||||
|
# [新增] 时区处理 (APScheduler 需要)
|
||||||
|
pytz
|
||||||
@ -1,9 +1,39 @@
|
|||||||
# inventory-backend/run.py
|
# inventory-backend/run.py
|
||||||
from app import create_app
|
from app import create_app
|
||||||
|
|
||||||
# Gunicorn 或 uWSGI 会寻找名为 'app' 的实例
|
|
||||||
app = create_app()
|
app = create_app()
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# 启动时注册库存预警定时任务(每天 9:30 北京时)
|
||||||
|
# =========================================================
|
||||||
|
from apscheduler.schedulers.background import BackgroundScheduler
|
||||||
|
from apscheduler.triggers.cron import CronTrigger
|
||||||
|
import pytz
|
||||||
|
|
||||||
|
beijing_tz = pytz.timezone('Asia/Shanghai')
|
||||||
|
|
||||||
|
|
||||||
|
def _run_warning_job():
|
||||||
|
with app.app_context():
|
||||||
|
try:
|
||||||
|
from app.services.inventory_task import InventoryWarningService
|
||||||
|
result = InventoryWarningService.check_and_send_warning_emails()
|
||||||
|
print(f"[Scheduler] 库存预警扫描完成: red={result['red_count']}, yellow={result['yellow_count']}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[Scheduler] 库存预警任务失败: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
scheduler = BackgroundScheduler(timezone=beijing_tz)
|
||||||
|
scheduler.add_job(
|
||||||
|
func=_run_warning_job,
|
||||||
|
trigger=CronTrigger(hour=14, minute=32, timezone=beijing_tz),
|
||||||
|
id='inventory_warning_daily',
|
||||||
|
name='库存预警每日邮件发送',
|
||||||
|
replace_existing=True
|
||||||
|
)
|
||||||
|
scheduler.start()
|
||||||
|
print("✅ 库存预警定时任务已启动(每天 9:30 北京时间执行)")
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# =================================================
|
# =================================================
|
||||||
# 路由打印调试 (启动时会在控制台列出所有 URL)
|
# 路由打印调试 (启动时会在控制台列出所有 URL)
|
||||||
|
|||||||
@ -302,6 +302,22 @@
|
|||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<el-table-column v-if="userStore.hasPermission('material_list:view_warning')" label="预警状态" width="120" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<template v-if="row.warningStatus === 2">
|
||||||
|
<el-tag type="danger" size="small">红色预警</el-tag>
|
||||||
|
<div style="font-size: 11px; color: #999;">阈值: {{ row.warningRed }}</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="row.warningStatus === 1">
|
||||||
|
<el-tag type="warning" size="small">黄色预警</el-tag>
|
||||||
|
<div style="font-size: 11px; color: #999;">阈值: {{ row.warningYellow }}</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="row.warningEnabled">
|
||||||
|
<el-tag type="success" size="small">已配置</el-tag>
|
||||||
|
</template>
|
||||||
|
<span v-else style="color: #c0c4cc;">-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column v-if="userStore.hasPermission('material_list:operation')" label="操作" width="280" fixed="right" align="center">
|
<el-table-column v-if="userStore.hasPermission('material_list:operation')" label="操作" width="280" fixed="right" align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button v-if="userStore.hasPermission('material_list:operation')" link type="primary" size="small" @click="handleEdit(scope.row)">编辑</el-button>
|
<el-button v-if="userStore.hasPermission('material_list:operation')" link type="primary" size="small" @click="handleEdit(scope.row)">编辑</el-button>
|
||||||
@ -659,6 +675,8 @@ interface MaterialBaseVO {
|
|||||||
availableCount?: number;
|
availableCount?: number;
|
||||||
warningStatus?: number;
|
warningStatus?: number;
|
||||||
warningOrdered?: boolean;
|
warningOrdered?: boolean;
|
||||||
|
warningRedEmails?: string;
|
||||||
|
warningYellowEmails?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface QueryParams {
|
interface QueryParams {
|
||||||
@ -1403,8 +1421,8 @@ const handleSetSingleWarning = (row: MaterialBaseVO) => {
|
|||||||
warningForm.isEnabled = row.warningEnabled || false;
|
warningForm.isEnabled = row.warningEnabled || false;
|
||||||
warningForm.redThreshold = row.warningRed;
|
warningForm.redThreshold = row.warningRed;
|
||||||
warningForm.yellowThreshold = row.warningYellow;
|
warningForm.yellowThreshold = row.warningYellow;
|
||||||
warningForm.redEmails = (row as any).redEmails || '';
|
warningForm.redEmails = (row as any).warningRedEmails || (row as any).redEmails || '';
|
||||||
warningForm.yellowEmails = (row as any).yellowEmails || '';
|
warningForm.yellowEmails = (row as any).warningYellowEmails || (row as any).yellowEmails || '';
|
||||||
|
|
||||||
warningDialog.title = '设置预警';
|
warningDialog.title = '设置预警';
|
||||||
warningDialog.visible = true;
|
warningDialog.visible = true;
|
||||||
|
|||||||
Reference in New Issue
Block a user