Compare commits
9 Commits
91c4ae85db
...
b4e43d641a
| Author | SHA1 | Date | |
|---|---|---|---|
| b4e43d641a | |||
| ac22c9959b | |||
| 825c088e98 | |||
| f2bb95becc | |||
| 243355bc68 | |||
| eafdf992c7 | |||
| e3fe1fc12a | |||
| ccdd6e7306 | |||
| 1eaa203eba |
68
db_migrations/phase10_purchase_supplier_link_text.sql
Normal file
68
db_migrations/phase10_purchase_supplier_link_text.sql
Normal file
@ -0,0 +1,68 @@
|
||||
-- =============================================================================
|
||||
-- 采购申请「商家地址链接」放宽为 text
|
||||
--
|
||||
-- 现象
|
||||
-- 新建采购申请时粘贴电商商品链接报错。实测该链接 **516 字符**,而
|
||||
-- purchase_request.supplier_link 是 varchar(500) —— 超出 16 个字符,
|
||||
-- PostgreSQL 直接拒绝(value too long for type character varying(500))。
|
||||
--
|
||||
-- 为什么用 text 而不是把 500 调大
|
||||
-- 电商外链(淘宝/1688 等)普遍带很长的跟踪参数,长度没有稳定上界:
|
||||
-- 本例已 516,再叠加一轮营销参数就可能破千。任何有限的 varchar(N) 都只是
|
||||
-- 把报错往后推,而且**截断是静默的** —— 用户只会觉得「链接打不开」。
|
||||
-- 同 material_base.purchase_link 的处理(那边一开始就用 text)。
|
||||
--
|
||||
-- 附带确认(无需改动)
|
||||
-- 全库扫描 varchar 且列名像 link/url/photo/image/signature/path 的列,仅本列
|
||||
-- 是用户粘贴的外链;sys_menu.path(200)、sys_warehouse_location.full_path(500)
|
||||
-- 是内部生成数据,stock_adjustment.linked_*(50/100) 是 SKU 与单号,均不受影响。
|
||||
--
|
||||
-- 幂等:ALTER TYPE 对已是 text 的列会报错,故用 DO 块先判断当前类型。
|
||||
-- 不含 psql 元命令,DataGrip 可直接执行。
|
||||
-- =============================================================================
|
||||
|
||||
BEGIN;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'purchase_request'
|
||||
AND column_name = 'supplier_link'
|
||||
AND data_type = 'character varying'
|
||||
) THEN
|
||||
-- 加长类型不会丢数据(varchar → text 是放宽,不是转换)
|
||||
ALTER TABLE purchase_request
|
||||
ALTER COLUMN supplier_link TYPE text;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
COMMENT ON COLUMN purchase_request.supplier_link IS
|
||||
'商家地址链接。text 无长度上限 —— 电商外链常带很长的跟踪参数,varchar(N) 会把「链接太长」变成静默失败';
|
||||
|
||||
COMMIT;
|
||||
|
||||
|
||||
-- =============================================================================
|
||||
-- 执行后核对
|
||||
-- =============================================================================
|
||||
SELECT '=== 1) 列类型应为 text(character_maximum_length 为空)===' AS "核对项";
|
||||
SELECT column_name, data_type, character_maximum_length AS 上限
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'purchase_request' AND column_name = 'supplier_link';
|
||||
|
||||
SELECT '=== 2) 现有数据长度分布(确认无截断)===' AS "核对项";
|
||||
SELECT count(*) AS 链接总数,
|
||||
count(*) FILTER (WHERE supplier_link IS NOT NULL AND supplier_link <> '') AS 非空数,
|
||||
coalesce(max(length(supplier_link)), 0) AS 最长字符数
|
||||
FROM purchase_request;
|
||||
|
||||
|
||||
-- =============================================================================
|
||||
-- 回滚段
|
||||
-- 注意:回滚前必须先确认没有超过 500 字符的数据,否则会失败。
|
||||
-- =============================================================================
|
||||
-- BEGIN;
|
||||
-- ALTER TABLE purchase_request
|
||||
-- ALTER COLUMN supplier_link TYPE varchar(500);
|
||||
-- COMMIT;
|
||||
112
db_migrations/phase9_fix_return_time_utc.sql
Normal file
112
db_migrations/phase9_fix_return_time_utc.sql
Normal file
@ -0,0 +1,112 @@
|
||||
-- =============================================================================
|
||||
-- 修正存量「归还时间 / 报废时间」的 8 小时时区偏差
|
||||
--
|
||||
-- 问题
|
||||
-- process_return 与 scrap_sources.deduct 原先用 datetime.now() 写 return_time /
|
||||
-- operation_time,取的是**容器本地时间**(Docker 默认 UTC);而同一行的
|
||||
-- borrow_time 由 beijing_time() 写入。同一行两个时钟,差 8 小时。
|
||||
-- 实测(开发库):53 行 trans_borrow 出现「归还时间早于借出时间」这种
|
||||
-- 物理上不可能的数据,例如 id=80 借出 09-09 11:04、归还 09-09 03:46。
|
||||
--
|
||||
-- 代码侧已修
|
||||
-- 提交 f7c49f4(2026-09-17 09:18 北京时间)起改用 beijing_time(),
|
||||
-- **新写入的数据已经是北京时间**(已验证 TransReturn / TransBorrowReturn /
|
||||
-- TransScrap 三张表的模型默认值均为北京)。本脚本只处理存量。
|
||||
--
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- ★★ 执行前必读:请先跑「第 0 步 自检」,据其结果决定是否执行 ★★
|
||||
--
|
||||
-- 1) 若自检的「异常行数」为 0,且 UTC 区间计数为 0 —— 说明**本服务器无此问题**
|
||||
-- (例如 API 容器的 TZ 本就是 Asia/Shanghai,datetime.now() 一直是北京时间),
|
||||
-- **不要执行第 2 步**,否则会把正确的时间又 +8 小时。
|
||||
--
|
||||
-- 2) 分界时间 @boundary 的含义:**修复代码在本服务器部署生效的时刻**。
|
||||
-- · 该时刻之前写入的行 → UTC,需 +8h;
|
||||
-- · 该时刻之后写入的行 → 北京时间,不能动。
|
||||
-- 开发库用的是提交时间 2026-09-17 09:18(北京)= 01:18(UTC),
|
||||
-- 两组数据之间通常有一段空档,请用自检查看空档是否存在。
|
||||
-- 若你的部署时间不同,请替换下方两处 '2026-09-17 01:18:00'。
|
||||
--
|
||||
-- 幂等性
|
||||
-- ⚠ 本脚本**不幂等** —— 重复执行会把已经修正过的时间再加 8 小时。
|
||||
-- 请在执行前确认已备份,且只执行一次。
|
||||
--
|
||||
-- 回滚
|
||||
-- 把两处 `+ interval '8 hours'` 改成 `- interval '8 hours'`,用同一条件再执行一次。
|
||||
-- =============================================================================
|
||||
|
||||
|
||||
-- =============================================================================
|
||||
-- 第 0 步:自检(只读,先跑这一段看结果)
|
||||
-- =============================================================================
|
||||
SELECT '=== 0.1) 物理上不可能的「归还早于借出」行数 ===' AS "核对项";
|
||||
SELECT count(*) AS 异常行数
|
||||
FROM trans_borrow
|
||||
WHERE return_time IS NOT NULL AND borrow_time IS NOT NULL
|
||||
AND return_time < borrow_time;
|
||||
|
||||
SELECT '=== 0.2) 按分界切分:UTC 段 / 空档 / 北京段 ===' AS "核对项";
|
||||
SELECT
|
||||
count(*) FILTER (WHERE return_time < '2026-09-17 01:18:00') AS UTC段_需修正,
|
||||
count(*) FILTER (WHERE return_time >= '2026-09-17 01:18:00'
|
||||
AND return_time < '2026-09-17 09:18:00') AS 空档_应为0,
|
||||
count(*) FILTER (WHERE return_time >= '2026-09-17 09:18:00') AS 北京段_不可动
|
||||
FROM trans_borrow
|
||||
WHERE return_time IS NOT NULL;
|
||||
|
||||
SELECT '=== 0.3) trans_scrap 同样切分 ===' AS "核对项";
|
||||
SELECT
|
||||
count(*) FILTER (WHERE operation_time < '2026-09-17 01:18:00') AS UTC段_需修正,
|
||||
count(*) FILTER (WHERE operation_time >= '2026-09-17 01:18:00'
|
||||
AND operation_time < '2026-09-17 09:18:00') AS 空档_应为0,
|
||||
count(*) FILTER (WHERE operation_time >= '2026-09-17 09:18:00') AS 北京段_不可动
|
||||
FROM trans_scrap;
|
||||
|
||||
-- 判读:
|
||||
-- · 0.1 为 0 且 0.2/0.3 的 UTC 段也为 0 → 无需修正,停止。
|
||||
-- · 0.1 大于 0 → 确有此问题,可继续第 2 步。
|
||||
|
||||
|
||||
-- =============================================================================
|
||||
-- 第 1 步:备份(强烈建议)
|
||||
-- =============================================================================
|
||||
-- CREATE TABLE trans_borrow_bak_20260918 AS
|
||||
-- SELECT id, return_time, return_operator FROM trans_borrow;
|
||||
-- CREATE TABLE trans_scrap_bak_20260918 AS
|
||||
-- SELECT id, operation_time FROM trans_scrap;
|
||||
|
||||
|
||||
-- =============================================================================
|
||||
-- 第 2 步:修正(确认第 0 步结果后再执行)
|
||||
-- =============================================================================
|
||||
BEGIN;
|
||||
|
||||
UPDATE trans_borrow
|
||||
SET return_time = return_time + interval '8 hours'
|
||||
WHERE return_time IS NOT NULL
|
||||
AND return_time < '2026-09-17 01:18:00'; -- ← 替换为你的部署时刻(UTC 口径)
|
||||
|
||||
UPDATE trans_scrap
|
||||
SET operation_time = operation_time + interval '8 hours'
|
||||
WHERE operation_time < '2026-09-17 01:18:00'; -- ← 同上
|
||||
|
||||
COMMIT;
|
||||
|
||||
|
||||
-- =============================================================================
|
||||
-- 第 3 步:执行后核对
|
||||
-- =============================================================================
|
||||
SELECT '=== 3.1) 异常行数应为 0 ===' AS "核对项";
|
||||
SELECT count(*) AS 异常行数
|
||||
FROM trans_borrow
|
||||
WHERE return_time IS NOT NULL AND borrow_time IS NOT NULL
|
||||
AND return_time < borrow_time;
|
||||
|
||||
SELECT '=== 3.2) 抽样:归还时间应晚于借出时间,且落在工作时段 ===' AS "核对项";
|
||||
SELECT id,
|
||||
to_char(borrow_time, 'MM-DD HH24:MI') AS 借出,
|
||||
to_char(return_time, 'MM-DD HH24:MI') AS 归还
|
||||
FROM trans_borrow
|
||||
WHERE return_time IS NOT NULL
|
||||
ORDER BY return_time DESC
|
||||
LIMIT 8;
|
||||
@ -17,7 +17,12 @@ class PurchaseRequest(db.Model):
|
||||
spec_model = db.Column(db.String(255), comment='规格型号')
|
||||
quantity = db.Column(db.Numeric(19, 4), nullable=False, comment='采购数量')
|
||||
purchase_date = db.Column(db.Date, nullable=False, comment='采购时间')
|
||||
supplier_link = db.Column(db.String(500), comment='商家地址链接')
|
||||
# ★ 用 Text 而非 String(N):电商外链常带很长的跟踪参数,长度没有稳定上界
|
||||
# (实测一条淘宝链接已 516 字符,超过原先的 varchar(500) 直接报错)。
|
||||
# 任何有限的 varchar(N) 都只是把报错往后推,且**截断是静默的** ——
|
||||
# 用户只会觉得「链接打不开」。同 material_base.purchase_link 的处理。
|
||||
# DDL 见 db_migrations/phase10_purchase_supplier_link_text.sql
|
||||
supplier_link = db.Column(db.Text, comment='商家地址链接')
|
||||
remark = db.Column(db.Text, comment='备注信息')
|
||||
images = db.Column(db.Text, comment='图片列表JSON')
|
||||
unit_price = db.Column(db.Numeric(19, 4), default=0, comment='不含税单价')
|
||||
|
||||
@ -16,11 +16,23 @@ def _borrow_user_name(uid):
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
**经手库管**的展示映射(注意:不是「归还人」—— 两者是不同的人)。
|
||||
|
||||
该字段历史上存过三种口径,统一归一到「展示名」:
|
||||
· 数字 user id(更早期) → 反查 sys_user 取姓名;
|
||||
· 完整 username「姓名/拼音」 → 取斜杠前段(与全站展示口径一致);
|
||||
· 已经是展示名 → 原样返回。
|
||||
"""
|
||||
if not op:
|
||||
return op
|
||||
s = str(op).strip()
|
||||
if s.isdigit():
|
||||
mapped = _borrow_user_name(int(s))
|
||||
if mapped:
|
||||
return mapped.split('/')[0] if '/' in mapped else mapped
|
||||
return op
|
||||
return s.split('/')[0] if '/' in s else op
|
||||
|
||||
|
||||
class TransBorrow(db.Model):
|
||||
|
||||
@ -1089,7 +1089,10 @@ class TransService:
|
||||
# 无限期梯队内:单号内最早借出时间(呆滞借用盘点)
|
||||
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')
|
||||
func.max(case((TransBorrow.expected_return_time.isnot(None), 1), else_=0)).label('has_finite'),
|
||||
# 「已归还」页签的排序键:单号内**最晚**一次归还时间
|
||||
# (多明细分批归还时,整单结清的那一刻才是有意义的节点)
|
||||
func.max(TransBorrow.return_time).label('max_return_time')
|
||||
)
|
||||
.group_by(TransBorrow.borrow_no)
|
||||
.subquery()
|
||||
@ -1434,18 +1437,31 @@ class TransService:
|
||||
)
|
||||
)
|
||||
|
||||
# ★ 默认排序(多级复合,符合"优先关注快到期/逾期"业务):
|
||||
# 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)),
|
||||
# ★ 无限期梯队内按借出时间**从近到远**(desc)。
|
||||
# 原实现是 asc「借出越久越靠前」,设计意图是暴露呆滞借用;
|
||||
# 业务方明确要求改为从近到远,故反转。
|
||||
desc(order_subq.c.min_borrow_time)
|
||||
# ★ 排序按页签分开:
|
||||
#
|
||||
# 「已归还」页签 —— 按**归还时间倒序**(从近到远)。
|
||||
# 这张列表此时回答的是「最近还了哪几笔」,而不是「哪笔快到期」,
|
||||
# 所以不能沿用未归还那套「逾期优先」的排序,否则最近刚还的
|
||||
# 反而排在最后。取单号内**最晚**一次归还时间:多明细分批归还时,
|
||||
# 整单结清的那一刻才是有意义的节点,也与主行「归还时间」列的
|
||||
# 展示口径一致(前端同样取 latest)。
|
||||
#
|
||||
# 其余页签(全部 / 未归还)—— 沿用「优先关注快到期/逾期」:
|
||||
# 1) 有限期单(含 expected_return_time)排前,无限期单排后
|
||||
# 2) 有限期内按最早预计归还时间 ASC(越快到期/逾期越久越靠前)
|
||||
# 3) 无限期内按最早借出时间 DESC(从近到远)
|
||||
_order_by = (
|
||||
[nullslast(desc(order_subq.c.max_return_time)),
|
||||
desc(order_subq.c.borrow_no)] # 同一时刻的稳定兜底
|
||||
if status == 'returned' else
|
||||
[case((order_subq.c.has_finite == 0, 1), else_=0).asc(),
|
||||
nullslast(asc(order_subq.c.sort_key)),
|
||||
# ★ 无限期梯队内按借出时间**从近到远**(desc)。
|
||||
# 原实现是 asc「借出越久越靠前」,设计意图是暴露呆滞借用;
|
||||
# 业务方明确要求改为从近到远,故反转。
|
||||
desc(order_subq.c.min_borrow_time)]
|
||||
)
|
||||
borrow_no_q = borrow_no_q.order_by(*_order_by)
|
||||
|
||||
# 分页(基准 = borrow_no 单号数)
|
||||
pagination = borrow_no_q.paginate(page=page, per_page=limit, error_out=False)
|
||||
@ -1570,6 +1586,35 @@ class TransService:
|
||||
TransBorrowTransfer.status == TRANSFER_STATUS_PENDING,
|
||||
).all() if _ids else []
|
||||
_pending_map = {t.borrow_id: t.to_dict() for t in _pending}
|
||||
# ============================================================
|
||||
# ★ 实际归还人(trans_borrow_return.returner_id)
|
||||
#
|
||||
# 与主表的 return_operator(**经手库管**)是**两个不同的人**:
|
||||
# · returner_id —— 把东西交回窗口的人(已校验 == 当时持有人)
|
||||
# · return_operator —— 办理还库的库管
|
||||
# 列表原先把后者标成「归还人」展示,属标签错误;此处补上真正
|
||||
# 的归还人,供前端分列展示。
|
||||
# ⚠ 本表是二期才建的,**历史归还没有这个记录** —— 那部分行的
|
||||
# returners 为空,前端显示为空并提示「历史数据未记录」,
|
||||
# 而不是拿库管的名字顶上(那正是本次要修的错)。
|
||||
# ============================================================
|
||||
_ret_rows = TransBorrowReturn.query.filter(
|
||||
TransBorrowReturn.borrow_id.in_(_ids)
|
||||
).all() if _ids else []
|
||||
_ret_uids = {t.returner_id for t in _ret_rows if t.returner_id}
|
||||
_ret_names = {}
|
||||
if _ret_uids:
|
||||
from app.models.system import SysUser
|
||||
for _u in SysUser.query.filter(SysUser.id.in_(_ret_uids)).all():
|
||||
_ret_names[_u.id] = user_display_name(_u)
|
||||
_ret_map = {}
|
||||
for _t in _ret_rows:
|
||||
_nm = _ret_names.get(_t.returner_id)
|
||||
if _nm:
|
||||
_ret_map.setdefault(_t.borrow_id, set()).add(_nm)
|
||||
for d in items_with_names:
|
||||
d['returners'] = sorted(_ret_map.get(d.get('id'), set()))
|
||||
|
||||
for d in items_with_names:
|
||||
_pt = _pending_map.get(d.get('id'))
|
||||
if _pt is not None:
|
||||
|
||||
@ -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.83
|
||||
当前版本:V3.84
|
||||
</span>
|
||||
</footer>
|
||||
|
||||
|
||||
@ -200,6 +200,7 @@
|
||||
<el-checkbox v-if="hasColPermission('files')" v-model="columns.files.visible" label="资料" />
|
||||
<el-checkbox v-if="hasColPermission('isEnabled')" v-model="columns.isEnabled.visible" label="状态" />
|
||||
<el-checkbox v-if="hasColPermission('isInspectionRequired')" v-model="columns.isInspectionRequired.visible" label="强制质检" />
|
||||
<el-checkbox v-if="hasColPermission('isApprovalRequired')" v-model="columns.isApprovalRequired.visible" label="出库/借库需审批" />
|
||||
<el-checkbox v-if="hasColPermission('referencePrice')" v-model="columns.referencePrice.visible" label="参考价格" />
|
||||
<el-checkbox v-if="hasColPermission('purchaseLink')" v-model="columns.purchaseLink.visible" label="采购链接" />
|
||||
<el-checkbox v-if="hasColPermission('warningStatus')" v-model="columns.warningStatus.visible" label="预警状态" />
|
||||
@ -350,6 +351,15 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="columns.isApprovalRequired.visible && hasColPermission('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 && hasColPermission('referencePrice')" 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>
|
||||
@ -434,10 +444,19 @@
|
||||
>
|
||||
<el-icon style="margin-right: 4px"><Plus /></el-icon>加入或查看BOM
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="form.id"
|
||||
type="warning"
|
||||
:underline="false"
|
||||
style="font-size: 14px;"
|
||||
@click="createPurchaseForMaterial"
|
||||
>
|
||||
<el-icon style="margin-right: 4px"><ShoppingCart /></el-icon>发起采购申请
|
||||
</el-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px" :disabled="formDisabled">
|
||||
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
@ -655,7 +674,7 @@
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="cancel" :disabled="isUploading">取 消</el-button>
|
||||
<el-button type="primary" @click="submitForm" :loading="submitLoading || isUploading">确 定</el-button>
|
||||
<el-button type="primary" @click="submitForm" :loading="submitLoading || isUploading" :disabled="formDisabled">确 定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@ -748,7 +767,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, nextTick, computed, watch } from 'vue';
|
||||
import { Plus, Document, DocumentCopy, Refresh, Setting, Rank, Camera, Download, Bell, CircleCheck, Files, ZoomIn, Delete, Picture, FolderOpened, Loading, Upload } from '@element-plus/icons-vue';
|
||||
import { Plus, Document, DocumentCopy, Refresh, Setting, Rank, Camera, Download, Bell, CircleCheck, Files, ZoomIn, Delete, Picture, FolderOpened, Loading, Upload, ShoppingCart } from '@element-plus/icons-vue';
|
||||
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus';
|
||||
import type { FormInstance, FormRules } from 'element-plus';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
@ -765,6 +784,7 @@ import {
|
||||
exportAssetStatistics,
|
||||
batchSetWarning,
|
||||
batchSetInspection,
|
||||
batchSetApprovalRequired,
|
||||
markWarningOrdered,
|
||||
getMaterialUnitsAPI,
|
||||
getOdooSummary
|
||||
@ -789,6 +809,16 @@ const canEditRemark = computed(() => {
|
||||
return userStore.hasPermission('material_list:remark_edit');
|
||||
});
|
||||
|
||||
// 表单全局禁用:没有 material_list:operation 权限时,所有表单字段不可编辑
|
||||
// ★ 为什么必须有:列表页的「编辑/新增」按钮虽然有权限守卫,但 URL 深链
|
||||
// ?edit_id= 不受守卫,可直接打开本弹窗。若此处不禁用,用户能改完并点确定,
|
||||
// 最终被后端 @permission_required('material_list:operation') 拦下 —— 白填一遍。
|
||||
// (与 list.vue 对齐)
|
||||
const formDisabled = computed(() => {
|
||||
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') return false;
|
||||
return !userStore.hasPermission('material_list:operation');
|
||||
});
|
||||
|
||||
// --- 类型定义 ---
|
||||
interface MaterialBaseVO {
|
||||
id: number;
|
||||
@ -1039,7 +1069,8 @@ const columns = reactive({
|
||||
commonName: { visible: true }, category: { visible: true }, type: { visible: true },
|
||||
spec: { visible: true }, unit: { visible: true }, inventory: { visible: true },
|
||||
available: { visible: true }, files: { visible: true }, isEnabled: { visible: true },
|
||||
isInspectionRequired: { visible: true }, referencePrice: { visible: true }, warningStatus: { visible: true },
|
||||
isInspectionRequired: { visible: true }, isApprovalRequired: { visible: true },
|
||||
referencePrice: { visible: true }, warningStatus: { visible: true },
|
||||
purchaseLink: { visible: true }
|
||||
});
|
||||
|
||||
@ -1050,7 +1081,9 @@ const permissionMap: Record<string, string> = {
|
||||
available: 'material_list:availableCount', files: 'material_list:files', isEnabled: 'material_list:isEnabled',
|
||||
// ★ 与后端读过滤(field_permissions.py)对齐;原先写 material_list:operation
|
||||
// 会与后端口径不一致,渲染出一列全是「-」的空表头
|
||||
isInspectionRequired: 'material_list:isInspectionRequired', referencePrice: 'material_list:referencePrice',
|
||||
isInspectionRequired: 'material_list:isInspectionRequired',
|
||||
isApprovalRequired: 'material_list:isApprovalRequired',
|
||||
referencePrice: 'material_list:referencePrice',
|
||||
purchaseLink: 'material_list:purchaseLink',
|
||||
warningStatus: 'material_list:view_warning'
|
||||
};
|
||||
@ -1413,7 +1446,10 @@ const isArraysEqual = (a: any[], b: any[]): boolean => {
|
||||
|
||||
const buildPartialPayload = (current: any, original: any): any => {
|
||||
const payload: any = { id: current.id };
|
||||
const compareFields = ['name', 'commonName', 'category', 'type', 'spec', 'unit', 'visibilityLevel', 'isEnabled', 'isInspectionRequired', 'generalImage', 'generalManual', 'companyName', 'productImageRemark', 'manualLinkRemark', 'purchaseLink'];
|
||||
// ★ referencePrice 必须在内:漏掉它会走成「未变更」——只改参考价格时提示
|
||||
// 「没有检测到数据变更,无需保存」,连带改其他字段时提示「修改成功」
|
||||
// 但 payload 里压根没有该字段,价格静默丢失。(与 list.vue 对齐)
|
||||
const compareFields = ['name', 'commonName', 'category', 'type', 'spec', 'unit', 'referencePrice', 'visibilityLevel', 'isEnabled', 'isInspectionRequired', 'generalImage', 'generalManual', 'companyName', 'productImageRemark', 'manualLinkRemark', 'purchaseLink'];
|
||||
for (const key of compareFields) {
|
||||
const currentVal = current[key]; const originalVal = original[key];
|
||||
if (Array.isArray(currentVal) && Array.isArray(originalVal)) { if (!isArraysEqual(currentVal, originalVal)) payload[key] = currentVal; }
|
||||
@ -1470,6 +1506,35 @@ const createBomForMaterial = () => {
|
||||
window.open(routeUrl.href, '_blank');
|
||||
};
|
||||
|
||||
// 编辑弹窗里的「发起采购申请」:带物料信息跳采购管理并自动预填(与 list.vue 对齐)
|
||||
const createPurchaseForMaterial = () => {
|
||||
if (!form.value.id) return ElMessage.warning('请先保存物料基础信息后再操作');
|
||||
const routeUrl = router.resolve({
|
||||
path: '/purchase',
|
||||
query: {
|
||||
material_id: String(form.value.id),
|
||||
name: form.value.name,
|
||||
spec: form.value.spec,
|
||||
unit: form.value.unit
|
||||
}
|
||||
});
|
||||
window.open(routeUrl.href, '_blank');
|
||||
};
|
||||
|
||||
// 列表内直接切换「出库/借库需审批」(与 list.vue 对齐)
|
||||
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 resetForm = () => {
|
||||
form.value = JSON.parse(JSON.stringify(initForm));
|
||||
fileListImage.value = []; fileListManual.value = [];
|
||||
|
||||
@ -209,9 +209,29 @@
|
||||
<el-tag type="info">{{ row.children ? row.children.length : 0 }} 项</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- ★ 归还人改为整单汇总:主行原取「首条明细」的归还人,多明细分批归还时
|
||||
会只显示其中一位,令人误以为其他人没还过。 -->
|
||||
<el-table-column v-if="hasColumnPermission('return_operator')" label="归还人" min-width="90">
|
||||
<!-- ★ 「归还人」与「经手库管」是**两个不同的人**,必须分列:
|
||||
returners ← trans_borrow_return.returner_id(把东西交回窗口的人)
|
||||
return_operators ← trans_borrow.return_operator(办理还库的库管)
|
||||
原先只有一列,标着「归还人」读的却是 return_operator —— 展示成了库管。
|
||||
⚠ 实际归还人流水是二期才建的,**历史归还查不到**:此时列显示为空并给出
|
||||
提示,而不是拿库管的名字顶上(那正是本次要修的错)。 -->
|
||||
<el-table-column label="归还人" min-width="90">
|
||||
<template #default="{row}">
|
||||
<template v-if="row.returners && row.returners.length">
|
||||
<span v-for="n in row.returners" :key="n" class="name-fixed">{{ formatName(n) }}</span>
|
||||
</template>
|
||||
<el-tooltip
|
||||
v-else-if="row.status === 'returned' || row.status === 'scrapped'"
|
||||
placement="top"
|
||||
content="该笔归还发生在「实际归还人」记录上线之前,系统当时只留了经手库管"
|
||||
>
|
||||
<span class="text-info">—</span>
|
||||
</el-tooltip>
|
||||
<span v-else class="text-info">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- 经手库管:办理还库的库管,与「归还人」不是同一人,故单列展示 -->
|
||||
<el-table-column v-if="hasColumnPermission('return_operator')" label="经手库管" min-width="90">
|
||||
<template #default="{row}">
|
||||
<template v-if="row.return_operators && row.return_operators.length">
|
||||
<span v-for="op in row.return_operators" :key="op" class="name-fixed">{{ formatName(op) }}</span>
|
||||
|
||||
Reference in New Issue
Block a user