Compare commits
1 Commits
00839863f5
...
2.0权限管理
| Author | SHA1 | Date | |
|---|---|---|---|
| 259f3a7e0d |
@ -4,7 +4,8 @@
|
||||
"Bash(git add *)",
|
||||
"Bash(git commit *)",
|
||||
"Bash(git *)",
|
||||
"Bash(del *)"
|
||||
"Bash(del *)",
|
||||
"Bash(rm *)"
|
||||
]
|
||||
},
|
||||
"$version": 3
|
||||
|
||||
BIN
db_sync.sql.gz
BIN
db_sync.sql.gz
Binary file not shown.
BIN
deploy.tar.gz
BIN
deploy.tar.gz
Binary file not shown.
0
deploy_code.sh
Normal file → Executable file
0
deploy_code.sh
Normal file → Executable file
0
deploy_full.sh
Normal file → Executable file
0
deploy_full.sh
Normal file → Executable file
Binary file not shown.
@ -3,6 +3,7 @@
|
||||
from flask import Flask
|
||||
from config import Config
|
||||
from app.extensions import db, migrate, cors, jwt
|
||||
from app.api.v1.scan import scan_bp
|
||||
import os
|
||||
|
||||
|
||||
@ -213,6 +214,15 @@ def create_app():
|
||||
except ImportError as e:
|
||||
print(f"❌ 错误: Common 模块导入失败: {e}")
|
||||
|
||||
# -----------------------------------------------------
|
||||
# 2.13 注册扫码查库存模块 (Scan)
|
||||
# -----------------------------------------------------
|
||||
try:
|
||||
app.register_blueprint(scan_bp, url_prefix='/api/v1/scan')
|
||||
print("✅ Scan 模块注册成功")
|
||||
except Exception as e:
|
||||
print(f"❌ 错误: Scan 模块注册失败: {e}")
|
||||
|
||||
# =========================================================
|
||||
# 3. 预加载数据模型
|
||||
# =========================================================
|
||||
|
||||
@ -2,8 +2,10 @@ from flask import Blueprint
|
||||
from .inbound import inbound_bp
|
||||
from .bom import bom_bp
|
||||
from .common import common_bp
|
||||
from .scan import scan_bp
|
||||
|
||||
v1_bp = Blueprint('v1', __name__)
|
||||
v1_bp.register_blueprint(inbound_bp, url_prefix='/inbound')
|
||||
v1_bp.register_blueprint(bom_bp, url_prefix='/bom')
|
||||
v1_bp.register_blueprint(common_bp, url_prefix='/common')
|
||||
v1_bp.register_blueprint(scan_bp, url_prefix='/scan')
|
||||
|
||||
@ -381,6 +381,8 @@ def batch_set_warning():
|
||||
red_val = item.get('redThreshold')
|
||||
warning.yellow_threshold = float(yellow_val) if yellow_val is not None else 0
|
||||
warning.red_threshold = float(red_val) if red_val is not None else 0
|
||||
warning.yellow_emails = item.get('yellowEmails', warning.yellow_emails)
|
||||
warning.red_emails = item.get('redEmails', warning.red_emails)
|
||||
updated_count += 1
|
||||
else:
|
||||
# 创建新记录
|
||||
@ -390,7 +392,9 @@ def batch_set_warning():
|
||||
base_id=base_id,
|
||||
is_enabled=item.get('isEnabled', False),
|
||||
yellow_threshold=float(yellow_val) if yellow_val is not None else 0,
|
||||
red_threshold=float(red_val) if red_val is not None else 0
|
||||
red_threshold=float(red_val) if red_val is not None else 0,
|
||||
yellow_emails=item.get('yellowEmails', ''),
|
||||
red_emails=item.get('redEmails', '')
|
||||
)
|
||||
db.session.add(warning)
|
||||
created_count += 1
|
||||
@ -412,7 +416,48 @@ def batch_set_warning():
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# 2.6 批量设置强制质检 API (POST /api/v1/inbound/base/batch-inspection)
|
||||
# 2.6 标记已采购 API (POST /api/v1/inbound/base/warning/mark-ordered)
|
||||
# ==============================================================================
|
||||
@inbound_base_bp.route('/warning/mark-ordered', methods=['POST'])
|
||||
@permission_required('material_list:edit_warning')
|
||||
def mark_warning_ordered():
|
||||
"""
|
||||
前端标记预警物料已处理采购(标记 is_ordered)
|
||||
请求体格式: {"baseId": 123, "isOrdered": true}
|
||||
"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({"code": 400, "msg": "No data provided"}), 400
|
||||
|
||||
base_id = data.get('baseId')
|
||||
if not base_id:
|
||||
return jsonify({"code": 400, "msg": "baseId 不能为空"}), 400
|
||||
|
||||
is_ordered = bool(data.get('isOrdered', False))
|
||||
|
||||
warning = MaterialWarningSetting.query.filter_by(base_id=base_id).first()
|
||||
if not warning:
|
||||
return jsonify({"code": 404, "msg": f"物料ID {base_id} 的预警配置不存在"}), 404
|
||||
|
||||
warning.is_ordered = is_ordered
|
||||
db.session.commit()
|
||||
|
||||
status_text = "已标记为已采购" if is_ordered else "已重置为未采购"
|
||||
return jsonify({
|
||||
"code": 200,
|
||||
"msg": status_text,
|
||||
"data": warning.to_dict()
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
current_app.logger.error(f"标记已采购失败: {str(e)}")
|
||||
return jsonify({"code": 500, "msg": f"标记已采购失败: {str(e)}"}), 500
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# 2.7 批量设置强制质检 API (POST /api/v1/inbound/base/batch-inspection)
|
||||
# ==============================================================================
|
||||
@inbound_base_bp.route('/batch-inspection', methods=['POST'])
|
||||
@permission_required('material_list:operation')
|
||||
|
||||
69
inventory-backend/app/api/v1/scan/__init__.py
Normal file
69
inventory-backend/app/api/v1/scan/__init__.py
Normal file
@ -0,0 +1,69 @@
|
||||
"""
|
||||
扫码查库存接口(移动端专用)
|
||||
GET /api/v1/scan/inventory?barcode=xxx
|
||||
"""
|
||||
from flask import Blueprint, jsonify, request
|
||||
from app.extensions import db
|
||||
from app.models.base import MaterialBase
|
||||
from app.models.inbound.buy import StockBuy
|
||||
from app.models.inbound.product import StockProduct
|
||||
from app.models.inbound.semi import StockSemi
|
||||
|
||||
scan_bp = Blueprint('scan', __name__, url_prefix='/scan')
|
||||
|
||||
|
||||
def _build_response(stock_record, stock_type: str) -> dict:
|
||||
"""联表 MaterialBase 提取物料信息并组装返回结构"""
|
||||
material = MaterialBase.query.get(stock_record.base_id)
|
||||
return {
|
||||
'code': 200,
|
||||
'data': {
|
||||
'materialName': material.name if material else '未知物料',
|
||||
'spec': material.spec_model if material else '',
|
||||
'location': stock_record.warehouse_location or '',
|
||||
'quantity': float(stock_record.available_quantity) if stock_record.available_quantity else 0.0,
|
||||
'stockType': stock_type
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@scan_bp.route('/inventory', methods=['GET'])
|
||||
def scan_inventory():
|
||||
"""
|
||||
扫码精确查找库存
|
||||
入参: barcode (query string)
|
||||
逻辑: 在 StockBuy / StockProduct / StockSemi 三表中精确匹配,只要命中一张即返回
|
||||
"""
|
||||
barcode = (request.args.get('barcode') or '').strip()
|
||||
if not barcode:
|
||||
return jsonify({'code': 400, 'msg': 'barcode 参数不能为空'}), 400
|
||||
|
||||
# 1. 采购库
|
||||
buy = StockBuy.query.filter(
|
||||
StockBuy.barcode == barcode,
|
||||
StockBuy.stock_quantity > 0
|
||||
).first()
|
||||
if buy:
|
||||
return jsonify(_build_response(buy, '采购库'))
|
||||
|
||||
# 2. 成品库
|
||||
product = StockProduct.query.filter(
|
||||
StockProduct.barcode == barcode,
|
||||
StockProduct.stock_quantity > 0
|
||||
).first()
|
||||
if product:
|
||||
return jsonify(_build_response(product, '成品库'))
|
||||
|
||||
# 3. 半成品库
|
||||
semi = StockSemi.query.filter(
|
||||
StockSemi.barcode == barcode,
|
||||
StockSemi.stock_quantity > 0
|
||||
).first()
|
||||
if semi:
|
||||
return jsonify(_build_response(semi, '半成品库'))
|
||||
|
||||
# 4. 全部未命中
|
||||
return jsonify({
|
||||
'code': 404,
|
||||
'msg': f'未找到条码 [{barcode}] 对应的库存记录,或该物料当前库存为零'
|
||||
}), 404
|
||||
@ -101,6 +101,10 @@ class MaterialWarningSetting(db.Model):
|
||||
is_enabled = db.Column(db.Boolean, default=False, comment='是否启用预警')
|
||||
yellow_threshold = db.Column(db.Numeric(10, 2), nullable=True, comment='黄色预警阈值')
|
||||
red_threshold = db.Column(db.Numeric(10, 2), nullable=True, comment='红色预警阈值')
|
||||
yellow_emails = db.Column(db.String(500), nullable=True, comment='黄色预警通知邮箱')
|
||||
red_emails = db.Column(db.String(500), nullable=True, comment='红色预警通知邮箱')
|
||||
is_ordered = db.Column(db.Boolean, default=False, comment='是否已处理采购')
|
||||
last_notified_at = db.Column(db.DateTime, nullable=True, comment='上次邮件通知时间')
|
||||
|
||||
# 关联关系
|
||||
material = db.relationship('MaterialBase', back_populates='warning_settings')
|
||||
@ -111,5 +115,9 @@ class MaterialWarningSetting(db.Model):
|
||||
'baseId': self.base_id,
|
||||
'isEnabled': bool(self.is_enabled),
|
||||
'yellowThreshold': float(self.yellow_threshold) if self.yellow_threshold is not None else None,
|
||||
'redThreshold': float(self.red_threshold) if self.red_threshold is not None else None
|
||||
'redThreshold': float(self.red_threshold) if self.red_threshold is not None else None,
|
||||
'yellowEmails': self.yellow_emails or '',
|
||||
'redEmails': self.red_emails or '',
|
||||
'isOrdered': bool(self.is_ordered),
|
||||
'lastNotifiedAt': self.last_notified_at.strftime('%Y-%m-%d %H:%M:%S') if self.last_notified_at else None
|
||||
}
|
||||
@ -375,34 +375,29 @@ class MaterialBaseService:
|
||||
|
||||
if enable_warning_sort:
|
||||
print("====== [DEBUG] 成功进入预警强排逻辑 ======")
|
||||
# 直接在 order_by 中进行计算排序,不污染 select 列
|
||||
inv_val = inner_sub.c.total_inv
|
||||
red_val = cast(MaterialWarningSetting.red_threshold, Numeric)
|
||||
yellow_val = cast(MaterialWarningSetting.yellow_threshold, Numeric)
|
||||
|
||||
# 预警等级计算:红=2, 黄=1, 正常=0
|
||||
warning_level = case(
|
||||
(and_(MaterialWarningSetting.is_enabled.is_(True), inv_val <= red_val), 2),
|
||||
(and_(MaterialWarningSetting.is_enabled.is_(True), inv_val <= yellow_val), 1),
|
||||
(and_(MaterialWarningSetting.is_enabled.is_(True), red_val.isnot(None), inv_val <= red_val), 2),
|
||||
(and_(MaterialWarningSetting.is_enabled.is_(True), yellow_val.isnot(None), inv_val <= yellow_val), 1),
|
||||
else_=0
|
||||
)
|
||||
|
||||
# 统一计算缺口 (Shortage) = 目标阈值 - 当前库存
|
||||
# 红色算红色的缺口,黄色算黄色的缺口,越大说明缺的越多
|
||||
shortage = case(
|
||||
(and_(MaterialWarningSetting.is_enabled.is_(True), red_val.isnot(None), inv_val <= red_val), red_val - inv_val),
|
||||
(and_(MaterialWarningSetting.is_enabled.is_(True), yellow_val.isnot(None), inv_val <= yellow_val), yellow_val - inv_val),
|
||||
else_=0
|
||||
)
|
||||
# 红色预警时的缺口
|
||||
red_shortage = case(
|
||||
(and_(MaterialWarningSetting.is_enabled.is_(True), inv_val <= red_val), red_val - inv_val),
|
||||
else_=0
|
||||
)
|
||||
# 黄色预警时的缺口
|
||||
yellow_distance = case(
|
||||
(and_(MaterialWarningSetting.is_enabled.is_(True), inv_val > red_val, inv_val <= yellow_val), inv_val - red_val),
|
||||
else_=999999
|
||||
)
|
||||
|
||||
# 直接在 order_by 中使用 case() 表达式
|
||||
query = query.order_by(
|
||||
desc(warning_level),
|
||||
desc(red_shortage),
|
||||
asc(yellow_distance),
|
||||
desc(inv_val),
|
||||
desc(warning_level), # 1. 先按红、黄、正常排
|
||||
desc(shortage), # 2. 同级别内,缺口越大的排越上面
|
||||
desc(inv_val), # 3. 缺口一样,库存多的排上面
|
||||
desc(MaterialBase.id)
|
||||
)
|
||||
elif order_by_column:
|
||||
@ -462,12 +457,18 @@ class MaterialBaseService:
|
||||
item_dict['warningRed'] = float(warning_red) if warning_red is not None else None
|
||||
|
||||
# 计算预警状态
|
||||
if warning_enabled and warning_red is not None:
|
||||
if warning_enabled:
|
||||
invQty = item_dict['inventoryCount']
|
||||
if invQty <= warning_red:
|
||||
|
||||
# 优先判断红色预警(如果设置了红阈值,且库存 <= 红阈值)
|
||||
if warning_red is not None and invQty <= warning_red:
|
||||
item_dict['warningStatus'] = 2 # 红色
|
||||
|
||||
# 其次判断黄色预警(如果设置了黄阈值,且库存 <= 黄阈值)
|
||||
elif warning_yellow is not None and invQty <= warning_yellow:
|
||||
item_dict['warningStatus'] = 1 # 黄色
|
||||
|
||||
# 都不满足则正常
|
||||
else:
|
||||
item_dict['warningStatus'] = 0 # 正常
|
||||
else:
|
||||
|
||||
199
inventory-backend/app/services/inventory_task.py
Normal file
199
inventory-backend/app/services/inventory_task.py
Normal file
@ -0,0 +1,199 @@
|
||||
"""
|
||||
库存预警扫描与邮件通知服务
|
||||
|
||||
定时(或手动触发)扫描所有 is_enabled=True 且 is_ordered=False 的预警配置,
|
||||
按物料配置的邮箱独立发送,不依赖 SysUser 角色。
|
||||
|
||||
- 库存 <= red_threshold → 红色预警邮件(发 setting.red_emails)
|
||||
- red_threshold < 库存 <= yellow_threshold → 黄色预警邮件(发 setting.yellow_emails)
|
||||
- 同一收件人在多条记录中出现 → 聚合为一封邮件
|
||||
- 发送成功后更新 last_notified_at
|
||||
"""
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from collections import defaultdict
|
||||
from sqlalchemy import func
|
||||
|
||||
from app.extensions import db
|
||||
from app.models.base import MaterialBase, MaterialWarningSetting
|
||||
from app.models.inbound.buy import StockBuy
|
||||
from app.models.inbound.semi import StockSemi
|
||||
from app.models.inbound.product import StockProduct
|
||||
|
||||
|
||||
class InventoryWarningService:
|
||||
|
||||
@staticmethod
|
||||
def _get_total_inventory(base_id: int) -> float:
|
||||
"""
|
||||
计算指定物料在所有库存表(采购件 + 半成品 + 成品)中的总库存量
|
||||
"""
|
||||
buy_q = db.session.query(func.sum(StockBuy.stock_quantity)).filter(
|
||||
StockBuy.base_id == base_id
|
||||
).scalar() or 0
|
||||
semi_q = db.session.query(func.sum(StockSemi.stock_quantity)).filter(
|
||||
StockSemi.base_id == base_id
|
||||
).scalar() or 0
|
||||
prod_q = db.session.query(func.sum(StockProduct.stock_quantity)).filter(
|
||||
StockProduct.base_id == base_id
|
||||
).scalar() or 0
|
||||
return float(buy_q) + float(semi_q) + float(prod_q)
|
||||
|
||||
@staticmethod
|
||||
def _parse_emails(email_str: str) -> list:
|
||||
"""从逗号分隔字符串中提取并清洗有效邮箱列表"""
|
||||
if not email_str or not email_str.strip():
|
||||
return []
|
||||
return [e.strip() for e in email_str.split(',') if e.strip() and '@' in e.strip()]
|
||||
|
||||
@staticmethod
|
||||
def _build_text_table(rows: list, level: str) -> str:
|
||||
"""
|
||||
构建纯文本物料清单表格
|
||||
|
||||
Args:
|
||||
rows: [{"name": ..., "spec": ..., "qty": ..., "threshold": ...}, ...]
|
||||
level: "red" 或 "yellow",决定阈值列标题
|
||||
"""
|
||||
threshold_label = "红色阈值" if level == "red" else "黄色阈值"
|
||||
lines = [
|
||||
"名称 | 规格 | 当前库存 | " + threshold_label,
|
||||
"-" * 60,
|
||||
]
|
||||
for r in rows:
|
||||
name = r.get('name', '-') or '-'
|
||||
spec = r.get('spec', '-') or '-'
|
||||
qty = r.get('qty', '-')
|
||||
th = r.get('threshold', '-')
|
||||
lines.append(f"{name} | {spec} | {qty} | {th}")
|
||||
return '\n'.join(lines)
|
||||
|
||||
@staticmethod
|
||||
def check_and_send_warning_emails() -> dict:
|
||||
"""
|
||||
执行库存预警扫描与邮件发送
|
||||
|
||||
1. 查询所有 is_enabled=True 且 is_ordered=False 的预警配置
|
||||
2. 按 level 归类物料,按邮箱聚合(同一邮箱 → 一封邮件)
|
||||
3. 调用 send_email 发送,更新 last_notified_at
|
||||
|
||||
Returns:
|
||||
{
|
||||
"red_count": N, # 触发红色预警的物料数
|
||||
"yellow_count": N, # 触发黄色预警的物料数
|
||||
"red_sent": True/False,
|
||||
"yellow_sent": True/False,
|
||||
"timestamp": "..."
|
||||
}
|
||||
"""
|
||||
from app.utils.email_service import send_email
|
||||
|
||||
beijing_tz = timezone(timedelta(hours=8))
|
||||
now = datetime.now(beijing_tz)
|
||||
|
||||
# 查询启用了预警且未标记采购的配置
|
||||
settings = MaterialWarningSetting.query.filter(
|
||||
MaterialWarningSetting.is_enabled == True,
|
||||
MaterialWarningSetting.is_ordered == False
|
||||
).all()
|
||||
|
||||
red_rows_by_email = defaultdict(list) # email -> [物料row, ...]
|
||||
yellow_rows_by_email = defaultdict(list)
|
||||
|
||||
total_red = 0
|
||||
total_yellow = 0
|
||||
sent_red = False
|
||||
sent_yellow = False
|
||||
processed_settings = []
|
||||
|
||||
for setting in settings:
|
||||
base_id = setting.base_id
|
||||
material = MaterialBase.query.get(base_id)
|
||||
if not material:
|
||||
continue
|
||||
|
||||
name = material.name
|
||||
spec = material.spec_model or ''
|
||||
red_th = float(setting.red_threshold) if setting.red_threshold is not None else None
|
||||
yellow_th = float(setting.yellow_threshold) if setting.yellow_threshold is not None else None
|
||||
inv = InventoryWarningService._get_total_inventory(base_id)
|
||||
|
||||
# ★ 红色预警:库存 <= red_threshold,走 setting.red_emails ★
|
||||
if red_th is not None and inv <= red_th:
|
||||
total_red += 1
|
||||
red_emails = InventoryWarningService._parse_emails(setting.red_emails)
|
||||
if red_emails:
|
||||
processed_settings.append(setting)
|
||||
row = {
|
||||
'name': name,
|
||||
'spec': spec,
|
||||
'qty': round(inv, 2),
|
||||
'threshold': round(red_th, 2),
|
||||
}
|
||||
for email in red_emails:
|
||||
red_rows_by_email[email].append(row)
|
||||
else:
|
||||
print(f"[InventoryWarning] 物料「{name}」红单跳过:无 red_emails 配置")
|
||||
|
||||
# ★ 黄色预警:red_threshold < 库存 <= yellow_threshold,走 setting.yellow_emails ★
|
||||
elif (
|
||||
(red_th is not None and yellow_th is not None and red_th < inv <= yellow_th)
|
||||
or (red_th is None and yellow_th is not None and inv <= yellow_th)
|
||||
):
|
||||
total_yellow += 1
|
||||
yellow_emails = InventoryWarningService._parse_emails(setting.yellow_emails)
|
||||
if yellow_emails:
|
||||
processed_settings.append(setting)
|
||||
row = {
|
||||
'name': name,
|
||||
'spec': spec,
|
||||
'qty': round(inv, 2),
|
||||
'threshold': round(yellow_th, 2),
|
||||
}
|
||||
for email in yellow_emails:
|
||||
yellow_rows_by_email[email].append(row)
|
||||
else:
|
||||
print(f"[InventoryWarning] 物料「{name}」黄单跳过:无 yellow_emails 配置")
|
||||
else:
|
||||
continue
|
||||
|
||||
# ★ 按邮箱聚合,批量发送红色预警邮件 ★
|
||||
for email, rows in red_rows_by_email.items():
|
||||
table = InventoryWarningService._build_text_table(rows, 'red')
|
||||
subject = f"【红色预警】库存告急(共 {len(rows)} 条)"
|
||||
content = (
|
||||
f"您好,\n\n"
|
||||
f"以下物料当前库存已达到红色预警阈值,请立即处理采购:\n\n"
|
||||
f"{table}\n\n"
|
||||
"详情请登录仓库管理系统查看。\n\n"
|
||||
"此邮件由系统自动发送,请勿回复。"
|
||||
)
|
||||
send_email(email, subject, content)
|
||||
sent_red = True
|
||||
|
||||
# ★ 按邮箱聚合,批量发送黄色预警邮件 ★
|
||||
for email, rows in yellow_rows_by_email.items():
|
||||
table = InventoryWarningService._build_text_table(rows, 'yellow')
|
||||
subject = f"【黄色预警】库存偏低(共 {len(rows)} 条)"
|
||||
content = (
|
||||
f"您好,\n\n"
|
||||
f"以下物料当前库存已达到黄色预警阈值,请关注采购进度:\n\n"
|
||||
f"{table}\n\n"
|
||||
"详情请登录仓库管理系统查看。\n\n"
|
||||
"此邮件由系统自动发送,请勿回复。"
|
||||
)
|
||||
send_email(email, subject, content)
|
||||
sent_yellow = True
|
||||
|
||||
# ★ 批量更新 last_notified_at ★
|
||||
if processed_settings:
|
||||
for s in processed_settings:
|
||||
s.last_notified_at = now
|
||||
db.session.commit()
|
||||
|
||||
return {
|
||||
'red_count': total_red,
|
||||
'yellow_count': total_yellow,
|
||||
'red_sent': sent_red,
|
||||
'yellow_sent': sent_yellow,
|
||||
'timestamp': now.strftime('%Y-%m-%d %H:%M:%S')
|
||||
}
|
||||
@ -253,6 +253,13 @@ class OutboundService:
|
||||
)
|
||||
db.session.add(new_record)
|
||||
|
||||
# ★ 出库后检查低库存预警
|
||||
try:
|
||||
from app.utils.stock_alert import check_and_alert
|
||||
check_and_alert(stock_record.base_id)
|
||||
except Exception as e:
|
||||
current_app.logger.warning(f"⚠️ 低库存预警检查失败: {e}")
|
||||
|
||||
# ★ 如果关联了审批单,出库成功后更新审批单状态为"已完成"
|
||||
if approval:
|
||||
approval.status = 3 # 3-已完成
|
||||
|
||||
@ -159,6 +159,12 @@ def send_new_request_notify(to_emails: List[str], request_no: str,
|
||||
物料清单如下:
|
||||
{chr(10).join(rows)}
|
||||
|
||||
---
|
||||
⚡ 快速通道:
|
||||
请点击下方链接直接进入系统审批:
|
||||
https://172.16.0.198/outbound/approval
|
||||
---
|
||||
|
||||
请登录仓库管理系统进行审批。
|
||||
|
||||
此邮件由系统自动发送,请勿回复。
|
||||
|
||||
@ -77,4 +77,13 @@ export function getLatestSpecs() {
|
||||
url: '/inbound/base/spec-latest',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 8. 标记预警物料已采购
|
||||
export function markWarningOrdered(data: { baseId: number; isOrdered: boolean }) {
|
||||
return request({
|
||||
url: '/inbound/base/warning/mark-ordered',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
@ -302,10 +302,14 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="userStore.hasPermission('material_list:operation')" label="操作" min-width="200" fixed="right" align="center">
|
||||
<el-table-column v-if="userStore.hasPermission('material_list:operation')" label="操作" width="280" fixed="right" align="center">
|
||||
<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:edit_warning')" link type="warning" size="small" @click="handleSetSingleWarning(scope.row)">设置预警</el-button>
|
||||
<template v-if="userStore.hasPermission('material_list:edit_warning') && scope.row.warningStatus > 0">
|
||||
<el-button v-if="scope.row.warningOrdered" disabled size="small" type="info">采购在途</el-button>
|
||||
<el-button v-else link type="success" size="small" @click="handleMarkOrdered(scope.row)">标记已采购</el-button>
|
||||
</template>
|
||||
<el-button v-if="userStore.hasPermission('material_list:operation')" link type="danger" size="small" @click="handleDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@ -560,10 +564,16 @@
|
||||
<el-input-number v-model="warningForm.redThreshold" :min="0" :precision="0" step="1" placeholder="库存≤此值为红色预警" style="width: 100%" />
|
||||
<div class="form-tip">库存数量 ≤ 此值时显示红色预警</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="红色预警邮箱" v-if="warningForm.isEnabled">
|
||||
<el-input v-model="warningForm.redEmails" placeholder="逗号分隔多个邮箱" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="黄色阈值" prop="yellowThreshold" v-if="warningForm.isEnabled">
|
||||
<el-input-number v-model="warningForm.yellowThreshold" :min="0" :precision="0" step="1" placeholder="库存≤此值为黄色预警" style="width: 100%" />
|
||||
<div class="form-tip">红色阈值 < 库存 ≤ 此值时显示黄色预警</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="黄色预警邮箱" v-if="warningForm.isEnabled">
|
||||
<el-input v-model="warningForm.yellowEmails" placeholder="逗号分隔多个邮箱" clearable />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
@ -622,7 +632,8 @@ import {
|
||||
getMaterialBaseOptions,
|
||||
exportAssetStatistics,
|
||||
batchSetWarning,
|
||||
batchSetInspection
|
||||
batchSetInspection,
|
||||
markWarningOrdered
|
||||
} from '@/api/material_base';
|
||||
import { uploadFile, deleteFile } from '@/api/common/upload';
|
||||
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue';
|
||||
@ -646,6 +657,8 @@ interface MaterialBaseVO {
|
||||
statusLoading?: boolean;
|
||||
inventoryCount?: number;
|
||||
availableCount?: number;
|
||||
warningStatus?: number;
|
||||
warningOrdered?: boolean;
|
||||
}
|
||||
|
||||
interface QueryParams {
|
||||
@ -791,7 +804,9 @@ const warningLoading = ref(false);
|
||||
const warningForm = reactive({
|
||||
isEnabled: false,
|
||||
redThreshold: undefined as number | undefined,
|
||||
yellowThreshold: undefined as number | undefined
|
||||
yellowThreshold: undefined as number | undefined,
|
||||
redEmails: '',
|
||||
yellowEmails: ''
|
||||
});
|
||||
const warningRules = {
|
||||
yellowThreshold: [
|
||||
@ -1388,11 +1403,34 @@ const handleSetSingleWarning = (row: MaterialBaseVO) => {
|
||||
warningForm.isEnabled = row.warningEnabled || false;
|
||||
warningForm.redThreshold = row.warningRed;
|
||||
warningForm.yellowThreshold = row.warningYellow;
|
||||
warningForm.redEmails = (row as any).redEmails || '';
|
||||
warningForm.yellowEmails = (row as any).yellowEmails || '';
|
||||
|
||||
warningDialog.title = '设置预警';
|
||||
warningDialog.visible = true;
|
||||
};
|
||||
|
||||
// 标记预警物料已采购
|
||||
const handleMarkOrdered = (row: MaterialBaseVO) => {
|
||||
ElMessageBox.confirm(
|
||||
'确认已对该预警物料下单?标记后在途期间将不再发送预警邮件。',
|
||||
'确认标记已采购',
|
||||
{
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
).then(async () => {
|
||||
try {
|
||||
await markWarningOrdered({ baseId: row.id, isOrdered: true });
|
||||
ElMessage.success('已标记为已采购');
|
||||
getList();
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.msg || '标记失败');
|
||||
}
|
||||
}).catch(() => {});
|
||||
};
|
||||
|
||||
// 提交预警设置
|
||||
const submitWarning = async () => {
|
||||
if (!warningFormRef.value) return;
|
||||
@ -1415,7 +1453,9 @@ const submitWarning = async () => {
|
||||
baseId,
|
||||
isEnabled: warningForm.isEnabled,
|
||||
redThreshold: red,
|
||||
yellowThreshold: yellow
|
||||
yellowThreshold: yellow,
|
||||
redEmails: warningForm.redEmails || '',
|
||||
yellowEmails: warningForm.yellowEmails || ''
|
||||
}));
|
||||
|
||||
await batchSetWarning(data);
|
||||
@ -1463,17 +1503,13 @@ const submitBatchInspection = async () => {
|
||||
|
||||
// 表格行样式(根据预警状态)
|
||||
const tableRowClassName = ({ row }: { row: MaterialBaseVO }) => {
|
||||
// 只有拥有 view_warning 权限且有预警状态时才显示特殊样式
|
||||
if (!userStore.hasPermission('material_list:view_warning')) return '';
|
||||
|
||||
const status = (row as any).warningStatus;
|
||||
if (status === 2) {
|
||||
return 'warning-row-red'; // 红色预警
|
||||
} else if (status === 1) {
|
||||
return 'warning-row-yellow'; // 黄色预警
|
||||
if (row.warningStatus === 2) {
|
||||
return 'danger-row'; // 红色预警
|
||||
} else if (row.warningStatus === 1) {
|
||||
return 'warning-row'; // 黄色预警
|
||||
}
|
||||
return '';
|
||||
};
|
||||
}
|
||||
|
||||
// --- 文件上传辅助函数 ---
|
||||
|
||||
@ -1715,34 +1751,32 @@ onMounted(() => {
|
||||
.upload-file-item .el-upload-list__item-actions .el-icon { color: #fff; font-size: 20px; cursor: pointer; margin: 0 4px; }
|
||||
.upload-add-trigger { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; }
|
||||
|
||||
/* 预警行样式 - 加深颜色 */
|
||||
:deep(.warning-row-red) {
|
||||
--el-table-tr-bg-color: #ffcdd2 !important;
|
||||
background-color: #ffcdd2 !important;
|
||||
}
|
||||
:deep(.warning-row-red td) {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
:deep(.warning-row-yellow) {
|
||||
--el-table-tr-bg-color: #fff59d !important;
|
||||
background-color: #fff59d !important;
|
||||
}
|
||||
:deep(.warning-row-yellow td) {
|
||||
background-color: transparent !important;
|
||||
/* ================================================================
|
||||
Element Plus 表格预警行样式 & 固定列重叠修复
|
||||
================================================================ */
|
||||
|
||||
/* 黄色预警行底色 (全覆盖) */
|
||||
:deep(.el-table .warning-row),
|
||||
:deep(.el-table .warning-row > td.el-table__cell) {
|
||||
background-color: #fcedc4 !important; /* 明显的黄色 */
|
||||
}
|
||||
|
||||
/* 表单提示文字 */
|
||||
.form-tip {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 4px;
|
||||
line-height: 1.4;
|
||||
/* 红色预警行底色 (全覆盖) */
|
||||
:deep(.el-table .danger-row),
|
||||
:deep(.el-table .danger-row > td.el-table__cell) {
|
||||
background-color: #fcd3d3 !important; /* 明显的红色 */
|
||||
}
|
||||
|
||||
/* 固定列的按钮容器底色跟随所在行的背景色,视觉无缝融合 */
|
||||
:deep(.el-table .el-table__cell.is-fixed) {
|
||||
background-color: inherit !important;
|
||||
}
|
||||
|
||||
/* 按钮间距微调,更紧凑 */
|
||||
:deep(.el-table .el-table__cell.is-fixed .cell) {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
justify-content: flex-start; /* 左对齐更自然 */
|
||||
flex-wrap: nowrap; /* 尽量不换行 */
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* 增加下拉框的最大高度,使其能容纳更多选项而不必频繁滚动 */
|
||||
.long-dropdown .el-select-dropdown__wrap {
|
||||
max-height: 600px !important; /* 可以根据屏幕大小适当调整 */
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user