Compare commits
9 Commits
aa3a470d67
...
4f8d765bb3
| Author | SHA1 | Date | |
|---|---|---|---|
| 4f8d765bb3 | |||
| 3cb9b0da98 | |||
| 87919146de | |||
| 129cb1119f | |||
| ec9aafbe51 | |||
| bf7b2cc6d3 | |||
| 85130db6ea | |||
| 0887b20302 | |||
| d8be963036 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -11,6 +11,8 @@ inventory-web/dist/
|
||||
inventory-web/*.local
|
||||
|
||||
# --- 通用忽略 ---
|
||||
# 根目录 .env(docker compose 读取,含 Track Webhook 密钥等敏感配置)
|
||||
.env
|
||||
.idea/
|
||||
.vscode/
|
||||
.DS_Store
|
||||
|
||||
@ -24,6 +24,8 @@ ssh $SERVER "mkdir -p $REMOTE_BACKUP_DIR && \
|
||||
--exclude='inventory-backend/uploads' \
|
||||
--exclude='inventory-backend/pgdata' \
|
||||
--exclude='inventory-backend/pgdata_docker' \
|
||||
--exclude='inventory-backend/venv' \
|
||||
--exclude='inventory-web/node_modules' \
|
||||
inventory-backend inventory-web docker-compose.prod.yml && \
|
||||
echo '>> 备份 SSL 证书...' && \
|
||||
(tar -czf $REMOTE_BACKUP_DIR/ssl_backup.tar.gz ssl 2>/dev/null || echo '⚠️ ssl 目录不可读(属主 root)或不存在,跳过该备份') && \
|
||||
|
||||
@ -38,11 +38,12 @@ services:
|
||||
MAIL_USE_SSL: "true"
|
||||
MAIL_USE_TLS: "false"
|
||||
# Track 系统联动(扫码入库/出库/Webhook 通知)
|
||||
# ★ 生产用容器名 track_backend_prod 访问(MOM 与 Track 同处 mom_net 外部网络)
|
||||
TRACK_WEBHOOK_URL: ${TRACK_WEBHOOK_URL:-http://track_backend_prod:8000/api/v1/external/webhooks/mom-inbound}
|
||||
# ★ 生产地址硬编码为容器名 track_backend_prod(同一服务器 mom_net 网络内互通),
|
||||
# 避免被服务器上残留的 .env 覆盖成 dev 的 track_backend
|
||||
TRACK_WEBHOOK_URL: http://track_backend_prod:8000/api/v1/external/webhooks/mom-inbound
|
||||
TRACK_WEBHOOK_KEY: ${TRACK_WEBHOOK_KEY:-2ce5fedb48fde3fd7e0abf67472a5027b03e9ae6f19cf768}
|
||||
TRACK_API_URL: ${TRACK_API_URL:-http://track_backend_prod:8000}
|
||||
TRACK_OUTBOUND_WEBHOOK_URL: ${TRACK_OUTBOUND_WEBHOOK_URL:-http://track_backend_prod:8000/api/v1/external/webhooks/mom-outbound}
|
||||
TRACK_API_URL: http://track_backend_prod:8000
|
||||
TRACK_OUTBOUND_WEBHOOK_URL: http://track_backend_prod:8000/api/v1/external/webhooks/mom-outbound
|
||||
depends_on:
|
||||
- db
|
||||
# 加入 mom_net 外部网络,与 Track 生产容器互通
|
||||
|
||||
@ -10,3 +10,23 @@ uploads
|
||||
pgdata
|
||||
.env
|
||||
simhei.ttf
|
||||
|
||||
# ---- 补充排除项 ----
|
||||
# Windows 残留空文件
|
||||
nul
|
||||
# 数据库备份文件(避免打进镜像/context)
|
||||
*.dump
|
||||
*.sql.gz
|
||||
*.tar.gz
|
||||
*.gz
|
||||
*.log
|
||||
# 数据库迁移脚本(运行时用 volume 挂载宿主机,镜像内不需要)
|
||||
db_migrations
|
||||
# 测试脚本与缓存
|
||||
test_*.py
|
||||
tests
|
||||
.pytest_cache
|
||||
.coverage
|
||||
# 运维修复脚本(宿主机保留,无需进镜像)
|
||||
fix_*.py
|
||||
fix_*.sql
|
||||
|
||||
@ -5,9 +5,12 @@ WORKDIR /app
|
||||
|
||||
# 1. 复制依赖并安装
|
||||
COPY requirements.txt .
|
||||
# 安装依赖 + gunicorn
|
||||
RUN pip install --no-cache-dir -r requirements.txt && \
|
||||
pip install --no-cache-dir gunicorn
|
||||
# 安装依赖 + gunicorn(清华镜像源 + BuildKit 缓存挂载)
|
||||
# ★ 关键优化:pip 下载的 wheel 会缓存在宿主 /root/.cache/pip,
|
||||
# 只要 requirements.txt 不变,重复构建不再重新下载(首次约100s,之后秒级)
|
||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
||||
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt && \
|
||||
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple gunicorn
|
||||
|
||||
# 2. 复制后端代码
|
||||
COPY . .
|
||||
|
||||
@ -198,9 +198,10 @@ def save_bom():
|
||||
@jwt_required()
|
||||
@permission_required('bom_manage')
|
||||
def get_bom_with_stock_by_no(bom_no):
|
||||
"""根据 BOM 编号获取配方详情及库存信息"""
|
||||
"""根据 BOM 编号 (和可选 version) 获取配方详情及库存信息"""
|
||||
try:
|
||||
data = BomService.get_bom_with_stock_by_bom_no(bom_no)
|
||||
version = request.args.get('version')
|
||||
data = BomService.get_bom_with_stock_by_bom_no(bom_no, version=version)
|
||||
if not data:
|
||||
return jsonify({'code': 404, 'msg': 'BOM 不存在'}), 404
|
||||
# 字段级脱敏
|
||||
|
||||
@ -422,11 +422,11 @@ class BomService:
|
||||
return bom_no
|
||||
|
||||
@staticmethod
|
||||
def get_bom_with_stock_by_bom_no(bom_no):
|
||||
def get_bom_with_stock_by_bom_no(bom_no, version=None):
|
||||
"""
|
||||
根据 bom_no 获取配方详情,并计算(已修复 N+1 性能问题)
|
||||
根据 bom_no (和可选 version) 获取配方详情,并计算(已修复 N+1 性能问题)
|
||||
"""
|
||||
detail = BomService.get_bom_detail(bom_no)
|
||||
detail = BomService.get_bom_detail(bom_no, version=version)
|
||||
if not detail or not detail.get('children'):
|
||||
return detail
|
||||
|
||||
|
||||
@ -33,4 +33,6 @@ pytz
|
||||
# [新增] 进度条库 (脚本和任务所需)
|
||||
tqdm>=4.66.0
|
||||
# [新增] pgvector 向量数据库支持(以图搜图 / 实时向量提取)
|
||||
pgvector>=0.2.0
|
||||
pgvector>=0.2.0
|
||||
# [新增] HTTP 客户端(Track 系统 Webhook 异步通知)
|
||||
httpx>=0.24.0
|
||||
@ -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.69
|
||||
当前版本:V3.70
|
||||
</span>
|
||||
</footer>
|
||||
|
||||
|
||||
@ -19,12 +19,13 @@ export function getBomSummary(params?: { keyword?: string }) {
|
||||
}
|
||||
|
||||
// 获取BOM详情(含库存信息)
|
||||
export function getBomWithStock(bomNo: string) {
|
||||
export function getBomWithStock(bomNo: string, version?: string) {
|
||||
const trimmed = bomNo.replace(/^\/+|\/+$/g, '');
|
||||
const encoded = encodeURIComponent(trimmed);
|
||||
return request({
|
||||
url: `/v1/bom/stock/${encoded}`,
|
||||
method: 'get'
|
||||
method: 'get',
|
||||
params: version ? { version } : {}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -247,6 +247,7 @@
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="bomSelectVisible = false">取消</el-button>
|
||||
<el-button :icon="Printer" :disabled="bomDetailList.length === 0" @click="printBomChecklist" title="先选择 BOM 配方后即可打印清单">打印 BOM 清单</el-button>
|
||||
<el-button
|
||||
v-if="userStore.hasPermission('op_borrow_apply:operation')"
|
||||
type="primary"
|
||||
@ -257,6 +258,62 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- ★ BOM 打印区域(隐藏,打印时克隆到 iframe) -->
|
||||
<div id="bom-print-area" class="print-bom-area" style="display:none;">
|
||||
<h2 style="text-align:center; margin:0 0 8px;">BOM 借库清单</h2>
|
||||
<p style="text-align:center; margin:0 0 16px; font-size:13px;">
|
||||
BOM: <b>{{ selectedBomLabel }}</b> | 套数: <b>{{ bomSets }}</b> 套 | 生成时间: {{ bomPrintTime }}
|
||||
</p>
|
||||
<table class="print-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>序号</th><th>物料名称</th><th>规格型号</th><th>需求量</th><th>可用库存</th><th>缺料</th><th>结果</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, idx) in bomDetailList" :key="idx">
|
||||
<td>{{ idx + 1 }}</td>
|
||||
<td>{{ row.name }}</td>
|
||||
<td>{{ row.sku }}</td>
|
||||
<td>{{ row.need }}</td>
|
||||
<td>{{ row.available }}</td>
|
||||
<td>{{ row.shortage > 0 ? row.shortage : '-' }}</td>
|
||||
<td>{{ row.shortage > 0 ? '缺货未加' : '可加' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p style="margin-top:12px;">
|
||||
可加 <b style="color:#67C23A;">{{ bomAvailableCount }}</b> 种 |
|
||||
缺货 <b style="color:#F56C6C;">{{ bomShortageCount }}</b> 种
|
||||
</p>
|
||||
<p style="color:#909399; font-size:12px;">缺货物料不会加入本次借库申请单,请补货后重新按 BOM 添加。</p>
|
||||
|
||||
<!-- ★ 欠料待补签收区(仅当有缺货时打印此联,与出库选单一致) -->
|
||||
<div v-if="bomShortageCount > 0" style="margin-top: 20px; border-top: 2px dashed #000; padding-top: 10px;">
|
||||
<p style="text-align:center; margin:0 0 8px; font-size:13px;">
|
||||
BOM: <b>{{ selectedBomLabel }}</b> | 套数: <b>{{ bomSets }}</b> 套 | 生成时间: {{ bomPrintTime }}
|
||||
</p>
|
||||
<p style="font-weight: bold; font-size: 14px;">⚠️ 欠料待补清单(请补料后在此签字)</p>
|
||||
<table class="print-table" style="margin-top: 6px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>序号</th><th>物料名称</th><th>规格型号</th><th>需补数量</th><th>补领人签字</th><th>日期</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, idx) in bomDetailList.filter(i => i.shortage > 0)" :key="idx">
|
||||
<td>{{ idx + 1 }}</td>
|
||||
<td>{{ row.name }}</td>
|
||||
<td>{{ row.sku }}</td>
|
||||
<td>{{ row.shortage }}</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
v-model="previewVisible"
|
||||
title="借库单核对与打印"
|
||||
@ -347,6 +404,19 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="预计归还日期">
|
||||
<el-date-picker
|
||||
v-model="requestExpectedReturn"
|
||||
type="date"
|
||||
placeholder="请选择日期"
|
||||
style="width: 100%"
|
||||
value-format="YYYY-MM-DD"
|
||||
:disabled="requestIndefinite"
|
||||
/>
|
||||
<el-checkbox v-model="requestIndefinite" @change="handleRequestIndefiniteChange" style="margin-left: 12px;">
|
||||
长期借用
|
||||
</el-checkbox>
|
||||
</el-form-item>
|
||||
<el-form-item label="申请原因">
|
||||
<el-input
|
||||
v-model="requestRemark"
|
||||
@ -473,6 +543,8 @@ const printLoading = ref(false)
|
||||
const requestDialogVisible = ref(false)
|
||||
const borrowerName = ref('')
|
||||
const requestRemark = ref('')
|
||||
const requestExpectedReturn = ref('')
|
||||
const requestIndefinite = ref(false)
|
||||
const requestApproverId = ref<number | null>(null)
|
||||
const approvers = ref<any[]>([])
|
||||
const requestSubmitting = ref(false)
|
||||
@ -495,6 +567,7 @@ const bomOptions = ref<any[]>([])
|
||||
const selectedBomNo = ref('')
|
||||
const bomSets = ref(1)
|
||||
const currentBomDetail = ref<any[]>([]) // 当前选中的BOM明细
|
||||
const bomPrintTime = ref('')
|
||||
|
||||
// BOM 树形数据(将分组数据映射为 el-tree-select 需要的结构)
|
||||
const treeData = computed(() => {
|
||||
@ -503,7 +576,8 @@ const treeData = computed(() => {
|
||||
label: `${group.category} (${group.count})`,
|
||||
disabled: true, // 禁止选中分类本身
|
||||
children: (group.items || []).map((b: any) => ({
|
||||
value: b.bom_no,
|
||||
// ★ 用 bom_no + version 组合作为唯一 value,避免同一产品多版本时回显错乱
|
||||
value: `${b.bom_no}###${b.version}`,
|
||||
label: `${b.bom_no} - ${b.parent_name} - ${b.version}`
|
||||
}))
|
||||
}))
|
||||
@ -546,6 +620,14 @@ const bomDetailList = computed(() => {
|
||||
})
|
||||
|
||||
const hasShortage = computed(() => bomDetailList.value.some((item: any) => item.shortage > 0) && bomSets.value > maxBuildableSets.value)
|
||||
const bomAvailableCount = computed(() => bomDetailList.value.filter(i => i.shortage === 0).length)
|
||||
const bomShortageCount = computed(() => bomDetailList.value.filter(i => i.shortage > 0).length)
|
||||
|
||||
// BOM 选择显示标签(bom_no###version → "bom_no (version)")
|
||||
const selectedBomLabel = computed(() => {
|
||||
const [no, ver] = (selectedBomNo.value || '').split('###')
|
||||
return ver ? `${no} (${ver})` : no
|
||||
})
|
||||
|
||||
// 打印相关
|
||||
const currentTime = ref('')
|
||||
@ -715,13 +797,15 @@ const openBomSelect = async () => {
|
||||
}
|
||||
|
||||
// 监听 BOM 选择变化,自动加载明细(含库存)并计算齐套性
|
||||
watch(selectedBomNo, async (newBomNo) => {
|
||||
if (!newBomNo) {
|
||||
watch(selectedBomNo, async (newKey) => {
|
||||
if (!newKey) {
|
||||
currentBomDetail.value = []
|
||||
return
|
||||
}
|
||||
// ★ 组合 key 拆解为 bom_no + version,按选中版本精确拉取明细
|
||||
const [bomNo, version] = newKey.split('###')
|
||||
try {
|
||||
const detailRes: any = await getBomWithStock(newBomNo)
|
||||
const detailRes: any = await getBomWithStock(bomNo, version)
|
||||
currentBomDetail.value = detailRes.data?.children || []
|
||||
} catch (e) {
|
||||
ElMessage.error('加载 BOM 明细失败')
|
||||
@ -734,7 +818,9 @@ const confirmBomAdd = async () => {
|
||||
|
||||
if (currentBomDetail.value.length === 0) {
|
||||
try {
|
||||
const detailRes: any = await getBomWithStock(selectedBomNo.value)
|
||||
// ★ 组合 key 拆解为 bom_no + version,按选中版本精确拉取明细
|
||||
const [bomNo, version] = (selectedBomNo.value || '').split('###')
|
||||
const detailRes: any = await getBomWithStock(bomNo, version)
|
||||
currentBomDetail.value = detailRes.data?.children || []
|
||||
} catch (e) {
|
||||
ElMessage.error('获取 BOM 详情失败')
|
||||
@ -792,6 +878,51 @@ const confirmBomAdd = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// ★ BOM 清单打印(复用与 confirmPrint 相同的 iframe 打印机制,与出库选单/借库选单一致)
|
||||
const printBomChecklist = () => {
|
||||
if (bomDetailList.value.length === 0) return ElMessage.warning('请先选择 BOM 并加载清单')
|
||||
bomPrintTime.value = new Date().toLocaleString('zh-CN')
|
||||
|
||||
setTimeout(() => {
|
||||
const printElement = document.getElementById('bom-print-area')
|
||||
if (!printElement) return
|
||||
|
||||
const iframe = document.createElement('iframe')
|
||||
iframe.style.position = 'fixed'
|
||||
iframe.style.right = '0'
|
||||
iframe.style.bottom = '0'
|
||||
iframe.style.width = '0'
|
||||
iframe.style.height = '0'
|
||||
iframe.style.border = '0'
|
||||
document.body.appendChild(iframe)
|
||||
|
||||
const iframeDoc = iframe.contentWindow?.document
|
||||
if (!iframeDoc) return
|
||||
iframeDoc.open()
|
||||
iframeDoc.write('<html><head><title>BOM 借库清单</title></head><body></body></html>')
|
||||
iframeDoc.close()
|
||||
|
||||
const style = `
|
||||
body { font-family: 'Microsoft YaHei', sans-serif; }
|
||||
.print-bom-area { display: block !important; padding: 20px; }
|
||||
.print-table { width: 100%; border-collapse: collapse; }
|
||||
.print-table th, .print-table td { border: 1px solid #000; padding: 6px 8px; font-size: 13px; text-align: center; }
|
||||
.print-table th { background: #f0f0f0; }
|
||||
`;
|
||||
const styleEl = iframeDoc.createElement('style')
|
||||
styleEl.textContent = style
|
||||
iframeDoc.head.appendChild(styleEl)
|
||||
|
||||
iframeDoc.body.appendChild(printElement.cloneNode(true))
|
||||
|
||||
setTimeout(() => {
|
||||
iframe.contentWindow?.focus()
|
||||
iframe.contentWindow?.print()
|
||||
setTimeout(() => document.body.removeChild(iframe), 1000)
|
||||
}, 500)
|
||||
}, 300)
|
||||
}
|
||||
|
||||
// 主界面数量变更逻辑
|
||||
const handleMainQuantityChange = (val: number | undefined, row: any) => {
|
||||
if (val === 0) {
|
||||
@ -900,6 +1031,8 @@ const openRequestDialog = () => {
|
||||
borrowerName.value = userStore.user?.username || ''
|
||||
requestRemark.value = ''
|
||||
requestApproverId.value = null
|
||||
requestExpectedReturn.value = ''
|
||||
requestIndefinite.value = false
|
||||
loadApprovers()
|
||||
requestDialogVisible.value = true
|
||||
}
|
||||
@ -915,7 +1048,27 @@ const loadApprovers = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// ★ 长期借用勾选:勾选时清空已选的归还日期
|
||||
const handleRequestIndefiniteChange = (val: boolean) => {
|
||||
if (val) requestExpectedReturn.value = ''
|
||||
}
|
||||
|
||||
// ★ 确认提交借库申请
|
||||
// ★ 将 BOM 缺货清单拼接进申请原因(与出库选单一致)
|
||||
const buildRemarkWithShortage = (baseRemark: string): string => {
|
||||
const parts: string[] = []
|
||||
if (baseRemark) parts.push(baseRemark)
|
||||
|
||||
// ★ 用当前实时计算的缺货清单(需量 - 当前库存),而非 localStorage 历史记录
|
||||
const shortageItems = bomDetailList.value.filter((i: any) => i.shortage > 0)
|
||||
if (shortageItems.length > 0) {
|
||||
const items = shortageItems.map((i: any) => `${i.name}(${i.shortage}个)`).join('、')
|
||||
parts.push(`[BOM欠料待补] ${items}`)
|
||||
}
|
||||
|
||||
return parts.join(' | ')
|
||||
}
|
||||
|
||||
const confirmSubmitRequest = async () => {
|
||||
requestSubmitting.value = true
|
||||
try {
|
||||
@ -924,9 +1077,11 @@ const confirmSubmitRequest = async () => {
|
||||
name: item.name || '',
|
||||
spec_model: item.standard || '',
|
||||
warehouse_location: item.warehouse_location || '',
|
||||
quantity: item.export_quantity || 0
|
||||
quantity: item.export_quantity || 0,
|
||||
expected_return_time: requestIndefinite.value ? null : requestExpectedReturn.value,
|
||||
is_indefinite: requestIndefinite.value
|
||||
})),
|
||||
remark: requestRemark.value.trim(),
|
||||
remark: buildRemarkWithShortage(requestRemark.value.trim()),
|
||||
approver_id: requestApproverId.value
|
||||
}
|
||||
|
||||
|
||||
@ -76,6 +76,15 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="归还期限" width="130" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="getReturnDeadline(row)" :style="row?.items?.[0]?.is_indefinite ? 'color:#E6A23C;font-weight:bold;' : ''">
|
||||
{{ getReturnDeadline(row) }}
|
||||
</span>
|
||||
<span v-else style="color: #909399;">未填写</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="created_at" label="申请时间" width="170" />
|
||||
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
@ -219,6 +228,14 @@ const getApplicantName = (id: number | null) => {
|
||||
return userNameCache.value[id] ?? `用户 #${id}`
|
||||
}
|
||||
|
||||
// 从申请单明细快照中读取归还期限(旧申请单无此字段时返回空串)
|
||||
const getReturnDeadline = (row: any) => {
|
||||
const first = row?.items?.[0]
|
||||
if (first?.is_indefinite) return '长期借用'
|
||||
if (first?.expected_return_time) return first.expected_return_time
|
||||
return ''
|
||||
}
|
||||
|
||||
const getApproverName = (id: number | null) => {
|
||||
if (!id) return '-'
|
||||
return userNameCache.value[id] ?? `用户 #${id}`
|
||||
|
||||
@ -1473,7 +1473,7 @@ 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'];
|
||||
const compareFields = ['name', 'commonName', 'category', 'type', 'spec', 'unit', 'referencePrice', 'visibilityLevel', 'isEnabled', 'isInspectionRequired', 'generalImage', 'generalManual', 'companyName', 'productImageRemark', 'manualLinkRemark'];
|
||||
|
||||
for (const key of compareFields) {
|
||||
const currentVal = current[key];
|
||||
|
||||
@ -268,7 +268,7 @@
|
||||
<div id="bom-print-area" class="print-bom-area" style="display:none;">
|
||||
<h2 style="text-align:center; margin:0 0 8px;">BOM 出库清单</h2>
|
||||
<p style="text-align:center; margin:0 0 16px; font-size:13px;">
|
||||
BOM: <b>{{ selectedBomNo }}</b> | 套数: <b>{{ bomSets }}</b> 套 | 生成时间: {{ bomPrintTime }}
|
||||
BOM: <b>{{ selectedBomLabel }}</b> | 套数: <b>{{ bomSets }}</b> 套 | 生成时间: {{ bomPrintTime }}
|
||||
</p>
|
||||
<table class="print-table">
|
||||
<thead>
|
||||
@ -296,6 +296,9 @@
|
||||
|
||||
<!-- ★ 欠料待补签收区(仅当有缺货时打印此联) -->
|
||||
<div v-if="bomShortageCount > 0" style="margin-top: 20px; border-top: 2px dashed #000; padding-top: 10px;">
|
||||
<p style="text-align:center; margin:0 0 8px; font-size:13px;">
|
||||
BOM: <b>{{ selectedBomLabel }}</b> | 套数: <b>{{ bomSets }}</b> 套 | 生成时间: {{ bomPrintTime }}
|
||||
</p>
|
||||
<p style="font-weight: bold; font-size: 14px;">⚠️ 欠料待补清单(请补料后在此签字)</p>
|
||||
<table class="print-table" style="margin-top: 6px;">
|
||||
<thead>
|
||||
@ -560,7 +563,8 @@ const treeData = computed(() => {
|
||||
label: `${group.category} (${group.count})`,
|
||||
disabled: true, // 禁止选中分类本身
|
||||
children: (group.items || []).map((b: any) => ({
|
||||
value: b.bom_no,
|
||||
// ★ 用 bom_no + version 组合作为唯一 value,避免同一产品多版本时回显错乱
|
||||
value: `${b.bom_no}###${b.version}`,
|
||||
label: `${b.bom_no} - ${b.parent_name} - ${b.version}`
|
||||
}))
|
||||
}))
|
||||
@ -635,6 +639,12 @@ const bomPrintTime = ref('')
|
||||
const bomAvailableCount = computed(() => bomDetailList.value.filter(i => i.shortage === 0).length)
|
||||
const bomShortageCount = computed(() => bomDetailList.value.filter(i => i.shortage > 0).length)
|
||||
|
||||
// BOM 选择显示标签(bom_no###version → "bom_no (version)")
|
||||
const selectedBomLabel = computed(() => {
|
||||
const [no, ver] = (selectedBomNo.value || '').split('###')
|
||||
return ver ? `${no} (${ver})` : no
|
||||
})
|
||||
|
||||
// --- 辅助方法 ---
|
||||
const getTypeTag = (type: string) => {
|
||||
switch (type) {
|
||||
@ -808,35 +818,21 @@ const openBomSelect = async () => {
|
||||
category, count: items.length, items
|
||||
}))
|
||||
|
||||
// ★ 提示有历史缺货记录的 BOM,防止下次重复出/漏出
|
||||
const pendingBoms = Object.keys(bomShortageRecords.value).filter(no =>
|
||||
flatItems.some(i => i.bom_no === no)
|
||||
)
|
||||
if (pendingBoms.length > 0) {
|
||||
ElMessageBox.confirm(
|
||||
`检测到以下 BOM 存在未完成的缺货记录(上次按 BOM 出库时缺货):\n\n` +
|
||||
pendingBoms.map(no => {
|
||||
const rec = bomShortageRecords.value[no]
|
||||
return `• ${no}(${rec.items.length} 种缺货,${rec.time})`
|
||||
}).join('\n') +
|
||||
`\n\n建议先补货后再出库,避免漏出。`,
|
||||
'存在未完成出库',
|
||||
{ confirmButtonText: '知道了', cancelButtonText: '忽略', type: 'warning' }
|
||||
).catch(() => {})
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error('加载 BOM 列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 监听 BOM 选择变化,自动加载明细(含库存)并计算齐套性
|
||||
watch(selectedBomNo, async (newBomNo) => {
|
||||
if (!newBomNo) {
|
||||
watch(selectedBomNo, async (newKey) => {
|
||||
if (!newKey) {
|
||||
currentBomDetail.value = []
|
||||
return
|
||||
}
|
||||
// ★ 组合 key 拆解为 bom_no + version,按选中版本精确拉取明细
|
||||
const [bomNo, version] = newKey.split('###')
|
||||
try {
|
||||
const detailRes: any = await getBomWithStock(newBomNo)
|
||||
const detailRes: any = await getBomWithStock(bomNo, version)
|
||||
currentBomDetail.value = detailRes.data?.children || []
|
||||
} catch (e) {
|
||||
ElMessage.error('加载 BOM 明细失败')
|
||||
@ -849,7 +845,9 @@ const confirmBomAdd = async () => {
|
||||
|
||||
if (currentBomDetail.value.length === 0) {
|
||||
try {
|
||||
const detailRes: any = await getBomWithStock(selectedBomNo.value)
|
||||
// ★ 组合 key 拆解为 bom_no + version,按选中版本精确拉取明细
|
||||
const [bomNo, version] = (selectedBomNo.value || '').split('###')
|
||||
const detailRes: any = await getBomWithStock(bomNo, version)
|
||||
currentBomDetail.value = detailRes.data?.children || []
|
||||
} catch (e) {
|
||||
ElMessage.error('获取 BOM 详情失败')
|
||||
|
||||
@ -416,11 +416,25 @@ const loadApprovalRequests = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// ★ 切换审批单时:清空购物车和签名,防止跨单据污染
|
||||
// ★ 切换审批单时:带出申请时填写的归还日期/长期借用,并清空购物车和签名,防止跨单据污染
|
||||
const handleApprovalChange = (val: number | null) => {
|
||||
if (!val) {
|
||||
selectedApprovalId.value = null
|
||||
}
|
||||
// 从申请单明细快照中带出"预计归还日期"(申请时通过 items_json 存储,无需改数据库)
|
||||
const req = approvalRequests.value.find(r => r.id === val)
|
||||
const firstItem = req?.items?.[0]
|
||||
if (firstItem?.is_indefinite) {
|
||||
isIndefinite.value = true
|
||||
form.expected_return_time = ''
|
||||
} else if (firstItem?.expected_return_time) {
|
||||
isIndefinite.value = false
|
||||
form.expected_return_time = firstItem.expected_return_time
|
||||
} else {
|
||||
// 旧申请单/未填时间:保持手动选择
|
||||
isIndefinite.value = false
|
||||
form.expected_return_time = ''
|
||||
}
|
||||
cartItems.value = []
|
||||
signatureFile.value = null
|
||||
signaturePreviewUrl.value = ''
|
||||
|
||||
Reference in New Issue
Block a user