Files
KCGL/inventory-backend/app/models/outbound.py
yueli 39f3f8af45 feat(outbound): 审批单手动完结/作废功能
- 后端: outbound_service 新增 close_request 方法(状态 1-已通过 → 4-已完结)
- 后端: 新增 POST /api/v1/outbound/request/<id>/close 接口
- 模型: status_text 数组增加「已完结」
- 前端: create.vue 审批单选择区新增「强制完结此单」按钮
  完结后刷新列表,该单从已通过下拉中移除
- 权限: 超级管理员/审批人/拥有 outbound_create:operation 的用户可完结(库管可操作)
2026-08-31 10:15:33 +08:00

152 lines
6.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from app.extensions import db, beijing_time
from app.models.system import SysUser
from datetime import datetime
import json
class OutboundApproval(db.Model):
"""
出库审批单模型
用于管理出库申请的多级审批流程
"""
__tablename__ = 'outbound_approval'
id = db.Column(db.Integer, primary_key=True)
# 审批单号
request_no = db.Column(db.String(100), unique=True, nullable=False, index=True)
# 申请人ID
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)
# 允许审批的人员列表 (JSON格式: [{"type": "role", "value": "admin"}, {"type": "user", "value": "123"}])
allowed_approvers = db.Column(db.Text)
# 实际审批人ID (多人审批时记录第一个通过的)
actual_approver_id = db.Column(db.Integer, index=True)
# 审批时间
approved_at = db.Column(db.DateTime)
# 驳回原因
reject_reason = db.Column(db.Text)
# 明细快照 (存储出库物品的名称、规格、库位、数量等信息无SKU字段)
items_json = db.Column(db.Text)
# 创建时间和更新时间
created_at = db.Column(db.DateTime, default=beijing_time, nullable=False)
updated_at = db.Column(db.DateTime, default=beijing_time, onupdate=beijing_time, nullable=False)
def _safe_parse_json(self, value):
"""
安全解析 JSON 字段:
- 如果 value 已是 list/dict直接返回
- 如果是 str尝试 json.loads()
- 解析失败或为 None/空,均返回 []
"""
if value is None:
return []
if isinstance(value, (list, dict)):
return value
if isinstance(value, str):
val = value.strip()
if not val:
return []
try:
parsed = json.loads(val)
return parsed if isinstance(parsed, list) else []
except (json.JSONDecodeError, TypeError, ValueError):
return []
return []
def get_items(self):
"""解析 items_json返回物品列表"""
return self._safe_parse_json(self.items_json)
def set_items(self, items):
"""设置 items_json"""
self.items_json = json.dumps(items, ensure_ascii=False) if items else '[]'
def get_allowed_approvers(self):
"""解析 allowed_approvers返回审批人列表"""
return self._safe_parse_json(self.allowed_approvers)
def set_allowed_approvers(self, approvers):
"""设置 allowed_approvers"""
self.allowed_approvers = json.dumps(approvers, ensure_ascii=False) if approvers else '[]'
def to_dict(self):
return {
'id': self.id,
'request_no': self.request_no,
'applicant_id': self.applicant_id,
'applicant_name': self._get_user_name(self.applicant_id),
'remark': self.remark,
'status': self.status,
'status_text': ['待审批', '已通过', '已驳回', '已完成', '已完结'][self.status] if self.status in [0, 1, 2, 3, 4] else '未知',
'allowed_approvers': self.get_allowed_approvers(),
'actual_approver_id': self.actual_approver_id,
'approver_name': self._get_user_name(self.actual_approver_id) if self.actual_approver_id else None,
'approved_at': self.approved_at.strftime('%Y-%m-%d %H:%M:%S') if self.approved_at else None,
'reject_reason': self.reject_reason,
'items': self.get_items(),
'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S') if self.created_at else None,
'updated_at': self.updated_at.strftime('%Y-%m-%d %H:%M:%S') if self.updated_at else None,
}
def _get_user_name(self, user_id):
"""根据用户ID获取用户名"""
if not user_id:
return ""
from app.models.system import SysUser
try:
# ★ 必须用 .get() 按主键 ID 查询,千万不能用 username=user_id 去查
user = SysUser.query.get(user_id)
return user.username if user else f"未知用户({user_id})"
except Exception as e:
return f"用户({user_id})"
class TransOutbound(db.Model):
__tablename__ = 'trans_outbound'
id = db.Column(db.Integer, primary_key=True)
# 修改:不再唯一,因为批量出库时多个商品共用一个单号
outbound_no = db.Column(db.String(100), nullable=False)
# 关联源库存信息
sku = db.Column(db.String(100))
source_table = db.Column(db.String(50)) # 'stock_buy', 'stock_product', 'stock_semi'
stock_id = db.Column(db.Integer) # 对应源表的主键ID
barcode = db.Column(db.String(100)) # 实际扫码内容
# 业务信息
outbound_type = db.Column(db.String(50), default='SALES') # SALES(销售), USE(领用), PRODUCTION(生产)
quantity = db.Column(db.Numeric(19, 4), nullable=False)
# [新增] 出库时的单价,用于计算金额
unit_price = db.Column(db.Numeric(19, 2), default=0)
# 签字与追溯
consumer_name = db.Column(db.String(100)) # 领用人/客户
signature_path = db.Column(db.Text) # 电子签名图片路径
outbound_time = db.Column(db.DateTime, default=beijing_time)
operator_name = db.Column(db.String(100)) # 操作员
remark = db.Column(db.Text)
def to_dict(self):
return {
'id': self.id,
'outbound_no': self.outbound_no,
'sku': self.sku,
'source_table': self.source_table,
'outbound_type': self.outbound_type,
'quantity': float(self.quantity) if self.quantity else 0,
'unit_price': float(self.unit_price) if self.unit_price else 0,
'consumer_name': self.consumer_name,
'signature_path': self.signature_path,
'outbound_time': self.outbound_time.strftime('%Y-%m-%d %H:%M:%S') if self.outbound_time else None,
'operator_name': self.operator_name,
'remark': self.remark
}