17 Commits

Author SHA1 Message Date
815ad01a5b V3.74,9.9推送 2026-09-09 13:19:49 +08:00
ffbf4689f1 fix(inbound/buy): 从采购申请导入时价格不受“是否可显示价格”权限拦截
- 价格填充不再包在 hasFormFieldPermission(unit_price) 里,库管(前端不显示价格)也能把采购单价格带入隐藏字段用于成本
- 前端价格列/输入框仍按字段权限对库管隐藏
2026-09-09 13:10:46 +08:00
2a9e2e4560 fix(bom): BOM 三态互斥 + active_only排除归档 + 详情主备注
- get_bom_list active_only 补 is_archived==False,出库/借库“按BOM套餐”不再泄漏归档版本
- get_bom_detail 返回顶层 remark,详情弹窗主备注可回显
- 状态互斥:update_enabled 切启/停即清归档;update_archived 归档→废弃(enabled=False)、取消归档→启用;停用视图排除归档
2026-09-09 13:00:41 +08:00
c8cf2bd52b feat(db): BOM 三态互斥存量归一——归档(废弃)自动停用
- 将历史“归档且仍启用(True/True)”的行归一为 归档且停用(True/False)
2026-09-09 13:00:35 +08:00
eff9b62224 fix(borrow,outbound): 普通用户记录只看本人——按领用人/借用人姓名(不含账号前缀)匹配
- 出库记录/借还记录:非管理者视角时按 领用人(consumer_name)/借用人(borrower_name) 过滤
- 匹配取登录名 username.split(/)[0] 的姓名;兼容库里存“姓名/xiaolongxia”全名(姓名+/前缀)
- 修复:管理员替员工创建、领用人=员工 的单,员工登录可见
2026-09-09 11:19:15 +08:00
67d3113fe1 fix(permission): 记录管理者视角仅超管/主管/仓库管理员,去掉入库员/出库员
- PRIVILEGED_VIEWER_ROLES 收敛为 SUPER_ADMIN/SUPERVISOR/WAREHOUSE_MGR
- 入库员(INBOUND)、出库员(OUTBOUND)按普通处理:借还/出库记录只看自己
2026-09-09 11:19:09 +08:00
e622697d7c feat(borrow): 借库申请前端必填拦截与星标——申请原因、预计归还日期(长期借用除外)
- 提交前拦截:申请原因为空或(未勾长期借用且无归还日期)提示并 return
- 表单加必填星标:申请原因恒必填;预计归还日期仅在非长期借用时 required
2026-09-09 10:53:16 +08:00
aa9322bcbc feat(borrow): 借库申请后端必填校验——申请原因、预计归还日期(长期借用除外)
- submit_approval 校验:申请原因非空;未勾长期借用时必须有 expected_return_time,否则 ValueError 拦截
2026-09-09 10:53:12 +08:00
4146f35010 feat(permission): “出库/借库需审批”开关权限化——仅主管/超管可见可改
- 新增权限码 material_list:isApprovalRequired,授予仅 SUPER_ADMIN/SUPERVISOR(收回其它角色)
- 物料列表该列仅持码角色可见;后端批量端点与字段脱敏均改按新码校验,其余角色看不到值也无法改
2026-09-09 10:47:36 +08:00
6bdc8a82f2 feat(borrow,outbound): 申请预检按需显示审批人——默认不显示,命中需审批才出现并必选
- 后端新增 /borrow/request/check-approval 与 /outbound/request/check-approval 预检接口
- 申请弹窗默认隐藏审批人;提交前预检:命中需审批→显示审批人并红字列物料、未选拦截;未命中→隐藏并直接提交(approver=null)
- 申请 items 补传 base_id 供后端 ID 精确判定
2026-09-09 10:47:29 +08:00
d06ce45a72 feat(borrow,outbound): 需审批判定收敛——ID优先 + 名称精确AND核心规格(斜杠前段)
- approval_control 重写:带 base_id 按主键绝对判定;无 id 才名称100%一致 AND 核心规格(_code_of)一致兜底,杜绝同名不同规误杀
- 借/出库服务命中需审批但未选审批人时,报错列出需审批物料名/规格
2026-09-09 10:47:24 +08:00
2abac8b3f9 feat(borrow,outbound): 申请页审批人改可选(默认免审批),提示语更新
- 出库/借库申请不再强制选审批人;命中需审批且未选人时由后端提示补选
- 成功提示改为:含需审批物料将进入审批,否则直接交库管执行
2026-09-09 09:31:28 +08:00
93f149739d feat(material): 基础信息列表加“出库/借库需审批”开关列
- material_base.ts 新增 batchSetApprovalRequired
- material/list.vue:列配置/权限/表格 switch,单行拨动即保存(乐观更新)
2026-09-09 09:31:23 +08:00
4cd3eefdf4 feat(borrow,outbound): 默认免审批改造——命中物料需审批才走原流程
- models/base.py 加 is_approval_required 列及 isApprovalRequired 序列化;field_permissions 登记
- 新增 POST /inbound/base/batch-approval(批量设需审批,仿批量质检)
- borrow_service.submit_approval / outbound_service.create_request:明细含需审批物料→须选审批人走原审批;否则创建即 status=1(待库管执行)、不发审批邮件
- 判定按 (name,spec_model) 反查启用物料
2026-09-09 09:31:18 +08:00
e03c035ee0 feat(db): material_base 增加 is_approval_required 开关列(出库/借库需审批)
- boolean NOT NULL DEFAULT false,存量默认不审批
2026-09-09 09:31:12 +08:00
6aba09880c feat(borrow): 库管借库执行自动把申请单原因带入备注说明
- 选中已审批借库申请单时 form.remark 自动填充该单 remark(申请原因),无需重复手填,可再改
2026-09-09 09:31:06 +08:00
9622baf76e fix(borrow,outbound): 记录查看按角色收窄——普通只看自己,库管/主管/超管/跨域看全部
- decorators 新增 is_privileged_viewer(SUPER_ADMIN/SUPERVISOR/WAREHOUSE_MGR 或 crossDomain)
- 借库记录列表:非管理者强制 applicant_id=当前用户
- 出库记录列表+详情:非管理者强制只看自己/无权访问他人单(403)
2026-09-09 09:31:01 +08:00
23 changed files with 641 additions and 88 deletions

View File

@ -0,0 +1,10 @@
-- =============================================================================
-- 一次性迁移:物料“出库/借库需审批”开关
-- material_base.is_approval_required boolean NOT NULL DEFAULT false
-- 语义:命中该标记物料的出库/借库申请仍需审批;其余默认不审批、自动通过后直接交库管执行。
-- 存量默认 false不审批需要审批的物料再在基础信息中逐个/批量勾选。
-- 执行: docker exec -i inventory_db psql -U test -d inventory_system < 本文件
-- =============================================================================
BEGIN;
ALTER TABLE material_base ADD COLUMN IF NOT EXISTS is_approval_required boolean NOT NULL DEFAULT false;
COMMIT;

View File

@ -0,0 +1,32 @@
-- =============================================================================
-- 一次性迁移:物料“出库/借库需审批”开关的独立权限项
-- 权限码 material_list:isApprovalRequired操作级
-- 仅授予SUPER_ADMIN(超管)、SUPERVISOR(主管)。其余角色不可见、不可修改。
-- 幂等:重复执行安全(先收回“非主管/超管”的该权限,再确保主管/超管持有)。
-- 执行: docker exec -i inventory_db psql -U test -d inventory_system < 本文件
-- =============================================================================
BEGIN;
-- 1) 注册权限元素(幂等)
INSERT INTO sys_element (menu_code, name, code, element_type)
SELECT 'material_list', '出库/借库需审批(修改)', 'material_list:isApprovalRequired', 'operation'
WHERE NOT EXISTS (SELECT 1 FROM sys_element WHERE code = 'material_list:isApprovalRequired');
-- 2) 收回除主管/超管外所有角色的该权限(幂等)
DELETE FROM sys_role_permission
WHERE target_code = 'material_list:isApprovalRequired'
AND type = 'element'
AND role_code NOT IN ('SUPER_ADMIN', 'SUPERVISOR');
-- 3) 确保主管/超管持有(幂等)
INSERT INTO sys_role_permission (role_code, target_code, type, company_name)
SELECT v.role_code, 'material_list:isApprovalRequired', 'element', NULL
FROM (VALUES ('SUPER_ADMIN'), ('SUPERVISOR')) AS v(role_code)
WHERE NOT EXISTS (
SELECT 1 FROM sys_role_permission r
WHERE r.role_code = v.role_code
AND r.target_code = 'material_list:isApprovalRequired'
AND r.type = 'element'
);
COMMIT;

View File

@ -0,0 +1,12 @@
-- =============================================================================
-- 一次性迁移BOM 三态互斥归一(启用/停用/归档 三者互斥)
-- 规则:归档=彻底废弃 → is_archived=True 且 is_enabled=False
-- 启用=在用;停用=有问题/暂停
-- 把历史上“归档但仍启用(True/True)”的行归一到“归档且停用(True/False)”
-- 执行: docker exec -i inventory_db psql -U test -d inventory_system < 本文件
-- =============================================================================
BEGIN;
UPDATE bom_table
SET is_enabled = FALSE
WHERE is_archived = TRUE AND is_enabled = TRUE;
COMMIT;

View File

@ -539,6 +539,44 @@ def batch_set_inspection():
return jsonify({"code": 500, "msg": f"批量设置强制质检失败: {str(e)}"}), 500
# ==============================================================================
# 2.8 批量设置“出库/借库需审批” (POST /api/v1/inbound/base/batch-approval)
# ==============================================================================
@inbound_base_bp.route('/batch-approval', methods=['POST'])
@permission_required('material_list:isApprovalRequired')
def batch_set_approval_required():
"""
批量设置物料“出库/借库需审批”标记(仿强制质检批量接口)。
请求体: { "ids": [1,2,3], "isApprovalRequired": true }
"""
try:
data = request.get_json()
if not data:
return jsonify({"code": 400, "msg": "No data provided"}), 400
ids = data.get('ids', [])
is_approval_required = bool(data.get('isApprovalRequired', False))
if not ids:
return jsonify({"code": 400, "msg": "请选择要设置的物料"}), 400
updated_count = 0
for base_id in ids:
material = MaterialBase.query.get(base_id)
if material:
material.is_approval_required = is_approval_required
updated_count += 1
db.session.commit()
return jsonify({
"code": 200,
"msg": f"批量设置成功,已更新 {updated_count} 条记录",
"data": {"updated": updated_count}
})
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 (GET /api/v1/inbound/base/spec-latest)
# ==============================================================================

View File

@ -1,7 +1,7 @@
from flask import Blueprint, request, jsonify, current_app
from app.services.outbound_service import OutboundService
from flask_jwt_extended import jwt_required, get_jwt_identity, get_jwt
from app.utils.decorators import permission_required, audit_log, prevent_double_submit
from app.utils.decorators import permission_required, audit_log, prevent_double_submit, is_privileged_viewer
from app.services.auth_service import AuthService
import traceback
@ -175,8 +175,22 @@ def get_outbound_list():
search_type = request.args.get('search_type', 'all')
company = request.args.get('company', '')
# ★ 数据权限:普通用户只看“领用人=本人姓名(不含账号前缀)”的出库记录;管理者看全部
consumer_name = None
if not is_privileged_viewer():
_identity = get_jwt_identity()
if _identity:
from app.models.system import SysUser
_u = SysUser.query.get(int(_identity))
# username 形如 “中文名/xiaolongxia” → 取“/”前的领用人姓名
_uname = _u.username if _u else ''
consumer_name = _uname.split('/')[0].strip() if _uname else None
# ★ [修改] 调用分组查询服务,支持搜索类型
result = OutboundService.get_grouped_list(page, limit, keyword, search_type=search_type, company=company)
result = OutboundService.get_grouped_list(
page, limit, keyword, search_type=search_type,
company=company, consumer_name=consumer_name
)
# 字段级脱敏
user_permissions = get_current_user_permissions()
@ -429,6 +443,28 @@ def create_outbound_request():
return jsonify({'code': 500, 'msg': f'服务器内部错误: {str(e)}'}), 500
# --------------------------------------------------------
# 4.1 出库申请预检(判断所选物料是否需审批,驱动前端是否显示审批人)
# POST /api/v1/outbound/request/check-approval
# --------------------------------------------------------
@outbound_bp.route('/request/check-approval', methods=['POST'])
@jwt_required()
@permission_required('outbound_selection')
def check_outbound_approval():
try:
data = request.get_json() or {}
items = data.get('items', []) or []
from app.services.approval_control import resolve_approval_control
need_approval, flagged = resolve_approval_control(items)
return jsonify({
"code": 200, "msg": "success",
"data": {"need_approval": need_approval, "materials": flagged}
}), 200
except Exception as e:
traceback.print_exc()
return jsonify({"code": 500, "msg": f"预检失败: {str(e)}"}), 500
# --------------------------------------------------------
# 5. 审批出库申请
# PATCH /api/v1/outbound/request/<id>/approve
@ -551,6 +587,11 @@ def get_outbound_request_list():
if status is not None:
status = int(status)
# ★ 数据权限:普通申请人只能看“自己的”出库记录;库管/主管/超管(或跨域)才可看他人
if not is_privileged_viewer():
identity = get_jwt_identity()
applicant_id = int(identity) if identity else None
result = OutboundApprovalService.get_request_list(
page=page,
per_page=limit,
@ -584,6 +625,12 @@ def get_outbound_request_detail(request_id):
if not approval:
return jsonify({'code': 404, 'msg': '审批单不存在'}), 404
# ★ 数据权限:普通申请人只能看自己的单;库管/主管/超管(或跨域)可看任意
if not is_privileged_viewer():
identity = get_jwt_identity()
if int(approval.applicant_id or 0) != int(identity or 0):
return jsonify({'code': 403, 'msg': '无权查看他人的出库记录'}), 403
return jsonify({
'code': 200,
'msg': '获取成功',

View File

@ -1,6 +1,6 @@
from flask import Blueprint, jsonify, request # .material -> .base refactor checked
from flask_jwt_extended import jwt_required, get_jwt_identity, get_jwt
from app.utils.decorators import permission_required, audit_log, prevent_double_submit
from app.utils.decorators import permission_required, audit_log, prevent_double_submit, is_privileged_viewer
from app.services.auth_service import AuthService
from app.services.trans_service import TransService
from app.services.borrow_service import BorrowApprovalService
@ -163,7 +163,20 @@ def get_records():
keyword = request.args.get('keyword', '')
search_type = request.args.get('search_type', 'all')
res = TransService.get_records(page=page, limit=10, status=status, keyword=keyword, search_type=search_type)
# ★ 数据权限:普通用户只看“借用人=本人姓名(不含账号前缀)”的借还记录;管理者看全部
borrower_name = None
if not is_privileged_viewer():
_identity = get_jwt_identity()
if _identity:
from app.models.system import SysUser
_u = SysUser.query.get(int(_identity))
_uname = _u.username if _u else ''
borrower_name = _uname.split('/')[0].strip() if _uname else None
res = TransService.get_records(
page=page, limit=10, status=status, keyword=keyword,
search_type=search_type, borrower_name=borrower_name
)
# ★ service 层异常时code==500 的字典(带 traceback需要直通到前端便于排查
if isinstance(res, dict) and res.get('code') == 500:
@ -311,6 +324,25 @@ def close_borrow_request(request_id):
return jsonify({'code': 500, 'msg': f'服务器内部错误: {str(e)}'}), 500
# --- 借库申请预检(判断所选物料是否需审批,驱动前端是否显示审批人) ---
@trans_bp.route('/borrow/request/check-approval', methods=['POST'])
@jwt_required()
@permission_required('op_borrow_apply')
def check_borrow_approval():
try:
data = request.get_json() or {}
items = data.get('items', []) or []
from app.services.approval_control import resolve_approval_control
need_approval, flagged = resolve_approval_control(items)
return jsonify({
"code": 200, "msg": "success",
"data": {"need_approval": need_approval, "materials": flagged}
}), 200
except Exception as e:
import traceback; traceback.print_exc()
return jsonify({"code": 500, "msg": f"预检失败: {str(e)}"}), 500
# --- 获取借库审批单列表 ---
@trans_bp.route('/borrow/request', methods=['GET'])
@jwt_required()
@ -330,6 +362,11 @@ def get_borrow_request_list():
if status is not None:
status = int(status)
# ★ 数据权限:普通申请人只能看“自己的”借还记录;库管/主管/超管(或跨域)才可看他人
if not is_privileged_viewer():
identity = get_jwt_identity()
applicant_id = int(identity) if identity else None
result = BorrowApprovalService.get_request_list(
page=page, per_page=limit, applicant_id=applicant_id, status=status
)

View File

@ -39,6 +39,9 @@ class MaterialBase(db.Model):
# 强制质检标记(采购入库时必须上传检测报告)
is_inspection_required = db.Column(db.Boolean, default=False, comment='是否强制要求质检')
# 出库/借库需审批标记:命中该物料的出/借申请仍需走审批;否则默认自动通过、直接交库管执行
is_approval_required = db.Column(db.Boolean, default=False, comment='出库/借库需审批')
# 参考价格
reference_price = db.Column(db.Numeric(10, 2), nullable=True, comment='参考价格')
@ -99,6 +102,8 @@ class MaterialBase(db.Model):
'isEnabled': bool(self.is_enabled),
# 强制质检标记
'isInspectionRequired': bool(self.is_inspection_required),
# 出库/借库需审批标记
'isApprovalRequired': bool(self.is_approval_required),
# 参考价格
'referencePrice': float(self.reference_price) if self.reference_price is not None else None,
}

View File

@ -0,0 +1,88 @@
from app.models.base import MaterialBase
def _code_of(spec):
"""取规格的首段编码('S0013/S0013' -> 'S0013''S0013' -> 'S0013'"""
s = (spec or '').strip()
if not s:
return ''
return s.split('/')[0].strip().upper()
def _materials_flagged():
"""一次性取出所有“启用且需审批”的物料(量小,逐项判定前先全量拉一次)"""
rows = MaterialBase.query.filter(
MaterialBase.is_enabled == True,
MaterialBase.is_approval_required == True
).all()
return rows
def _is_int(v):
try:
int(v)
return True
except (TypeError, ValueError):
return False
def resolve_approval_control(items):
"""
判断一批出库/借库申请明细是否需要审批。
判定优先级:
1) 方案1最高明细带有效 base_id → 直接按物料主键查 is_approval_required
且只要有有效 ID无论命中与否都不再做任何字符串兜底杜绝同名/同规格误杀)。
2) 方案2无 ID 兜底):名称必须 100% 精确一致 AND 核心规格一致(斜杠前段,
或前端未传规格视为防呆拦截),两条件同时成立才判定命中。
"""
flagged = []
seen = set()
def _record(m_obj, req_name):
if m_obj.id in seen:
return
seen.add(m_obj.id)
flagged.append({
'name': m_obj.name or req_name,
'spec_model': m_obj.spec_model or '',
'base_id': m_obj.id,
})
# 1. 查出所有“启用且需审批”的危险物料库
flagged_materials = _materials_flagged()
for item in items or []:
name = str(item.get('name') or '').strip()
spec = str(item.get('spec_model') or '').strip()
base_id = item.get('base_id')
# --- 方案 1最高优先级依据 base_id 绝对判定 ---
if base_id is not None:
bid = int(base_id) if _is_int(base_id) else 0
if bid > 0:
m = MaterialBase.query.get(bid)
if m and m.is_enabled and m.is_approval_required:
_record(m, name)
# ★ 关键:只要有有效 ID无论是否需审批绝不允许往下走字符串兜底
continue
# --- 方案 2无 ID 时的降级兜底 (名称精确相等 AND 核心规格相等) ---
if not name:
continue
# 提取当前申请行的核心规格
req_core_spec = _code_of(spec)
for fm in flagged_materials:
# 条件 A名称必须 100% 绝对一致
if fm.name != name:
continue
# 条件 B核心规格一致 (或前端未传规格视为防呆拦截)
fm_core_spec = _code_of(fm.spec_model)
if not req_core_spec or req_core_spec == fm_core_spec:
_record(fm, name)
break # 命中即判定本申请行需审批,跳出内层循环
return bool(flagged), flagged

View File

@ -152,11 +152,11 @@ class BomService:
if status == 'enabled':
query = query.filter(BomTable.is_enabled == True, BomTable.is_archived == False)
elif status == 'disabled':
query = query.filter(BomTable.is_enabled == False)
query = query.filter(BomTable.is_enabled == False, BomTable.is_archived == False)
elif status == 'archived':
query = query.filter(BomTable.is_archived == True)
elif active_only:
query = query.filter(BomTable.is_enabled == True)
query = query.filter(BomTable.is_enabled == True, BomTable.is_archived == False)
# 【行级数据隔离】基于 JWT 多租户公司过滤
company_limit = get_current_company_filter()
@ -263,7 +263,7 @@ class BomService:
if status == 'enabled':
query = query.filter(BomTable.is_enabled == True, BomTable.is_archived == False)
elif status == 'disabled':
query = query.filter(BomTable.is_enabled == False)
query = query.filter(BomTable.is_enabled == False, BomTable.is_archived == False)
elif status == 'archived':
query = query.filter(BomTable.is_archived == True)
@ -404,6 +404,7 @@ class BomService:
'parent_name': parent_material.name if parent_material else '',
'parent_spec': parent_material.spec_model if parent_material else '',
'is_enabled': first.BomTable.is_enabled,
'remark': first.BomTable.remark or '', # ★ 主表备注
'children': children
}
@ -544,10 +545,9 @@ class BomService:
@staticmethod
def update_enabled(bom_no, version, is_enabled):
"""
仅更新某 BOM 版本(整组行)的启用状态,供列表/只读视图"启用/停用"开关使用
三态互斥:切“启用/停用”时同步清除“归档”
启用=在用;停用=有问题/暂停;两者都与归档互斥。
不触碰子件结构;成功后清除该 bom_no 的缓存。
注意:只按 bom_no+version 精确匹配,不加 is_enabled/is_archived 等多余过滤——
停用/归档中的版本只要记录存在即可继续改状态。
"""
bom_no = (bom_no or '').strip()
version = (version or '').strip()
@ -558,6 +558,7 @@ class BomService:
raise ValueError(f'BOM不存在bom_no={bom_no}, version={version}')
for rec in rows:
rec.is_enabled = bool(is_enabled)
rec.is_archived = False # 切启/停即退出归档(三者互斥)
db.session.commit()
_cache_delete(bom_no, version)
logger.info(f"[BOM Cache] update_enabled → 缓存已失效 bom_no={bom_no} version={version} enabled={is_enabled}")
@ -566,8 +567,9 @@ class BomService:
@staticmethod
def update_archived(bom_no, version, archived):
"""
仅更新某 BOM 版本(整组行)的归档标记。
归档=仍启用可查看/编辑,但不再作为其它 BOM 子件的引用候选;取消归档恢复正常引用
三态互斥归档:归档=彻底废弃is_archived=True 且 is_enabled=False
取消归档=恢复“启用(在用)”is_archived=False 且 is_enabled=True
归档后不出现在启用/停用视图、不作为子件引用候选。
"""
bom_no = (bom_no or '').strip()
version = (version or '').strip()
@ -576,8 +578,10 @@ class BomService:
rows = BomTable.query.filter_by(bom_no=bom_no, version=version).all()
if not rows:
raise ValueError(f'BOM不存在bom_no={bom_no}, version={version}')
target_archived = bool(archived)
for rec in rows:
rec.is_archived = bool(archived)
rec.is_archived = target_archived
rec.is_enabled = not target_archived # 归档→停用;取消归档→启用(互斥)
db.session.commit()
_cache_delete(bom_no, version)
logger.info(f"[BOM Cache] update_archived → 缓存已失效 bom_no={bom_no} version={version} archived={archived}")

View File

@ -33,6 +33,33 @@ class BorrowApprovalService:
return f"APR-BOR-{date_str}-{time_str}-{sequence:04d}"
@staticmethod
def _items_require_approval(items):
"""
明细中任一物料命中“出库/借库需审批”→ 需要审批。
申请明细只存 name/spec_model故按 (name, spec_model) 反查启用物料判定。
"""
from app.models.base import MaterialBase
seen = set()
for item in items:
name = str(item.get('name') or '').strip()
spec = str(item.get('spec_model') or '').strip()
if not name:
continue
key = (name, spec)
if key in seen:
continue
seen.add(key)
hit = MaterialBase.query.filter(
MaterialBase.name == name,
MaterialBase.spec_model == spec,
MaterialBase.is_enabled == True,
MaterialBase.is_approval_required == True
).first()
if hit:
return True
return False
@staticmethod
def submit_approval(applicant_id, items, allowed_approvers, remark=None, approver_id=None,
borrower_name=None):
@ -61,6 +88,14 @@ class BorrowApprovalService:
if not items:
raise ValueError("借库物品列表不能为空")
# ★ 借库申请必填:申请原因;预计归还日期(除非勾选长期借用/不限归还)
if not str(remark or '').strip():
raise ValueError("借库申请原因不能为空")
_has_indefinite = any(bool(item.get('is_indefinite')) for item in items)
_has_date = any(str(item.get('expected_return_time') or '').strip() for item in items)
if not _has_indefinite and not _has_date:
raise ValueError("请填写预计归还日期,或勾选长期借用(不限归还)")
required_fields = ['name', 'spec_model', 'quantity']
for idx, item in enumerate(items):
missing_fields = [f for f in required_fields if f not in item or str(item.get(f) or '').strip() == '']
@ -76,30 +111,53 @@ class BorrowApprovalService:
except (TypeError, ValueError) as e:
raise ValueError(f"{idx + 1} 条物品的 quantity 格式无效: {str(e)}")
if not allowed_approvers:
raise ValueError("必须指定至少一位审批人")
# ★ 需审批判定:明细含“出库/借库需审批”物料 → 走原审批流程;否则默认自动通过
from app.services.approval_control import resolve_approval_control
need_approval, flagged_materials = resolve_approval_control(items)
if need_approval and not approver_id:
_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 = [] # 免审批单不绑定审批人
request_no = BorrowApprovalService.generate_request_no()
approval = BorrowApproval(
request_no=request_no,
applicant_id=applicant_id,
remark=remark,
borrower_name=borrower_name,
status=0, # 待审批
)
if need_approval:
approval = BorrowApproval(
request_no=request_no,
applicant_id=applicant_id,
remark=remark,
borrower_name=borrower_name,
status=0, # 待审批
)
else:
# 默认不审批:创建即已通过(status=1),直接进入“待库管执行”
from datetime import datetime, timezone, timedelta
approval = BorrowApproval(
request_no=request_no,
applicant_id=applicant_id,
remark=remark,
borrower_name=borrower_name,
status=1,
actual_approver_id=applicant_id,
approved_at=datetime.now(timezone(timedelta(hours=8))),
)
approval.set_items(items)
approval.set_allowed_approvers(allowed_approvers)
if allowed_approvers:
approval.set_allowed_approvers(allowed_approvers)
else:
approval.allowed_approvers = '[]'
db.session.add(approval)
db.session.commit()
# ★ 创建成功后,发送邮件通知审批人(静默处理,不阻断主流程)
BorrowApprovalService._notify_new_request(approval, applicant_id, approver_id=approver_id)
# 仅需审批单通知审批人;免审批单静默进入“待库管执行”
if need_approval:
BorrowApprovalService._notify_new_request(approval, applicant_id, approver_id=approver_id)
return approval

View File

@ -327,7 +327,7 @@ class OutboundService:
raise e
@staticmethod
def get_grouped_list(page=1, per_page=10, keyword=None, search_type='all', start_date=None, end_date=None, company=None):
def get_grouped_list(page=1, per_page=10, keyword=None, search_type='all', start_date=None, end_date=None, company=None, consumer_name=None):
"""
查询出库记录(按出库单号分组),包含详细物品信息
支持跨表搜索单号、领用人、SKU、物料名称、规格型号
@ -543,6 +543,21 @@ class OutboundService:
if company_limit is not None:
stmt = stmt.filter(TransOutbound.outbound_no.in_(comp_all))
# ★ 数据权限:普通用户只看“领用人=本人姓名(不含账号前缀)”的出库记录;
# 同时兼容库里存成“姓名/xiaolongxia”全名的记录姓名 + '/' 前缀也命中)
if consumer_name:
from sqlalchemy import or_
_own_out_nos = (
db.session.query(TransOutbound.outbound_no)
.filter(or_(
TransOutbound.consumer_name == consumer_name,
TransOutbound.consumer_name.like(f"{consumer_name}/%")
))
.distinct()
.subquery()
)
stmt = stmt.filter(TransOutbound.outbound_no.in_(_own_out_nos))
stmt = stmt.order_by(desc('max_time'))
# 使用 distinct 确保跨表查询不重复
@ -700,6 +715,33 @@ class OutboundApprovalService:
return f"APR-OUT-{date_str}-{time_str}-{sequence:04d}"
@staticmethod
def _items_require_approval(items):
"""
明细中任一物料命中“出库/借库需审批”→ 需要审批。
申请明细只存 name/spec_model故按 (name, spec_model) 反查启用物料判定。
"""
from app.models.base import MaterialBase
seen = set()
for item in items:
name = str(item.get('name') or '').strip()
spec = str(item.get('spec_model') or '').strip()
if not name:
continue
key = (name, spec)
if key in seen:
continue
seen.add(key)
hit = MaterialBase.query.filter(
MaterialBase.name == name,
MaterialBase.spec_model == spec,
MaterialBase.is_enabled == True,
MaterialBase.is_approval_required == True
).first()
if hit:
return True
return False
@staticmethod
def create_request(applicant_id, items, allowed_approvers, remark=None, approver_id=None, outbound_type=None):
"""
@ -745,33 +787,55 @@ class OutboundApprovalService:
except (TypeError, ValueError) as e:
raise ValueError(f"{idx + 1} 条物品的 quantity 格式无效: {str(e)}")
# ★ 校验 allowed_approvers 非空
if not allowed_approvers:
raise ValueError("必须指定至少一位审批人")
# ★ 需审批判定:明细含“出库/借库需审批”物料 → 走原审批流程;否则默认自动通过
from app.services.approval_control import resolve_approval_control
need_approval, flagged_materials = resolve_approval_control(items)
if need_approval and not approver_id:
_names = "".join(f"{m['name']}{m['spec_model'] or '-'}" for m in flagged_materials)
raise ValueError(f"以下物料需审批出库/借库:{_names}。请选择审批人后再提交")
# ★ 指定审批人模式approver_id 覆盖 allowed_approvers
if approver_id:
allowed_approvers = [{"type": "user", "value": int(approver_id)}]
elif not need_approval:
allowed_approvers = [] # 免审批单不绑定审批人
request_no = OutboundApprovalService.generate_request_no()
approval = OutboundApproval(
request_no=request_no,
applicant_id=applicant_id,
remark=remark,
outbound_type=outbound_type, # 申请时确定的出库类型,扫码出库时带出
status=0, # 待审批
)
if need_approval:
approval = OutboundApproval(
request_no=request_no,
applicant_id=applicant_id,
remark=remark,
outbound_type=outbound_type, # 申请时确定的出库类型,扫码出库时带出
status=0, # 待审批
)
else:
# 默认不审批:创建即已通过(status=1),直接进入“待库管执行”
from datetime import datetime, timezone, timedelta
approval = OutboundApproval(
request_no=request_no,
applicant_id=applicant_id,
remark=remark,
outbound_type=outbound_type,
status=1,
actual_approver_id=applicant_id,
approved_at=datetime.now(timezone(timedelta(hours=8))),
)
# 直接存储前端传来的物料信息快照,不查询/不关联具体库存记录
approval.set_items(items)
approval.set_allowed_approvers(allowed_approvers)
if allowed_approvers:
approval.set_allowed_approvers(allowed_approvers)
else:
approval.allowed_approvers = '[]'
db.session.add(approval)
db.session.commit()
# ★ 创建成功后,发送邮件通知审批人(精确通知 approver_id 对应的邮箱)
OutboundApprovalService._notify_new_request(approval, applicant_id, approver_id=approver_id)
# 仅需审批单通知审批人;免审批单静默进入“待库管执行”
if need_approval:
OutboundApprovalService._notify_new_request(approval, applicant_id, approver_id=approver_id)
return approval

View File

@ -410,7 +410,7 @@ class TransService:
raise e
@staticmethod
def get_records(page=1, limit=10, status='all', keyword=None, search_type='all'):
def get_records(page=1, limit=10, status='all', keyword=None, search_type='all', borrower_name=None):
"""
获取借还记录列表(按单号 borrow_no 维度分页,避免明细撑爆 pageSize
@ -648,6 +648,22 @@ class TransService:
# ====================================================================
borrow_no_q = db.session.query(order_subq.c.borrow_no)
# ★ 数据权限:普通用户只看“借用人=本人姓名(不含账号前缀)”的借还记录;
# 兼容库里存成“姓名/xiaolongxia”全名(姓名 + '/' 前缀)的情况
if borrower_name:
own_borrow_nos_subq = (
db.session.query(TransBorrow.borrow_no)
.filter(or_(
TransBorrow.borrower_name == borrower_name,
TransBorrow.borrower_name.like(f"{borrower_name}/%")
))
.distinct()
.subquery()
)
borrow_no_q = borrow_no_q.filter(
order_subq.c.borrow_no.in_(own_borrow_nos_subq)
)
# 关键词过滤
if keyword_borrow_nos_subq is not None:
borrow_no_q = borrow_no_q.filter(

View File

@ -226,6 +226,28 @@ def _has_cross_domain_permission(user_role, user_company=''):
return False
# 能查看“他人借还/出库记录”的管理者角色(仓库管理员/主管/超管)。普通申请人(含入库员/出库员等)只能看自己的记录。
# 注意:入库员(INBOUND)、出库员(OUTBOUND)不是管理者视角,不应放此名单;如需让他们看全部请确认为库管角色后再加入。
PRIVILEGED_VIEWER_ROLES = ('SUPER_ADMIN', 'SUPERVISOR', 'WAREHOUSE_MGR')
def is_privileged_viewer():
"""
判断当前登录用户是否属于“管理者视角”(可查看他人借还/出库记录)。
规则:角色为 SUPER_ADMIN/SUPERVISOR/WAREHOUSE_MGR或拥有 crossDomain 权限。
普通申请人(其余角色)只能看自己的记录(由各列表 handler 强制 applicant_id=当前用户)。
"""
from flask_jwt_extended import get_jwt
claims = get_jwt()
role = (claims.get('role') or '').upper()
if role in PRIVILEGED_VIEWER_ROLES:
return True
user_company = claims.get('company_name', '')
if _has_cross_domain_permission(role, user_company):
return True
return False
def get_current_company_filter():
"""
多租户数据权限隔离工具函数。

View File

@ -14,6 +14,7 @@ STOCK_FIELD_RBAC_MAPPING = {
"category": "material_list:category", "type": "material_list:type",
"spec": "material_list:spec", "unit": "material_list:unit",
"companyName": "material_list:companyName", "isInspectionRequired": "material_list:isInspectionRequired",
"isApprovalRequired": "material_list:isApprovalRequired",
"generalImage": "material_list:files", "generalManual": "material_list:files",
"productImageRemark": "material_list:productImageRemark",
"manualLinkRemark": "material_list:manualLinkRemark",

View File

@ -239,7 +239,7 @@ const handleLogout = () => {
<footer v-if="!isLoginPage" class="app-footer">
<span class="version-tag">
<el-icon style="vertical-align: middle; margin-right: 4px"><InfoFilled /></el-icon>
当前版本:V3.73
当前版本:V3.74
</span>
</footer>

View File

@ -71,6 +71,15 @@ export function batchSetInspection(data: { ids: number[], isInspectionRequired:
})
}
// 6.1 批量设置“出库/借库需审批”
export function batchSetApprovalRequired(data: { ids: number[], isApprovalRequired: boolean }) {
return request({
url: '/inbound/base/batch-approval',
method: 'post',
data
})
}
// 7. 获取智能分组规格最大连号
export function getLatestSpecs() {
return request({

View File

@ -99,6 +99,15 @@ export function submitOutboundRequest(data: {
})
}
// 出库申请预检:判断所选物料是否需审批
export function checkOutboundApproval(data: { items: Array<{ name?: string; spec_model?: string }> }) {
return request({
url: '/v1/outbound/request/check-approval',
method: 'post',
data
})
}
/**
* 获取出库审批申请单列表
* @param params 支持 status, page, limit

View File

@ -149,6 +149,15 @@ export function submitBorrowRequest(data: {
})
}
// 借库申请预检:判断所选物料是否需审批
export function checkBorrowApproval(data: { items: Array<{ name?: string; spec_model?: string }> }) {
return request({
url: '/v1/transactions/borrow/request/check-approval',
method: 'post',
data
})
}
/**
* 获取借库审批申请单列表
* @param params 支持 status, page, limit

View File

@ -394,7 +394,10 @@
</el-table>
<el-form label-width="100px">
<el-form-item label="* 指定审批人" required>
<el-form-item v-if="approvalNeedVisible" label="指定审批人">
<div v-if="approvalNeedText" style="color:#E6A23C; font-size:12px; line-height:1.4; margin-bottom:4px;">
以下物料需审批出库/借库{{ approvalNeedText }}请选择审批人
</div>
<el-select
v-model="requestApproverId"
placeholder="请选择审批人"
@ -409,7 +412,7 @@
/>
</el-select>
</el-form-item>
<el-form-item label="预计归还日期">
<el-form-item label="预计归还日期" :required="!requestIndefinite">
<el-date-picker
v-model="requestExpectedReturn"
type="date"
@ -423,12 +426,12 @@
长期借用
</el-checkbox>
</el-form-item>
<el-form-item label="申请原因">
<el-form-item label="申请原因" required>
<el-input
v-model="requestRemark"
type="textarea"
:rows="3"
placeholder="请填写借库申请原因(填)"
placeholder="请填写借库申请原因(填)"
maxlength="200"
show-word-limit
/>
@ -518,7 +521,7 @@ import { Printer, Search, Plus, Download, List } from '@element-plus/icons-vue'
import { ElMessage, ElTable, ElMessageBox } from 'element-plus'
import { printSelectionList } from '@/api/inbound/stock'
import { useUserStore } from '@/stores/user'
import { submitBorrowRequest, getBorrowStockList } from '@/api/transaction'
import { submitBorrowRequest, getBorrowStockList, checkBorrowApproval } from '@/api/transaction'
import { bomMatchStock } from '@/api/outbound'
import { getApproversList } from '@/api/auth'
import { getBomList, getBomDetail, getBomWithStock } from '@/api/bom'
@ -554,6 +557,9 @@ const requestIndefinite = ref(false)
const requestApproverId = ref<number | null>(null)
const approvers = ref<any[]>([])
const requestSubmitting = ref(false)
// 是否需审批(由提交前预检驱动,需审批才显示审批人输入框)
const approvalNeedVisible = ref(false)
const approvalNeedText = ref('')
const stockList = ref<any[]>([])
const stockTotal = ref(0)
@ -1052,6 +1058,8 @@ const openRequestDialog = () => {
requestApproverId.value = null
requestExpectedReturn.value = ''
requestIndefinite.value = false
approvalNeedVisible.value = false // 打开时默认隐藏审批人,命中需审批时由预检显示
approvalNeedText.value = ''
loadApprovers()
requestDialogVisible.value = true
}
@ -1094,26 +1102,58 @@ const buildRemarkWithShortage = (baseRemark: string): string => {
}
const confirmSubmitRequest = async () => {
// ★ 借库申请必填拦截:申请原因;预计归还日期(除非长期借用/不限归还)
const _reasonText = requestRemark.value.trim()
if (!_reasonText) {
ElMessage.warning('请填写借库申请原因')
return
}
if (!requestIndefinite.value && !requestExpectedReturn.value) {
ElMessage.warning('请填写预计归还日期,或勾选“长期借用(不限归还)”')
return
}
requestSubmitting.value = true
try {
const items = validSelectedItems.value.map((item: any) => ({
base_id: item.base_id ?? null,
name: item.name || '',
spec_model: item.standard || '',
warehouse_location: item.warehouse_location || '',
quantity: item.export_quantity || 0,
expected_return_time: requestIndefinite.value ? null : requestExpectedReturn.value,
is_indefinite: requestIndefinite.value
}))
// ★ 提交前预检:判断所选物料是否需审批 → 只有需审批才显示并必选审批人
let needApproval = false
let flagged: any[] = []
try {
const chk: any = await checkBorrowApproval({ items })
needApproval = !!chk?.data?.need_approval
flagged = chk?.data?.materials || []
} catch (e) { needApproval = false; flagged = [] }
approvalNeedVisible.value = needApproval
approvalNeedText.value = flagged.map((m: any) => `${m.name}${m.spec_model || '-'}`).join('')
if (needApproval && !requestApproverId.value) {
ElMessage.warning(`以下物料需审批出库/借库:${approvalNeedText.value}。请先选择审批人`)
return
}
const payload = {
items: validSelectedItems.value.map((item: any) => ({
name: item.name || '',
spec_model: item.standard || '',
warehouse_location: item.warehouse_location || '',
quantity: item.export_quantity || 0,
expected_return_time: requestIndefinite.value ? null : requestExpectedReturn.value,
is_indefinite: requestIndefinite.value
})),
items,
remark: buildRemarkWithShortage(requestRemark.value.trim()),
approver_id: requestApproverId.value
approver_id: needApproval ? requestApproverId.value : null
}
await submitBorrowRequest(payload)
requestDialogVisible.value = false
selectedItems.value = []
ElMessage.success('借库申请已提交,等待主管审批!')
approvalNeedVisible.value = false
approvalNeedText.value = ''
ElMessage.success('借库申请已提交(含需审批物料时将进入审批,否则直接交库管执行)')
} catch (err: any) {
ElMessage.error(err?.message || err?.msg || '提交申请失败,请重试')
} finally {

View File

@ -328,6 +328,15 @@
</el-tag>
</template>
</el-table-column>
<el-table-column v-if="columns.isApprovalRequired.visible && userStore.hasPermission('material_list:isApprovalRequired')" label="出库/借库需审批" min-width="130" align="center">
<template #default="scope">
<el-switch
:model-value="!!scope.row.isApprovalRequired"
:disabled="!userStore.hasPermission('material_list:isApprovalRequired')"
@change="(val: boolean) => toggleApprovalRequired(scope.row, val)"
/>
</template>
</el-table-column>
<el-table-column v-if="columns.referencePrice.visible" prop="referencePrice" label="参考价格" min-width="120" align="center" sortable="custom">
<template #default="scope">
<span v-if="scope.row.referencePrice != null" class="money-text">{{ scope.row.referencePrice?.toFixed(2) }}</span>
@ -729,6 +738,7 @@ import {
exportAssetStatistics,
batchSetWarning,
batchSetInspection,
batchSetApprovalRequired,
markWarningOrdered,
getMaterialUnitsAPI
} from '@/api/material_base';
@ -958,6 +968,7 @@ const columns = reactive({
files: { visible: true },
isEnabled: { visible: true },
isInspectionRequired: { visible: true },
isApprovalRequired: { visible: true },
referencePrice: { visible: true },
warningStatus: { visible: true }
});
@ -977,6 +988,7 @@ const permissionMap: Record<string, string> = {
files: 'material_list:files',
isEnabled: 'material_list:isEnabled',
isInspectionRequired: 'material_list:operation',
isApprovalRequired: 'material_list:operation',
referencePrice: 'material_list:referencePrice',
warningStatus: 'material_list:view_warning'
};
@ -1747,6 +1759,20 @@ const submitBatchInspection = async () => {
}
};
// 单行切换“出库/借库需审批”
const toggleApprovalRequired = async (row: any, val: boolean) => {
if (!row || !row.id) return;
const prev = !!row.isApprovalRequired;
row.isApprovalRequired = val; // 乐观更新
try {
await batchSetApprovalRequired({ ids: [row.id], isApprovalRequired: val });
ElMessage.success(val ? '已标记:该物料出库/借库需审批' : '已取消:该物料出库/借库免审批');
} catch (error: any) {
row.isApprovalRequired = prev;
ElMessage.error(error?.msg || '设置失败');
}
};
// 表格行样式(根据预警状态)
const tableRowClassName = ({ row }: { row: MaterialBaseVO }) => {
if (row.warningStatus === 2) {

View File

@ -409,7 +409,10 @@
<el-option label="维修出库" value="REPAIR" />
</el-select>
</el-form-item>
<el-form-item label="* 指定审批人" required>
<el-form-item v-if="approvalNeedVisible" label="指定审批人">
<div v-if="approvalNeedText" style="color:#E6A23C; font-size:12px; line-height:1.4; margin-bottom:4px;">
以下物料需审批出库/借库{{ approvalNeedText }}请选择审批人
</div>
<el-select
v-model="requestApproverId"
placeholder="请选择审批人"
@ -519,7 +522,7 @@ import { ElMessage, ElTable, ElMessageBox } from 'element-plus'
import { getStockList, printSelectionList } from '@/api/inbound/stock'
import { getBomList, getBomDetail, getBomWithStock } from '@/api/bom'
import { useUserStore } from '@/stores/user'
import { submitOutboundRequest, bomMatchStock } from '@/api/outbound'
import { submitOutboundRequest, bomMatchStock, checkOutboundApproval } from '@/api/outbound'
import { getApproversList } from '@/api/auth'
const userStore = useUserStore()
@ -551,6 +554,9 @@ const requestOutboundType = ref('') // 申请时必选出库类型,扫码出
const requestApproverId = ref<number | null>(null)
const approvers = ref<any[]>([])
const requestSubmitting = ref(false)
// 是否需审批(由提交前预检驱动,需审批才显示审批人输入框)
const approvalNeedVisible = ref(false)
const approvalNeedText = ref('')
const stockList = ref<any[]>([]) // 服务端分页数据
const stockTotal = ref(0)
@ -1079,6 +1085,8 @@ const openRequestDialog = () => {
}
requestRemark.value = ''
requestApproverId.value = null
approvalNeedVisible.value = false // 打开时默认隐藏审批人,命中需审批时由预检显示
approvalNeedText.value = ''
loadApprovers()
requestDialogVisible.value = true
}
@ -1100,10 +1108,6 @@ const confirmSubmitRequest = async () => {
ElMessage.warning('请填写申请原因')
return
}
if (!requestApproverId.value) {
ElMessage.warning('请选择指定审批人')
return
}
if (!requestOutboundType.value) {
ElMessage.warning('请选择出库类型')
return
@ -1111,17 +1115,36 @@ const confirmSubmitRequest = async () => {
requestSubmitting.value = true
try {
const items = validSelectedItems.value.map(item => ({
material_type: item.typeLabel || item.type || '',
base_id: item.base_id ?? null,
name: item.name || '',
spec_model: item.standard || '',
warehouse_location: item.warehouse_location || '',
quantity: item.export_quantity || 0
}))
// ★ 提交前预检:判断所选物料是否需审批 → 只有需审批才显示并必选审批人
let needApproval = false
let flagged: any[] = []
try {
const chk: any = await checkOutboundApproval({ items })
needApproval = !!chk?.data?.need_approval
flagged = chk?.data?.materials || []
} catch (e) { needApproval = false; flagged = [] }
approvalNeedVisible.value = needApproval
approvalNeedText.value = flagged.map((m: any) => `${m.name}${m.spec_model || '-'}`).join('')
if (needApproval && !requestApproverId.value) {
ElMessage.warning(`以下物料需审批出库/借库:${approvalNeedText.value}。请先选择审批人`)
return
}
const payload: any = {
items: validSelectedItems.value.map(item => ({
material_type: item.typeLabel || item.type || '',
name: item.name || '',
spec_model: item.standard || '',
warehouse_location: item.warehouse_location || '',
quantity: item.export_quantity || 0
})),
items,
remark: buildRemarkWithShortage(trimmed),
outbound_type: requestOutboundType.value,
approver_id: requestApproverId.value
approver_id: needApproval ? requestApproverId.value : null
}
await submitOutboundRequest(payload)
@ -1129,7 +1152,9 @@ const confirmSubmitRequest = async () => {
// 成功:关闭弹窗、清空列表、提示
requestDialogVisible.value = false
selectedItems.value = []
ElMessage.success('出库申请已提交,等待主管审批!')
approvalNeedVisible.value = false
approvalNeedText.value = ''
ElMessage.success('出库申请已提交(含需审批物料时将进入审批,否则直接交库管执行)')
} catch (err: any) {
ElMessage.error(err?.message || err?.msg || '提交申请失败,请重试')
} finally {

View File

@ -1933,25 +1933,24 @@ const confirmPurchaseImport = () => {
const qty = Number(po.quantity) || 1
form.in_quantity = qty
// 价格字段:优先用含税总价反算,否则用含税单价
if (hasFormFieldPermission('unit_price')) {
const hasUnitPrice = po.unit_price !== null && po.unit_price !== undefined && Number(po.unit_price) > 0
const hasTotalPrice = po.total_price !== null && po.total_price !== undefined && Number(po.total_price) > 0
// 价格字段:无论前端是否对当前角色显示价格,导入都应把采购单价格带入(隐藏字段,用于成本)。
// 库管在前端看不到价格输入/列,由字段权限控制展示;这里只做数据带入,不因“不能看价格”而跳过。
const hasUnitPrice = po.unit_price !== null && po.unit_price !== undefined && Number(po.unit_price) > 0
const hasTotalPrice = po.total_price !== null && po.total_price !== undefined && Number(po.total_price) > 0
// 税率带过来
if (po.tax_rate != null) {
form.tax_rate = Number(po.tax_rate)
}
// 税率带过来
if (po.tax_rate != null) {
form.tax_rate = Number(po.tax_rate)
}
if (hasTotalPrice) {
// 优先: 含税总价 → 调用 post_total 逻辑反算单价
postTaxTotalInput.value = Number(po.total_price)
onPostTaxTotalChange(postTaxTotalInput.value)
} else if (hasUnitPrice) {
// 备选: 含税单价 → 反算不含税
form.post_tax_unit_price = Number(po.unit_price)
updatePrices('post')
}
if (hasTotalPrice) {
// 优先: 含税总价 → 调用 post_total 逻辑反算单价
postTaxTotalInput.value = Number(po.total_price)
onPostTaxTotalChange(postTaxTotalInput.value)
} else if (hasUnitPrice) {
// 备选: 含税单价 → 反算不含税
form.post_tax_unit_price = Number(po.unit_price)
updatePrices('post')
}
if (po.supplier_link) {
form.detail_link = po.supplier_link

View File

@ -435,6 +435,8 @@ const handleApprovalChange = (val: number | null) => {
isIndefinite.value = false
form.expected_return_time = ''
}
// ★ 自动关联:把该借库申请单的“申请原因(remark)”自动带入执行备注,库管无需重复填写(可手动改)
form.remark = (req && req.remark) || ''
cartItems.value = []
signatureFile.value = null
signaturePreviewUrl.value = ''