Compare commits
13 Commits
0daf50ac21
...
859d783b5c
| Author | SHA1 | Date | |
|---|---|---|---|
| 859d783b5c | |||
| 478b90fe50 | |||
| eaafeaf24e | |||
| 73510d3a71 | |||
| b274ed1ed1 | |||
| 02f36b383c | |||
| 808e491fe8 | |||
| b40aa3ab90 | |||
| 2f6027b43c | |||
| b6ecde11dd | |||
| 7e3fc11bf5 | |||
| bc6e97ae97 | |||
| 3bd19c1ab5 |
@ -103,7 +103,16 @@ def get_list():
|
|||||||
# 字段级脱敏
|
# 字段级脱敏
|
||||||
user_permissions = get_current_user_permissions()
|
user_permissions = get_current_user_permissions()
|
||||||
if result.get('items'):
|
if result.get('items'):
|
||||||
result['items'] = [filter_item_by_permissions(item, user_permissions) for item in result['items']]
|
# ★ 字段权限过滤会剥离 base_id,但前端扫码/选物料联动需要它匹配历史批号(自动叠加)
|
||||||
|
# 在过滤前保留 base_id,过滤后补回(非敏感字段,仅物料关联 ID,与 semi 列表一致)
|
||||||
|
restored = []
|
||||||
|
for item in result['items']:
|
||||||
|
base_id = item.get('base_id')
|
||||||
|
filtered = filter_item_by_permissions(item, user_permissions)
|
||||||
|
if base_id is not None:
|
||||||
|
filtered['base_id'] = base_id
|
||||||
|
restored.append(filtered)
|
||||||
|
result['items'] = restored
|
||||||
return jsonify({"code": 200, "msg": "success", "data": result})
|
return jsonify({"code": 200, "msg": "success", "data": result})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|||||||
@ -293,11 +293,17 @@ def _do_get_stock_list(permission_prefix=None):
|
|||||||
# ★ Fail-Closed: 选单/借用场景默认剥离所有价格成本字段
|
# ★ Fail-Closed: 选单/借用场景默认剥离所有价格成本字段
|
||||||
_strip_price_fields = _make_price_stripper(permission_prefix)
|
_strip_price_fields = _make_price_stripper(permission_prefix)
|
||||||
|
|
||||||
|
# ★ 行级公司隔离:普通用户只能看到本公司的库存(超管/跨域不受限)
|
||||||
|
from app.utils.decorators import get_current_company_filter
|
||||||
|
company_limit = get_current_company_filter()
|
||||||
|
|
||||||
all_items = []
|
all_items = []
|
||||||
|
|
||||||
# 1. 采购件
|
# 1. 采购件
|
||||||
if StockBuy:
|
if StockBuy:
|
||||||
q = StockBuy.query.filter(StockBuy.stock_quantity > 0)
|
q = StockBuy.query.filter(StockBuy.stock_quantity > 0)
|
||||||
|
if company_limit is not None:
|
||||||
|
q = q.filter(StockBuy.base.has(MaterialBase.company_name == company_limit))
|
||||||
if keyword:
|
if keyword:
|
||||||
q = q.filter(
|
q = q.filter(
|
||||||
db.or_(
|
db.or_(
|
||||||
@ -322,6 +328,8 @@ def _do_get_stock_list(permission_prefix=None):
|
|||||||
if StockSemi:
|
if StockSemi:
|
||||||
try:
|
try:
|
||||||
q = StockSemi.query.filter(StockSemi.stock_quantity > 0)
|
q = StockSemi.query.filter(StockSemi.stock_quantity > 0)
|
||||||
|
if company_limit is not None:
|
||||||
|
q = q.filter(StockSemi.base.has(MaterialBase.company_name == company_limit))
|
||||||
if keyword:
|
if keyword:
|
||||||
q = q.filter(
|
q = q.filter(
|
||||||
db.or_(
|
db.or_(
|
||||||
@ -347,6 +355,8 @@ def _do_get_stock_list(permission_prefix=None):
|
|||||||
# 3. 成品
|
# 3. 成品
|
||||||
if StockProduct:
|
if StockProduct:
|
||||||
q = StockProduct.query.filter(StockProduct.stock_quantity > 0)
|
q = StockProduct.query.filter(StockProduct.stock_quantity > 0)
|
||||||
|
if company_limit is not None:
|
||||||
|
q = q.filter(StockProduct.base.has(MaterialBase.company_name == company_limit))
|
||||||
if keyword:
|
if keyword:
|
||||||
q = q.filter(
|
q = q.filter(
|
||||||
db.or_(
|
db.or_(
|
||||||
|
|||||||
@ -215,6 +215,11 @@ def bom_match_stock():
|
|||||||
# 去重
|
# 去重
|
||||||
child_ids = list(set(int(x) for x in child_ids))
|
child_ids = list(set(int(x) for x in child_ids))
|
||||||
|
|
||||||
|
# ★ 行级公司隔离:普通用户只能匹配本公司的库存(超管/跨域不受限)
|
||||||
|
from app.utils.decorators import get_current_company_filter
|
||||||
|
from app.models.base import MaterialBase
|
||||||
|
company_limit = get_current_company_filter()
|
||||||
|
|
||||||
from app.models.inbound.buy import StockBuy
|
from app.models.inbound.buy import StockBuy
|
||||||
from app.models.inbound.semi import StockSemi
|
from app.models.inbound.semi import StockSemi
|
||||||
from app.models.inbound.product import StockProduct
|
from app.models.inbound.product import StockProduct
|
||||||
@ -226,7 +231,10 @@ def bom_match_stock():
|
|||||||
buy_items = StockBuy.query.filter(
|
buy_items = StockBuy.query.filter(
|
||||||
StockBuy.base_id.in_(child_ids),
|
StockBuy.base_id.in_(child_ids),
|
||||||
StockBuy.stock_quantity > 0
|
StockBuy.stock_quantity > 0
|
||||||
).options(joinedload(StockBuy.base)).all()
|
)
|
||||||
|
if company_limit is not None:
|
||||||
|
buy_items = buy_items.filter(StockBuy.base.has(MaterialBase.company_name == company_limit))
|
||||||
|
buy_items = buy_items.options(joinedload(StockBuy.base)).all()
|
||||||
for s in buy_items:
|
for s in buy_items:
|
||||||
d = s.to_dict()
|
d = s.to_dict()
|
||||||
d['type'] = 'material'
|
d['type'] = 'material'
|
||||||
@ -242,7 +250,10 @@ def bom_match_stock():
|
|||||||
semi_items = StockSemi.query.filter(
|
semi_items = StockSemi.query.filter(
|
||||||
StockSemi.base_id.in_(child_ids),
|
StockSemi.base_id.in_(child_ids),
|
||||||
StockSemi.stock_quantity > 0
|
StockSemi.stock_quantity > 0
|
||||||
).options(joinedload(StockSemi.base)).all()
|
)
|
||||||
|
if company_limit is not None:
|
||||||
|
semi_items = semi_items.filter(StockSemi.base.has(MaterialBase.company_name == company_limit))
|
||||||
|
semi_items = semi_items.options(joinedload(StockSemi.base)).all()
|
||||||
for s in semi_items:
|
for s in semi_items:
|
||||||
d = s.to_dict()
|
d = s.to_dict()
|
||||||
d['type'] = 'semi'
|
d['type'] = 'semi'
|
||||||
@ -260,7 +271,10 @@ def bom_match_stock():
|
|||||||
prod_items = StockProduct.query.filter(
|
prod_items = StockProduct.query.filter(
|
||||||
StockProduct.base_id.in_(child_ids),
|
StockProduct.base_id.in_(child_ids),
|
||||||
StockProduct.stock_quantity > 0
|
StockProduct.stock_quantity > 0
|
||||||
).options(joinedload(StockProduct.base)).all()
|
)
|
||||||
|
if company_limit is not None:
|
||||||
|
prod_items = prod_items.filter(StockProduct.base.has(MaterialBase.company_name == company_limit))
|
||||||
|
prod_items = prod_items.options(joinedload(StockProduct.base)).all()
|
||||||
for s in prod_items:
|
for s in prod_items:
|
||||||
d = s.to_dict()
|
d = s.to_dict()
|
||||||
d['type'] = 'product'
|
d['type'] = 'product'
|
||||||
@ -398,7 +412,8 @@ def create_outbound_request():
|
|||||||
items=items,
|
items=items,
|
||||||
allowed_approvers=allowed_approvers,
|
allowed_approvers=allowed_approvers,
|
||||||
remark=data.get('remark'),
|
remark=data.get('remark'),
|
||||||
approver_id=approver_id
|
approver_id=approver_id,
|
||||||
|
outbound_type=data.get('outbound_type') # 出库类型(申请时确定)
|
||||||
)
|
)
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
|
|||||||
@ -41,6 +41,16 @@ def get_current_user_info():
|
|||||||
return user.id if user else None, user.role if user else None
|
return user.id if user else None, user.role if user else None
|
||||||
|
|
||||||
|
|
||||||
|
def _current_username():
|
||||||
|
"""获取当前登录用户的用户名(姓名/账号),用于操作人展示;避免把 JWT 数字 ID 存进记录"""
|
||||||
|
identity = get_jwt_identity()
|
||||||
|
if not identity:
|
||||||
|
return 'System'
|
||||||
|
from app.models.system import SysUser
|
||||||
|
user = SysUser.query.get(identity)
|
||||||
|
return user.username if user else str(identity)
|
||||||
|
|
||||||
|
|
||||||
def filter_item_by_permissions(item_dict, user_permissions, prefix='op_records'):
|
def filter_item_by_permissions(item_dict, user_permissions, prefix='op_records'):
|
||||||
"""
|
"""
|
||||||
根据用户权限过滤 item 字典,无权限的字段值置为 None
|
根据用户权限过滤 item 字典,无权限的字段值置为 None
|
||||||
@ -102,9 +112,10 @@ def scan_borrowed_item():
|
|||||||
)
|
)
|
||||||
def submit_return():
|
def submit_return():
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
user = get_jwt_identity() # 库管
|
# ★ 归还人存"姓名",而非 JWT 数字 ID
|
||||||
|
operator_name = _current_username()
|
||||||
try:
|
try:
|
||||||
TransService.process_return(data, operator_name=user)
|
TransService.process_return(data, operator_name=operator_name)
|
||||||
return jsonify({'code': 200, 'msg': '还库成功'})
|
return jsonify({'code': 200, 'msg': '还库成功'})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'code': 400, 'msg': str(e)}), 400
|
return jsonify({'code': 400, 'msg': str(e)}), 400
|
||||||
@ -131,9 +142,9 @@ def scrap_borrow():
|
|||||||
if not record_ids:
|
if not record_ids:
|
||||||
return jsonify({'code': 400, 'msg': '请选择要报废的借出记录'}), 400
|
return jsonify({'code': 400, 'msg': '请选择要报废的借出记录'}), 400
|
||||||
|
|
||||||
user = get_jwt_identity() or 'Unknown'
|
operator_name = _current_username() or 'Unknown'
|
||||||
try:
|
try:
|
||||||
result = TransService.scrap_borrow(record_ids, operator_name=user, reason=reason)
|
result = TransService.scrap_borrow(record_ids, operator_name=operator_name, reason=reason)
|
||||||
return jsonify({'code': 200, 'msg': f'已报废 {result["count"]} 条借出记录', 'data': result})
|
return jsonify({'code': 200, 'msg': f'已报废 {result["count"]} 条借出记录', 'data': result})
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return jsonify({'code': 400, 'msg': str(e)}), 400
|
return jsonify({'code': 400, 'msg': str(e)}), 400
|
||||||
@ -277,6 +288,29 @@ def approve_borrow_request(request_id):
|
|||||||
return jsonify({'code': 500, 'msg': f'服务器内部错误: {str(e)}'}), 500
|
return jsonify({'code': 500, 'msg': f'服务器内部错误: {str(e)}'}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@trans_bp.route('/borrow/request/<int:request_id>/close', methods=['POST'])
|
||||||
|
@jwt_required()
|
||||||
|
@permission_required('op_borrow_approval')
|
||||||
|
def close_borrow_request(request_id):
|
||||||
|
"""
|
||||||
|
手动完结已通过的借库审批单(status 1-已通过 → 3-已完成)
|
||||||
|
参照出库审批的完结逻辑,供库管/主管在未走扫码借出时强制完结
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
user_id, _ = get_current_user_info()
|
||||||
|
if not user_id:
|
||||||
|
return jsonify({'code': 401, 'msg': '用户未登录'}), 401
|
||||||
|
|
||||||
|
success, message, approval = BorrowApprovalService.mark_completed(request_id)
|
||||||
|
if not success:
|
||||||
|
return jsonify({'code': 400, 'msg': message}), 400
|
||||||
|
|
||||||
|
return jsonify({'code': 200, 'msg': message, 'data': approval.to_dict() if approval else None}), 200
|
||||||
|
except Exception as e:
|
||||||
|
traceback.print_exc()
|
||||||
|
return jsonify({'code': 500, 'msg': f'服务器内部错误: {str(e)}'}), 500
|
||||||
|
|
||||||
|
|
||||||
# --- 获取借库审批单列表 ---
|
# --- 获取借库审批单列表 ---
|
||||||
@trans_bp.route('/borrow/request', methods=['GET'])
|
@trans_bp.route('/borrow/request', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
@ -351,7 +385,7 @@ def dispatch_borrow():
|
|||||||
borrow_no = TransService.execute_dispatch(
|
borrow_no = TransService.execute_dispatch(
|
||||||
approval_id=approval_id,
|
approval_id=approval_id,
|
||||||
items=data.get('items', []),
|
items=data.get('items', []),
|
||||||
operator_name=get_jwt_identity(),
|
operator_name=_current_username(),
|
||||||
borrower_name=data.get('borrower_name'),
|
borrower_name=data.get('borrower_name'),
|
||||||
signature=data.get('signature_path'),
|
signature=data.get('signature_path'),
|
||||||
remark=data.get('remark'),
|
remark=data.get('remark'),
|
||||||
|
|||||||
@ -10,23 +10,25 @@ warehouse_bp = Blueprint('warehouse', __name__, url_prefix='/api/v1/warehouse')
|
|||||||
|
|
||||||
def build_tree(nodes, parent_id=None):
|
def build_tree(nodes, parent_id=None):
|
||||||
"""
|
"""
|
||||||
将平铺的数据构建为树形结构
|
将平铺的数据构建为树形结构(O(N) 内存组装,避免递归时每层全量扫描导致 O(N²))
|
||||||
|
|
||||||
|
做法:先把全部节点按 parent_id 分组到 Map,再从根出发逐层用 Map 取子节点组装。
|
||||||
|
每个节点只被处理一次,显著快于"每次递归 for 遍历全表"的旧实现。
|
||||||
"""
|
"""
|
||||||
tree = []
|
by_parent = {}
|
||||||
for node in nodes:
|
for node in nodes:
|
||||||
if node.parent_id == parent_id:
|
by_parent.setdefault(node.parent_id, []).append(node)
|
||||||
children = build_tree(nodes, node.id)
|
|
||||||
node_dict = node.to_dict()
|
def assemble(pid):
|
||||||
if children:
|
kids = sorted(by_parent.get(pid, []), key=lambda x: (x.name or ''))
|
||||||
# 子节点按 name 升序排序
|
out = []
|
||||||
children_sorted = sorted(children, key=lambda x: x.get('name', ''))
|
for k in kids:
|
||||||
node_dict['children'] = children_sorted
|
d = k.to_dict()
|
||||||
else:
|
d['children'] = assemble(k.id)
|
||||||
node_dict['children'] = []
|
out.append(d)
|
||||||
tree.append(node_dict)
|
return out
|
||||||
# 当前层级按 name 升序排序
|
|
||||||
tree_sorted = sorted(tree, key=lambda x: x.get('name', ''))
|
return assemble(parent_id)
|
||||||
return tree_sorted
|
|
||||||
|
|
||||||
|
|
||||||
@warehouse_bp.route('/tree', methods=['GET'])
|
@warehouse_bp.route('/tree', methods=['GET'])
|
||||||
@ -38,9 +40,9 @@ def get_tree():
|
|||||||
# 查询所有库位,按 name 升序排序
|
# 查询所有库位,按 name 升序排序
|
||||||
all_locations = SysWarehouseLocation.query.order_by(SysWarehouseLocation.name.asc()).all()
|
all_locations = SysWarehouseLocation.query.order_by(SysWarehouseLocation.name.asc()).all()
|
||||||
|
|
||||||
# 构建树形结构
|
# 构建树形结构(O(N) 内存组装,见 build_tree)
|
||||||
tree_data = build_tree(all_locations, parent_id=None)
|
tree_data = build_tree(all_locations, parent_id=None)
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'code': 200,
|
'code': 200,
|
||||||
'msg': 'success',
|
'msg': 'success',
|
||||||
|
|||||||
@ -18,6 +18,8 @@ class OutboundApproval(db.Model):
|
|||||||
applicant_id = db.Column(db.Integer, nullable=False, index=True)
|
applicant_id = db.Column(db.Integer, nullable=False, index=True)
|
||||||
# 申请说明
|
# 申请说明
|
||||||
remark = db.Column(db.Text)
|
remark = db.Column(db.Text)
|
||||||
|
# 出库类型:SALES/USE/PRODUCTION/LOSS/REPAIR(申请时确定,扫码出库自动带出)
|
||||||
|
outbound_type = db.Column(db.String(50))
|
||||||
# 状态: 0-待审批, 1-已通过, 2-已驳回, 3-已完成(已出库)
|
# 状态: 0-待审批, 1-已通过, 2-已驳回, 3-已完成(已出库)
|
||||||
status = db.Column(db.Integer, default=0, nullable=False)
|
status = db.Column(db.Integer, default=0, nullable=False)
|
||||||
# 允许审批的人员列表 (JSON格式: [{"type": "role", "value": "admin"}, {"type": "user", "value": "123"}])
|
# 允许审批的人员列表 (JSON格式: [{"type": "role", "value": "admin"}, {"type": "user", "value": "123"}])
|
||||||
@ -81,6 +83,7 @@ class OutboundApproval(db.Model):
|
|||||||
'applicant_id': self.applicant_id,
|
'applicant_id': self.applicant_id,
|
||||||
'applicant_name': self._get_user_name(self.applicant_id),
|
'applicant_name': self._get_user_name(self.applicant_id),
|
||||||
'remark': self.remark,
|
'remark': self.remark,
|
||||||
|
'outbound_type': self.outbound_type or '',
|
||||||
'status': self.status,
|
'status': self.status,
|
||||||
'status_text': ['待审批', '已通过', '已驳回', '已完成', '已完结'][self.status] if self.status in [0, 1, 2, 3, 4] else '未知',
|
'status_text': ['待审批', '已通过', '已驳回', '已完成', '已完结'][self.status] if self.status in [0, 1, 2, 3, 4] else '未知',
|
||||||
'allowed_approvers': self.get_allowed_approvers(),
|
'allowed_approvers': self.get_allowed_approvers(),
|
||||||
|
|||||||
@ -1,6 +1,26 @@
|
|||||||
from app.extensions import db, beijing_time
|
from app.extensions import db, beijing_time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
|
from functools import lru_cache
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=512)
|
||||||
|
def _borrow_user_name(uid):
|
||||||
|
"""按用户 ID 取用户名(带缓存,避免列表页 N+1)"""
|
||||||
|
try:
|
||||||
|
from app.models.system import SysUser
|
||||||
|
u = SysUser.query.get(uid)
|
||||||
|
return u.username if u else None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _display_borrow_operator(op):
|
||||||
|
"""归还人展示映射:历史存的是数字 user id → 映射为用户名;新数据存姓名则原样返回"""
|
||||||
|
if op and str(op).strip().isdigit():
|
||||||
|
mapped = _borrow_user_name(int(op))
|
||||||
|
return mapped if mapped else op
|
||||||
|
return op
|
||||||
|
|
||||||
|
|
||||||
class TransBorrow(db.Model):
|
class TransBorrow(db.Model):
|
||||||
@ -48,7 +68,7 @@ class TransBorrow(db.Model):
|
|||||||
'expected_return_time': self.expected_return_time.strftime('%Y-%m-%d %H:%M') if self.expected_return_time else None,
|
'expected_return_time': self.expected_return_time.strftime('%Y-%m-%d %H:%M') if self.expected_return_time else None,
|
||||||
'is_returned': self.is_returned,
|
'is_returned': self.is_returned,
|
||||||
'return_time': self.return_time.strftime('%Y-%m-%d %H:%M') if self.return_time else None,
|
'return_time': self.return_time.strftime('%Y-%m-%d %H:%M') if self.return_time else None,
|
||||||
'return_operator': self.return_operator,
|
'return_operator': _display_borrow_operator(self.return_operator),
|
||||||
'return_signature': self.return_signature,
|
'return_signature': self.return_signature,
|
||||||
'return_location': self.return_location,
|
'return_location': self.return_location,
|
||||||
'location': self.location or '',
|
'location': self.location or '',
|
||||||
|
|||||||
@ -701,7 +701,7 @@ class OutboundApprovalService:
|
|||||||
return f"APR-OUT-{date_str}-{time_str}-{sequence:04d}"
|
return f"APR-OUT-{date_str}-{time_str}-{sequence:04d}"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_request(applicant_id, items, allowed_approvers, remark=None, approver_id=None):
|
def create_request(applicant_id, items, allowed_approvers, remark=None, approver_id=None, outbound_type=None):
|
||||||
"""
|
"""
|
||||||
创建出库审批单(申请阶段,直接存储前端传来的物料信息快照,不关联具体库存记录)
|
创建出库审批单(申请阶段,直接存储前端传来的物料信息快照,不关联具体库存记录)
|
||||||
|
|
||||||
@ -759,6 +759,7 @@ class OutboundApprovalService:
|
|||||||
request_no=request_no,
|
request_no=request_no,
|
||||||
applicant_id=applicant_id,
|
applicant_id=applicant_id,
|
||||||
remark=remark,
|
remark=remark,
|
||||||
|
outbound_type=outbound_type, # 申请时确定的出库类型,扫码出库时带出
|
||||||
status=0, # 待审批
|
status=0, # 待审批
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -434,7 +434,12 @@ class TransService:
|
|||||||
order_subq = (
|
order_subq = (
|
||||||
db.session.query(
|
db.session.query(
|
||||||
TransBorrow.borrow_no.label('borrow_no'),
|
TransBorrow.borrow_no.label('borrow_no'),
|
||||||
func.min(TransBorrow.expected_return_time).label('sort_key')
|
# 有限期梯队内:单号内最早预计归还时间
|
||||||
|
func.min(TransBorrow.expected_return_time).label('sort_key'),
|
||||||
|
# 无限期梯队内:单号内最早借出时间(呆滞借用盘点)
|
||||||
|
func.min(TransBorrow.borrow_time).label('min_borrow_time'),
|
||||||
|
# 单内是否含"有限期"明细:任一 expected_return_time 非空 → 1(有限期单排前)
|
||||||
|
func.max(case((TransBorrow.expected_return_time.isnot(None), 1), else_=0)).label('has_finite')
|
||||||
)
|
)
|
||||||
.group_by(TransBorrow.borrow_no)
|
.group_by(TransBorrow.borrow_no)
|
||||||
.subquery()
|
.subquery()
|
||||||
@ -673,8 +678,15 @@ class TransService:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# 排序(单号维度的 sort_key ASC)
|
# ★ 默认排序(多级复合,符合"优先关注快到期/逾期"业务):
|
||||||
borrow_no_q = borrow_no_q.order_by(nullslast(asc(order_subq.c.sort_key)))
|
# 1) 有限期单(含 expected_return_time)排前,无限期单排后
|
||||||
|
# 2) 有限期内按最早预计归还时间 ASC(越快到期/逾期越久越靠前)
|
||||||
|
# 3) 无限期内按最早借出时间 ASC(借出越久越靠前,暴露长期未还的呆滞借用)
|
||||||
|
borrow_no_q = borrow_no_q.order_by(
|
||||||
|
case((order_subq.c.has_finite == 0, 1), else_=0).asc(),
|
||||||
|
nullslast(asc(order_subq.c.sort_key)),
|
||||||
|
asc(order_subq.c.min_borrow_time)
|
||||||
|
)
|
||||||
|
|
||||||
# 分页(基准 = borrow_no 单号数)
|
# 分页(基准 = borrow_no 单号数)
|
||||||
pagination = borrow_no_q.paginate(page=page, per_page=limit, error_out=False)
|
pagination = borrow_no_q.paginate(page=page, per_page=limit, error_out=False)
|
||||||
|
|||||||
@ -239,7 +239,7 @@ const handleLogout = () => {
|
|||||||
<footer v-if="!isLoginPage" class="app-footer">
|
<footer v-if="!isLoginPage" class="app-footer">
|
||||||
<span class="version-tag">
|
<span class="version-tag">
|
||||||
<el-icon style="vertical-align: middle; margin-right: 4px"><InfoFilled /></el-icon>
|
<el-icon style="vertical-align: middle; margin-right: 4px"><InfoFilled /></el-icon>
|
||||||
当前版本:V3.71
|
当前版本:V3.72
|
||||||
</span>
|
</span>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
|||||||
@ -174,6 +174,14 @@ export function approveBorrowRequest(id: number, data: { action: 'approve' | 're
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 手动完结已通过的借库审批单(status 1 → 3)
|
||||||
|
export function closeBorrowRequest(id: number) {
|
||||||
|
return request({
|
||||||
|
url: `/v1/transactions/borrow/request/${id}/close`,
|
||||||
|
method: 'post'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 借库选单专用库存列表(独立权限 op_borrow_apply)
|
* 借库选单专用库存列表(独立权限 op_borrow_apply)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -137,6 +137,19 @@
|
|||||||
驳回
|
驳回
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
|
<!-- ★ 已通过的申请单:库管/主管可手动完结 -->
|
||||||
|
<template v-else-if="row.status === 1">
|
||||||
|
<el-button
|
||||||
|
v-if="canCompleteRequest"
|
||||||
|
type="danger"
|
||||||
|
plain
|
||||||
|
size="small"
|
||||||
|
:loading="row._closing"
|
||||||
|
@click="handleComplete(row)"
|
||||||
|
>
|
||||||
|
完结
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
<span v-else style="color: #c0c4cc;">-</span>
|
<span v-else style="color: #c0c4cc;">-</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@ -182,14 +195,44 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { Refresh, Warning } from '@element-plus/icons-vue'
|
import { Refresh, Warning } from '@element-plus/icons-vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { getBorrowApprovalList, approveBorrowRequest } from '@/api/transaction'
|
import { getBorrowApprovalList, approveBorrowRequest, closeBorrowRequest } from '@/api/transaction'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
// ★ 完结权限:超管 / 审批操作(op_borrow_approval:operation) / 借库执行(op_borrow:operation)
|
||||||
|
const canCompleteRequest = computed(() =>
|
||||||
|
userStore.role === 'SUPER_ADMIN' ||
|
||||||
|
userStore.hasPermission('op_borrow_approval:operation') ||
|
||||||
|
userStore.hasPermission('op_borrow:operation') ||
|
||||||
|
userStore.username === 'IRIS'
|
||||||
|
)
|
||||||
|
|
||||||
|
// ★ 手动完结已通过的借库审批单(status 1 → 3,仿出库审批)
|
||||||
|
const handleComplete = (row: any) => {
|
||||||
|
ElMessageBox.confirm(
|
||||||
|
`确定强制完结审批单【${row.request_no}】吗?\n\n完结后该单标记为「已完成」,不可再扫码借出。`,
|
||||||
|
'⚠️ 强制完结确认',
|
||||||
|
{ confirmButtonText: '确认完结', cancelButtonText: '取消', type: 'warning' }
|
||||||
|
)
|
||||||
|
.then(async () => {
|
||||||
|
row._closing = true
|
||||||
|
try {
|
||||||
|
await closeBorrowRequest(row.id)
|
||||||
|
ElMessage.success('审批单已完结')
|
||||||
|
fetchData()
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error(e?.response?.data?.msg || e?.msg || e?.message || '完结失败')
|
||||||
|
} finally {
|
||||||
|
row._closing = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
// --- 状态 ---
|
// --- 状态 ---
|
||||||
const list = ref<any[]>([])
|
const list = ref<any[]>([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|||||||
@ -395,6 +395,15 @@
|
|||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<el-form label-width="80px">
|
<el-form label-width="80px">
|
||||||
|
<el-form-item label="出库类型" required>
|
||||||
|
<el-select v-model="requestOutboundType" placeholder="请选择类型" style="width: 100%">
|
||||||
|
<el-option label="销售出库" value="SALES" />
|
||||||
|
<el-option label="内部领用" value="USE" />
|
||||||
|
<el-option label="生产出库" value="PRODUCTION" />
|
||||||
|
<el-option label="盘亏出库" value="LOSS" />
|
||||||
|
<el-option label="维修出库" value="REPAIR" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="* 指定审批人" required>
|
<el-form-item label="* 指定审批人" required>
|
||||||
<el-select
|
<el-select
|
||||||
v-model="requestApproverId"
|
v-model="requestApproverId"
|
||||||
@ -533,6 +542,7 @@ const printLoading = ref(false)
|
|||||||
// ★ 出库申请相关
|
// ★ 出库申请相关
|
||||||
const requestDialogVisible = ref(false)
|
const requestDialogVisible = ref(false)
|
||||||
const requestRemark = ref('')
|
const requestRemark = ref('')
|
||||||
|
const requestOutboundType = ref('SALES') // 申请时确定出库类型,扫码出库自动带出
|
||||||
const requestApproverId = ref<number | null>(null)
|
const requestApproverId = ref<number | null>(null)
|
||||||
const approvers = ref<any[]>([])
|
const approvers = ref<any[]>([])
|
||||||
const requestSubmitting = ref(false)
|
const requestSubmitting = ref(false)
|
||||||
@ -1088,6 +1098,7 @@ const confirmSubmitRequest = async () => {
|
|||||||
quantity: item.export_quantity || 0
|
quantity: item.export_quantity || 0
|
||||||
})),
|
})),
|
||||||
remark: buildRemarkWithShortage(trimmed),
|
remark: buildRemarkWithShortage(trimmed),
|
||||||
|
outbound_type: requestOutboundType.value,
|
||||||
approver_id: requestApproverId.value
|
approver_id: requestApproverId.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -187,6 +187,7 @@
|
|||||||
<el-option label="内部领用" value="USE" />
|
<el-option label="内部领用" value="USE" />
|
||||||
<el-option label="生产出库" value="PRODUCTION" />
|
<el-option label="生产出库" value="PRODUCTION" />
|
||||||
<el-option label="盘亏出库" value="LOSS" />
|
<el-option label="盘亏出库" value="LOSS" />
|
||||||
|
<el-option label="维修出库" value="REPAIR" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
@ -461,6 +462,10 @@ const handleRequestChange = (val: number | null) => {
|
|||||||
if (selectedRequest.value?.remark) {
|
if (selectedRequest.value?.remark) {
|
||||||
form.remark = selectedRequest.value.remark
|
form.remark = selectedRequest.value.remark
|
||||||
}
|
}
|
||||||
|
// ★ 自动带出申请时确定的出库类型(库管无需再选);旧单无类型则留待手动选择
|
||||||
|
if (selectedRequest.value?.outbound_type) {
|
||||||
|
form.outbound_type = selectedRequest.value.outbound_type
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 切换申请单时清空购物车,防止已扫物品与新单据混淆
|
// 切换申请单时清空购物车,防止已扫物品与新单据混淆
|
||||||
cartItems.value = []
|
cartItems.value = []
|
||||||
|
|||||||
@ -253,7 +253,8 @@ const formatType = (type: string) => {
|
|||||||
'USE': '内部领用',
|
'USE': '内部领用',
|
||||||
'PRODUCTION': '生产出库',
|
'PRODUCTION': '生产出库',
|
||||||
'SCRAP': '报废',
|
'SCRAP': '报废',
|
||||||
'LOSS': '盘亏出库' // ★ 补充映射:扫码出库下拉中存在「盘亏出库 LOSS」,避免出库记录显示英文
|
'LOSS': '盘亏出库', // ★ 补充映射:扫码出库下拉中存在「盘亏出库 LOSS」,避免出库记录显示英文
|
||||||
|
'REPAIR': '维修出库'
|
||||||
}
|
}
|
||||||
return map[type] || type
|
return map[type] || type
|
||||||
}
|
}
|
||||||
@ -264,7 +265,8 @@ const getTagType = (type: string) => {
|
|||||||
'USE': 'warning',
|
'USE': 'warning',
|
||||||
'PRODUCTION': 'info',
|
'PRODUCTION': 'info',
|
||||||
'SCRAP': 'danger',
|
'SCRAP': 'danger',
|
||||||
'LOSS': 'warning' // ★ 补充:盘亏出库标签样式
|
'LOSS': 'warning', // ★ 补充:盘亏出库标签样式
|
||||||
|
'REPAIR': 'info'
|
||||||
}
|
}
|
||||||
return map[type] || ''
|
return map[type] || ''
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user