feat(scrap): 报废申请审批流 + 按单报废(后端)
- ScrapApproval 模型 + ScrapApprovalService:提交/列表/审批/执行(锁库存扣 available_quantity 写 TransScrap) - 新路由 POST /scrap/request、/request/check-approval、GET /request、PATCH /request/<id>/approve、POST /request/<id>/execute;权限码 scrap_apply/scrap_approval/scrap_execute(无角色硬编码) - 旧 /scrap 直接报废入口保持不变 - /inbound/stock/list 每项返回 source_table,供按单流程精准选库存
This commit is contained in:
@ -315,6 +315,7 @@ def _do_get_stock_list(permission_prefix=None):
|
||||
rows = q.all()
|
||||
for item in rows:
|
||||
d = item.to_dict()
|
||||
d['source_table'] = 'stock_buy' # ★ 报废等按单流程需要精准定位来源表
|
||||
d['stock_type'] = 'material'
|
||||
d['type'] = 'material'
|
||||
d['typeLabel'] = '采购件'
|
||||
@ -341,6 +342,7 @@ def _do_get_stock_list(permission_prefix=None):
|
||||
rows = q.all()
|
||||
for item in rows:
|
||||
d = item.to_dict()
|
||||
d['source_table'] = 'stock_semi' # ★ 精准来源表
|
||||
d['stock_type'] = 'semi'
|
||||
d['type'] = 'semi'
|
||||
d['typeLabel'] = '半成品'
|
||||
@ -370,6 +372,7 @@ def _do_get_stock_list(permission_prefix=None):
|
||||
rows = q.all()
|
||||
for item in rows:
|
||||
d = item.to_dict()
|
||||
d['source_table'] = 'stock_product' # ★ 精准来源表
|
||||
d['stock_type'] = 'product'
|
||||
d['type'] = 'product'
|
||||
d['typeLabel'] = '成品'
|
||||
|
||||
@ -466,3 +466,159 @@ class ScrapService:
|
||||
'page': page,
|
||||
'pageSize': page_size
|
||||
}
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# 报废审批流(申请 → 审批 → 按单执行)—— 镜像 借库/出库 审批框架
|
||||
# 与旧 /scrap(直接报废)并存,旧入口不动
|
||||
# ==============================================================================
|
||||
|
||||
def _current_user_role():
|
||||
try:
|
||||
claims = get_jwt()
|
||||
return (claims.get('role') or '').upper()
|
||||
except Exception:
|
||||
return ''
|
||||
|
||||
|
||||
@scrap_bp.route('/request/check-approval', methods=['POST'])
|
||||
@jwt_required()
|
||||
@permission_required('scrap_apply')
|
||||
def scrap_check_approval():
|
||||
"""提交前预检:判断所选库存(物料)是否命中“需审批”"""
|
||||
try:
|
||||
from app.services.approval_control import resolve_approval_control
|
||||
data = request.get_json() or {}
|
||||
items = data.get('items', []) or []
|
||||
need, flagged = resolve_approval_control(items)
|
||||
return jsonify({'code': 200, 'msg': 'success',
|
||||
'data': {'need_approval': need, 'materials': flagged}}), 200
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({'code': 500, 'msg': f'预检失败: {str(e)}'}), 500
|
||||
|
||||
|
||||
@scrap_bp.route('/request', methods=['POST'])
|
||||
@jwt_required()
|
||||
@permission_required('scrap_apply')
|
||||
def create_scrap_request():
|
||||
"""提交报废申请(不扣库存;扣减在库管执行时)"""
|
||||
try:
|
||||
from app.services.scrap_approval_service import ScrapApprovalService
|
||||
identity = get_jwt_identity()
|
||||
if not identity:
|
||||
return jsonify({'code': 401, 'msg': '用户未登录'}), 401
|
||||
data = request.get_json() or {}
|
||||
req = ScrapApprovalService.submit_approval(
|
||||
applicant_id=int(identity),
|
||||
items=data.get('items', []),
|
||||
allowed_approvers=data.get('allowed_approvers'),
|
||||
remark=data.get('remark'),
|
||||
approver_id=data.get('approver_id'),
|
||||
force_approval=(_current_user_role() == 'WAREHOUSE_MGR'),
|
||||
)
|
||||
return jsonify({'code': 200, 'msg': '报废申请已提交', 'data': req.to_dict()}), 200
|
||||
except ValueError as e:
|
||||
return jsonify({'code': 400, 'msg': str(e)}), 400
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({'code': 500, 'msg': f'提交报废申请失败: {str(e)}'}), 500
|
||||
|
||||
|
||||
@scrap_bp.route('/request', methods=['GET'])
|
||||
@jwt_required()
|
||||
@permission_required('scrap_approval')
|
||||
def list_scrap_requests():
|
||||
"""
|
||||
报废申请列表。
|
||||
scope: mine(我提交的,默认) / pending(待我审批) / executable(待执行) / all(管理者)
|
||||
Query: page, limit, status(可选覆盖)
|
||||
"""
|
||||
try:
|
||||
from app.services.scrap_approval_service import ScrapApprovalService
|
||||
from app.utils.decorators import is_privileged_viewer
|
||||
identity = int(get_jwt_identity())
|
||||
|
||||
scope = (request.args.get('scope') or 'mine').lower()
|
||||
page = int(request.args.get('page', 1))
|
||||
limit = int(request.args.get('limit', 10))
|
||||
status = request.args.get('status')
|
||||
status = int(status) if status not in (None, '', 'all') else None
|
||||
priv = is_privileged_viewer()
|
||||
|
||||
kwargs = {'page': page, 'limit': limit, 'status': status}
|
||||
if scope == 'pending':
|
||||
kwargs['status'] = 0
|
||||
kwargs['approver_id'] = identity
|
||||
elif scope == 'executable':
|
||||
kwargs['status'] = 1
|
||||
elif scope == 'all':
|
||||
if not priv:
|
||||
kwargs['applicant_id'] = identity
|
||||
else: # mine
|
||||
kwargs['applicant_id'] = identity
|
||||
|
||||
data = ScrapApprovalService.get_list(**kwargs)
|
||||
return jsonify({'code': 200, 'msg': '获取成功', 'data': data}), 200
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({'code': 500, 'msg': f'获取报废申请列表失败: {str(e)}'}), 500
|
||||
|
||||
|
||||
@scrap_bp.route('/request/<int:request_id>', methods=['GET'])
|
||||
@jwt_required()
|
||||
@permission_required('scrap_approval')
|
||||
def get_scrap_request_detail(request_id):
|
||||
try:
|
||||
from app.models.scrap_approval import ScrapApproval
|
||||
from app.utils.decorators import is_privileged_viewer
|
||||
req = db.session.get(ScrapApproval, request_id)
|
||||
if not req:
|
||||
return jsonify({'code': 404, 'msg': '报废申请不存在'}), 404
|
||||
if not is_privileged_viewer() and int(req.applicant_id) != int(get_jwt_identity()):
|
||||
# 被指定审批人也可查看
|
||||
allowed = req.get_allowed_approvers() or []
|
||||
if str(get_jwt_identity()) not in [str(a.get('value')) for a in allowed if a.get('type') == 'user']:
|
||||
return jsonify({'code': 403, 'msg': '无权查看该报废申请'}), 403
|
||||
return jsonify({'code': 200, 'msg': '获取成功', 'data': req.to_dict()}), 200
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({'code': 500, 'msg': f'获取报废申请详情失败: {str(e)}'}), 500
|
||||
|
||||
|
||||
@scrap_bp.route('/request/<int:request_id>/approve', methods=['PATCH'])
|
||||
@jwt_required()
|
||||
@permission_required('scrap_approval')
|
||||
def approve_scrap_request(request_id):
|
||||
"""审批报废申请(仅被指定审批人)"""
|
||||
try:
|
||||
from app.services.scrap_approval_service import ScrapApprovalService
|
||||
data = request.get_json() or {}
|
||||
req = ScrapApprovalService.approve(
|
||||
request_id, int(get_jwt_identity()),
|
||||
data.get('action'), data.get('reject_reason', '')
|
||||
)
|
||||
return jsonify({'code': 200, 'msg': '操作成功', 'data': req.to_dict()}), 200
|
||||
except ValueError as e:
|
||||
return jsonify({'code': 400, 'msg': str(e)}), 400
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({'code': 500, 'msg': f'审批报废申请失败: {str(e)}'}), 500
|
||||
|
||||
|
||||
@scrap_bp.route('/request/<int:request_id>/execute', methods=['POST'])
|
||||
@jwt_required()
|
||||
@permission_required('scrap_execute')
|
||||
def execute_scrap_request(request_id):
|
||||
"""按单报废:有 scrap_execute 权限者执行,扣减实物库存并写报废流水"""
|
||||
try:
|
||||
from app.services.scrap_approval_service import ScrapApprovalService
|
||||
identity = int(get_jwt_identity())
|
||||
u = SysUser.query.get(identity)
|
||||
req = ScrapApprovalService.execute(request_id, operator_name=(u.username if u else str(identity)))
|
||||
return jsonify({'code': 200, 'msg': '已执行报废并扣减库存', 'data': req.to_dict()}), 200
|
||||
except ValueError as e:
|
||||
return jsonify({'code': 400, 'msg': str(e)}), 400
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({'code': 500, 'msg': f'执行报废失败: {str(e)}'}), 500
|
||||
|
||||
90
inventory-backend/app/models/scrap_approval.py
Normal file
90
inventory-backend/app/models/scrap_approval.py
Normal file
@ -0,0 +1,90 @@
|
||||
from app.extensions import db
|
||||
import json
|
||||
|
||||
|
||||
def _beijing_now():
|
||||
from datetime import datetime, timezone, timedelta
|
||||
return datetime.now(timezone(timedelta(hours=8)))
|
||||
|
||||
|
||||
class ScrapApproval(db.Model):
|
||||
"""
|
||||
报废申请/审批单(镜像 BorrowApproval / OutboundApproval)
|
||||
|
||||
与出/借库的关键差异:items_json 里每条明细必须携带
|
||||
source_table + stock_id(精准定位到某条实物库存记录),
|
||||
因为报废从“申请那一刻”就要求指明具体要报废的东西。
|
||||
"""
|
||||
__tablename__ = 'scrap_approval'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
request_no = db.Column(db.String(100), unique=True, nullable=False, index=True)
|
||||
|
||||
applicant_id = db.Column(db.Integer, nullable=False, index=True)
|
||||
remark = db.Column(db.Text) # 报废原因
|
||||
|
||||
# 状态: 0-待审批, 1-已通过(待执行), 2-已驳回, 3-已执行(已报废)
|
||||
status = db.Column(db.Integer, default=0, nullable=False, index=True)
|
||||
|
||||
allowed_approvers = db.Column(db.Text) # JSON: [{"type":"user","value":123}, ...]
|
||||
actual_approver_id = db.Column(db.Integer, index=True)
|
||||
approved_at = db.Column(db.DateTime)
|
||||
reject_reason = db.Column(db.Text)
|
||||
|
||||
# 明细快照: [{"source_table","stock_id","sku","name","spec_model","location","batch_number","scrap_qty"}]
|
||||
items_json = db.Column(db.Text, nullable=False)
|
||||
|
||||
# 执行信息
|
||||
executed_at = db.Column(db.DateTime)
|
||||
executor_name = db.Column(db.String(100))
|
||||
|
||||
created_at = db.Column(db.DateTime, default=_beijing_now, nullable=False)
|
||||
updated_at = db.Column(db.DateTime, default=_beijing_now, onupdate=_beijing_now, nullable=False)
|
||||
|
||||
def set_items(self, items):
|
||||
self.items_json = json.dumps(items or [], ensure_ascii=False)
|
||||
|
||||
def get_items(self):
|
||||
try:
|
||||
return json.loads(self.items_json) if self.items_json else []
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def set_allowed_approvers(self, approvers):
|
||||
self.allowed_approvers = json.dumps(approvers or [], ensure_ascii=False)
|
||||
|
||||
def get_allowed_approvers(self):
|
||||
try:
|
||||
return json.loads(self.allowed_approvers) if self.allowed_approvers else []
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def _user_name(user_id):
|
||||
if not user_id:
|
||||
return ''
|
||||
try:
|
||||
from app.models.system import SysUser
|
||||
u = SysUser.query.get(user_id)
|
||||
return u.username if u else ''
|
||||
except Exception:
|
||||
return ''
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'request_no': self.request_no,
|
||||
'applicant_id': self.applicant_id,
|
||||
'applicant_name': self._user_name(self.applicant_id),
|
||||
'remark': self.remark or '',
|
||||
'status': self.status,
|
||||
'allowed_approvers': self.get_allowed_approvers(),
|
||||
'actual_approver_id': self.actual_approver_id,
|
||||
'actual_approver_name': self._user_name(self.actual_approver_id),
|
||||
'approved_at': self.approved_at.strftime('%Y-%m-%d %H:%M:%S') if self.approved_at else None,
|
||||
'reject_reason': self.reject_reason or '',
|
||||
'items': self.get_items(),
|
||||
'executed_at': self.executed_at.strftime('%Y-%m-%d %H:%M:%S') if self.executed_at else None,
|
||||
'executor_name': self.executor_name or '',
|
||||
'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S') if self.created_at else None,
|
||||
}
|
||||
237
inventory-backend/app/services/scrap_approval_service.py
Normal file
237
inventory-backend/app/services/scrap_approval_service.py
Normal file
@ -0,0 +1,237 @@
|
||||
import logging
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
from sqlalchemy import func
|
||||
|
||||
from app.extensions import db
|
||||
from app.models.scrap_approval import ScrapApproval
|
||||
from app.models.transaction import TransScrap
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _beijing():
|
||||
return datetime.now(timezone(timedelta(hours=8)))
|
||||
|
||||
|
||||
STOCK_MODELS = {}
|
||||
|
||||
|
||||
def _stock_models():
|
||||
"""延迟导入三张实物库存表,避免循环依赖"""
|
||||
if not STOCK_MODELS:
|
||||
from app.models.inbound.buy import StockBuy
|
||||
from app.models.inbound.semi import StockSemi
|
||||
from app.models.inbound.product import StockProduct
|
||||
STOCK_MODELS.update({
|
||||
'stock_buy': StockBuy,
|
||||
'stock_semi': StockSemi,
|
||||
'stock_product': StockProduct,
|
||||
})
|
||||
return STOCK_MODELS
|
||||
|
||||
|
||||
class ScrapApprovalService:
|
||||
|
||||
@staticmethod
|
||||
def generate_request_no():
|
||||
now = _beijing()
|
||||
prefix = f"APR-SCRAP-{now.strftime('%Y%m%d-%H%M')}-"
|
||||
n = db.session.query(func.count(func.distinct(ScrapApproval.request_no))) \
|
||||
.filter(ScrapApproval.request_no.like(f"{prefix}%")).scalar() or 0
|
||||
return f"{prefix}{(n + 1):04d}"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 提交申请
|
||||
# ------------------------------------------------------------------
|
||||
@staticmethod
|
||||
def submit_approval(applicant_id, items, allowed_approvers=None, remark=None,
|
||||
approver_id=None, force_approval=False):
|
||||
"""
|
||||
提交报废申请(仅锁定“意向”,不扣库存;扣减在库管执行时进行)
|
||||
|
||||
items 每项必须包含 source_table + stock_id(精准实物),可带 scrap_qty / 快照字段。
|
||||
"""
|
||||
if not items:
|
||||
raise ValueError("报废明细不能为空")
|
||||
|
||||
models = _stock_models()
|
||||
normalized = []
|
||||
for idx, it in enumerate(items):
|
||||
st = (it.get('source_table') or '').strip()
|
||||
model = models.get(st)
|
||||
sid = it.get('stock_id')
|
||||
if not model or not sid:
|
||||
raise ValueError(f"第 {idx + 1} 条报废明细必须指定 source_table 与 stock_id(精准库存行)")
|
||||
try:
|
||||
sid = int(sid)
|
||||
except (TypeError, ValueError):
|
||||
raise ValueError(f"第 {idx + 1} 条 stock_id 无效")
|
||||
row = model.query.get(sid)
|
||||
if not row:
|
||||
raise ValueError(f"第 {idx + 1} 条对应的库存记录不存在")
|
||||
try:
|
||||
qty = float(it.get('scrap_qty') or 0)
|
||||
except (TypeError, ValueError):
|
||||
raise ValueError(f"第 {idx + 1} 条报废数量无效")
|
||||
if qty <= 0:
|
||||
raise ValueError(f"第 {idx + 1} 条报废数量必须大于 0")
|
||||
avail = float(getattr(row, 'available_quantity', 0) or 0)
|
||||
if qty > avail:
|
||||
raise ValueError(f"第 {idx + 1} 条报废数量({qty})超过可用库存({avail})")
|
||||
|
||||
base = getattr(row, 'base', None)
|
||||
normalized.append({
|
||||
'source_table': st,
|
||||
'stock_id': sid,
|
||||
'base_id': getattr(row, 'base_id', None),
|
||||
'sku': getattr(row, 'sku', '') or '',
|
||||
'name': (base.name if base else '') or it.get('name') or '',
|
||||
'spec_model': (base.spec_model if base else '') or it.get('spec_model') or '',
|
||||
'location': getattr(row, 'warehouse_location', '') or '',
|
||||
'batch_number': getattr(row, 'batch_number', '') or getattr(row, 'serial_number', '') or '',
|
||||
'scrap_qty': qty,
|
||||
'available_at_apply': avail,
|
||||
})
|
||||
|
||||
# ★ 是否需审批:命中需审批物料 或 库管代建(force_approval)
|
||||
from app.services.approval_control import resolve_approval_control
|
||||
need_approval, flagged_materials = resolve_approval_control(normalized)
|
||||
if force_approval:
|
||||
need_approval = True
|
||||
if need_approval and not approver_id:
|
||||
if force_approval:
|
||||
raise ValueError("库管代建报废申请必须选择审批人后再提交")
|
||||
_names = ";".join(f"{m['name']}({m['spec_model'] or '-'})" for m in flagged_materials)
|
||||
raise ValueError(f"以下物料需审批报废:{_names}。请选择审批人后再提交")
|
||||
|
||||
if approver_id:
|
||||
allowed_approvers = [{"type": "user", "value": int(approver_id)}]
|
||||
elif not need_approval:
|
||||
allowed_approvers = []
|
||||
|
||||
req = ScrapApproval(
|
||||
request_no=ScrapApprovalService.generate_request_no(),
|
||||
applicant_id=applicant_id,
|
||||
remark=remark,
|
||||
)
|
||||
req.set_items(normalized)
|
||||
req.set_allowed_approvers(allowed_approvers)
|
||||
|
||||
if need_approval:
|
||||
req.status = 0 # 待审批
|
||||
else:
|
||||
# 默认不审批:直接进入“待执行”
|
||||
req.status = 1
|
||||
req.actual_approver_id = applicant_id
|
||||
req.approved_at = _beijing()
|
||||
|
||||
db.session.add(req)
|
||||
db.session.commit()
|
||||
logger.info(f"[ScrapApproval] 提交成功 {req.request_no} need_approval={need_approval}")
|
||||
return req
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 列表
|
||||
# ------------------------------------------------------------------
|
||||
@staticmethod
|
||||
def get_list(page=1, limit=10, status=None, applicant_id=None, approver_id=None):
|
||||
query = ScrapApproval.query
|
||||
if status is not None:
|
||||
query = query.filter(ScrapApproval.status == status)
|
||||
if applicant_id is not None:
|
||||
query = query.filter(ScrapApproval.applicant_id == applicant_id)
|
||||
if approver_id is not None:
|
||||
query = query.filter(ScrapApproval.allowed_approvers.like(f'%"value": {approver_id}%'))
|
||||
query = query.order_by(ScrapApproval.created_at.desc())
|
||||
pg = query.paginate(page=page, per_page=limit, error_out=False)
|
||||
return {
|
||||
'items': [r.to_dict() for r in pg.items],
|
||||
'total': pg.total,
|
||||
'pages': pg.pages,
|
||||
'current_page': page,
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 审批
|
||||
# ------------------------------------------------------------------
|
||||
@staticmethod
|
||||
def approve(request_id, operator_id, action, reject_reason=None):
|
||||
req = db.session.get(ScrapApproval, request_id)
|
||||
if not req:
|
||||
raise ValueError("报废申请不存在")
|
||||
if req.status != 0:
|
||||
raise ValueError("当前状态不允许审批(仅待审批可操作)")
|
||||
|
||||
# 仅被指定的审批人可操作
|
||||
allowed = req.get_allowed_approvers() or []
|
||||
user_entries = [str(a.get('value')) for a in allowed if a.get('type') == 'user']
|
||||
if user_entries and str(operator_id) not in user_entries:
|
||||
raise ValueError("只有被指定的审批人可以审批该申请")
|
||||
|
||||
if action == 'approve':
|
||||
req.status = 1
|
||||
req.actual_approver_id = operator_id
|
||||
req.approved_at = _beijing()
|
||||
req.reject_reason = None
|
||||
elif action == 'reject':
|
||||
req.status = 2
|
||||
req.actual_approver_id = operator_id
|
||||
req.reject_reason = reject_reason or '未说明原因'
|
||||
else:
|
||||
raise ValueError("无效的审批动作")
|
||||
db.session.commit()
|
||||
logger.info(f"[ScrapApproval] {req.request_no} 审批 {action} by {operator_id}")
|
||||
return req
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 执行(按单报废:扣减实物库存 + 写报废流水)
|
||||
# ------------------------------------------------------------------
|
||||
@staticmethod
|
||||
def execute(request_id, operator_name='System'):
|
||||
models = _stock_models()
|
||||
req = db.session.get(ScrapApproval, request_id)
|
||||
if not req:
|
||||
raise ValueError("报废申请不存在")
|
||||
if req.status != 1:
|
||||
raise ValueError("仅“已通过(待执行)”的报废单可执行")
|
||||
|
||||
items = req.get_items()
|
||||
if not items:
|
||||
raise ValueError("报废明细为空,无法执行")
|
||||
|
||||
for it in items:
|
||||
model = models.get(it.get('source_table'))
|
||||
if not model:
|
||||
raise ValueError(f"不支持的报废来源:{it.get('source_table')}")
|
||||
row = model.query.with_for_update().get(int(it['stock_id']))
|
||||
if not row:
|
||||
raise ValueError(f"库存记录已不存在({it.get('sku')})")
|
||||
qty = float(it.get('scrap_qty') or 0)
|
||||
avail = float(getattr(row, 'available_quantity', 0) or 0)
|
||||
if qty <= 0 or qty > avail:
|
||||
raise ValueError(f"库存 {it.get('sku') or ''} 可用不足(剩 {avail}),无法报废 {qty}")
|
||||
|
||||
# ★ 真正扣减可用库存
|
||||
row.available_quantity = avail - qty
|
||||
db.session.flush()
|
||||
|
||||
# 写报废流水(台账)
|
||||
db.session.add(TransScrap(
|
||||
sku=it.get('sku') or getattr(row, 'sku', '') or '',
|
||||
source_table=it.get('source_table'),
|
||||
stock_id=int(it['stock_id']),
|
||||
quantity=qty,
|
||||
reason=req.remark or '',
|
||||
operator_name=operator_name,
|
||||
approver_name=ScrapApproval._user_name(req.actual_approver_id),
|
||||
approval_status='executed',
|
||||
scrap_request_no=req.request_no,
|
||||
))
|
||||
|
||||
req.status = 3
|
||||
req.executed_at = _beijing()
|
||||
req.executor_name = operator_name
|
||||
db.session.commit()
|
||||
logger.info(f"[ScrapApproval] {req.request_no} 执行报废完成 by {operator_name}")
|
||||
return req
|
||||
Reference in New Issue
Block a user