diff --git a/src/api/supplyCollectionGoods.js b/src/api/supplyCollectionGoods.js index 5c17d30..e9997d7 100755 --- a/src/api/supplyCollectionGoods.js +++ b/src/api/supplyCollectionGoods.js @@ -5,6 +5,7 @@ import { loadGoodsInScenes, sceneValuesObjectFromRaw } from '@/utils/rfSceneGood import { pickGoodsCategoryEntry, pickGoodsDisplayName, + pickGoodsNameFromListedRow, resolveGoodsCategoryCode, } from '@/utils/goodsCategorys' @@ -571,7 +572,15 @@ function parseStableSupply(v) { /** @param {unknown} row */ export function normalizeCollectionGoodsRow(row) { if (!row || typeof row !== 'object') return null - const r = row + const embedded = row.raw && typeof row.raw === 'object' ? row.raw : null + const r = embedded + ? { + ...embedded, + ...row, + goods_categorys: row.goods_categorys ?? embedded.goods_categorys, + goods: row.goods ?? embedded.goods, + } + : row const nested = r.goods && typeof r.goods === 'object' ? r.goods : null const goods_code = String( r.goods_code @@ -600,7 +609,8 @@ export function normalizeCollectionGoodsRow(row) { ).trim() const name = pickGoodsDisplayName(r, category_code, { allowTopLevelName: true }) || String( - r.goods_name + r.name + || r.goods_name || r.goodsName || r.product_name || r.material_name @@ -671,15 +681,22 @@ export function pickCollectionGoodsPreferredTax(row) { export function pickCollectionGoodsPickFields(row) { const normalized = normalizeCollectionGoodsRow(row) || (row && typeof row === 'object' ? row : null) if (!normalized) return null - const raw = normalized.raw || row + const sourceRow = normalized.raw || row const goods_code = String(normalized.goods_code || '').trim() if (!goods_code) return null - const payRaw = raw?.payment_price ?? '' + const payRaw = sourceRow?.payment_price ?? row?.payment_price ?? '' const payment_price = payRaw !== '' && payRaw != null ? Number(payRaw) : null - const bill_tax = pickCollectionGoodsPreferredTax(row) + const bill_tax = pickCollectionGoodsPreferredTax(sourceRow) + const nameCandidates = [ + String(row?.name || '').trim(), + String(normalized.name || '').trim(), + pickGoodsNameFromListedRow(sourceRow), + pickGoodsNameFromListedRow(row), + ].filter(Boolean) + const name = nameCandidates.find((n) => n !== goods_code) || nameCandidates[0] || goods_code return { goods_code, - name: String(normalized.name || goods_code).trim(), + name, payment_price: Number.isFinite(payment_price) ? payment_price : null, bill_tax, raw: normalized, diff --git a/src/api/warehouseAlloc.js b/src/api/warehouseAlloc.js index 35749eb..366b481 100644 --- a/src/api/warehouseAlloc.js +++ b/src/api/warehouseAlloc.js @@ -186,6 +186,23 @@ export async function allocAttrListed(body = {}) { } } +/** POST /api/warehouse/alloc/attr/search — 按商品/RDC/FDC 检索配货属性编码 */ +export async function allocAttrSearch(body = {}) { + const payload = buildListedPayload({ + goods_code: str(body.goods_code) || undefined, + rdc_warehouse_code: str(body.rdc_warehouse_code) || undefined, + fdc_warehouse_code: str(body.fdc_warehouse_code) || undefined, + page: body.page, + limit: body.limit, + }) + const res = await request.post('/api/warehouse/alloc/attr/search', payload) + const { list, total } = unwrapListed(res) + return { + list: list.map(normalizeAllocAttrRow).filter(Boolean), + total, + } +} + /** POST /api/warehouse/alloc/logs/listed */ export async function allocLogsListed(body = {}) { const res = await request.post('/api/warehouse/alloc/logs/listed', buildListedPayload(body)) diff --git a/src/api/warehouseWaste.js b/src/api/warehouseWaste.js index 84ee437..b08c9a1 100644 --- a/src/api/warehouseWaste.js +++ b/src/api/warehouseWaste.js @@ -198,6 +198,83 @@ export async function wasteLogsAdd(body) { return res?.data ?? res } +function normalizeWasteShareRow(raw) { + if (!raw || typeof raw !== 'object') return null + const wh = pickRdcFdcPair(raw) + const goods = pickGoodsFields(raw) + const goods_code = goods.goods_code + if (!goods_code && !wh.rdc_warehouse_code) return null + const waste_count = num( + raw.waste_count + ?? raw.rdc_waste_count + ?? raw.rdc_waste + ?? raw.waste_num, + ) + const rdc_in_count = num( + raw.rdc_in_count + ?? raw.total_rdc_in + ?? raw.rdc_inbound_count + ?? raw.in_count, + ) + const share_total_amount = num( + raw.total_amount + ?? raw.share_total_amount + ?? raw.share_amount, + ) + return { + ...raw, + ...wh, + ...goods, + goods_display_name: pickGoodsNameFromListedRow(raw) || goods.goods_name, + waste_count, + rdc_in_count, + ratio: num(raw.ratio ?? raw.share_ratio ?? raw.waste_ratio), + pre_price: num(raw.pre_price ?? raw.price ?? raw.cost_price), + unit_amount: num(raw.unit_amount ?? raw.share_unit_amount ?? raw.unit_share_amount), + share_total_amount, + share_time: str(raw.share_time ?? raw.amortize_time ?? raw.ctime ?? raw.create_time), + day_id: raw.day_id != null && raw.day_id !== '' ? Number(raw.day_id) : undefined, + } +} + +/** POST /api/warehouse/waste/share/listed */ +export async function wasteShareListed(body = {}) { + const payload = buildListedPayload({ + goods_code: str(body.goods_code) || undefined, + rdc_warehouse_code: str(body.rdc_warehouse_code) || undefined, + day_id: body.day_id != null && body.day_id !== '' ? Number(body.day_id) : undefined, + page: body.page, + limit: body.limit, + }) + const res = await request.post('/api/warehouse/waste/share/listed', payload) + const { list, total } = unwrapListed(res) + return { + list: list.map(normalizeWasteShareRow).filter(Boolean), + total, + } +} + +/** POST /api/warehouse/waste/amortize */ +export async function wasteAmortize(body) { + const payload = { + start_time: str(body?.start_time), + end_time: str(body?.end_time), + } + if (!payload.start_time || !payload.end_time) throw new Error('请选择分摊时间段') + const res = await request.post('/api/warehouse/waste/amortize', payload, { skipErrorNotify: true }) + return res?.data ?? res +} + +export function isBenignWasteAmortizeError(err) { + const msg = pickApiErrorMessage(err, '') + return !msg || /没有|已分摊|无需分摊|无可分摊|暂无|为空/.test(msg) +} + export function pickApiErrorMessage(err, fallback = '操作失败') { - return String(err?.data?.message || err?.message || fallback).trim() || fallback + return String( + err?.data?.message + ?? err?.data?.data?.message + ?? err?.message + ?? fallback, + ).trim() || fallback } diff --git a/src/config/buildId.json b/src/config/buildId.json index 64c368a..30a9bb1 100755 --- a/src/config/buildId.json +++ b/src/config/buildId.json @@ -1,4 +1,4 @@ { - "buildId": "20260703007", - "generatedAt": "2026-07-03T09:28:11.726Z" + "buildId": "20260706001", + "generatedAt": "2026-07-06T04:43:39.914Z" } \ No newline at end of file diff --git a/src/data/appChangelog.js b/src/data/appChangelog.js index 4293cf1..8d0d1ec 100755 --- a/src/data/appChangelog.js +++ b/src/data/appChangelog.js @@ -47,9 +47,49 @@ export const APP_VERSION = '0.7.029' /** @type {ChangelogDay[]} */ export const APP_CHANGELOG = [ + { + date: '2026-07-06', + items: [ + { + icon: 'mdi:cart-outline', + kind: 'fix', + text: '「采购管理 → 新增采购订单」从统筹商品添加至明细后,物料列正确展示「名称(编码)」;修复二次归一化丢失 `goods_categorys` 物料名、明细仅显示编码的问题。', + }, + { + icon: 'mdi:package-variant-closed-minus', + kind: 'change', + text: '「仓库管理 → 损耗管理 → 新增记录」配货属性编码旁增加「搜索」按钮,弹窗内按物料编码、RDC/FDC 仓查询 `alloc/attr/search` 后点选;主表单展示编码及「物料 · RDC · FDC」说明。', + }, + ], + }, + { + date: '2026-07-05', + items: [ + { + icon: 'mdi:cog-outline', + kind: 'change', + text: '系统工程调整:变更说明按日归档,便于发版对照;仓库管理相关 API 封装与字段归一化逻辑补充,提升与后端联调时的数据一致性。', + }, + ], + }, + { + date: '2026-07-04', + items: [ + { + icon: 'mdi:tune-variant', + kind: 'change', + text: '系统交互与展示优化:仓库运维及分摊预估类页面统一「进页跑批 / 手动查询 / 重新跑批」职责划分;列表 loading 与空态提示对齐;数字列间距、除物料外仅展示名称等展示规则在各模块间进一步收敛。', + }, + ], + }, { date: '2026-07-03', items: [ + { + icon: 'mdi:package-variant-closed-minus', + kind: 'add', + text: '「仓库管理 → 损耗分摊预估」新增页面(`WasteShareListed.vue`,路由 `/warehouse/waste-share`):进页自动 `waste/amortize` 跑批后拉 `waste/share/listed`;筛选分摊时间段、物料编码、RDC 仓库编码;展示损耗数、RDC 入库、占比、定价成本、分摊总额、单件分摊(`is-cols-share-waste`,9 列)。', + }, { icon: 'mdi:help-circle-outline', kind: 'change', diff --git a/src/router/index.js b/src/router/index.js index 57f1711..ff8b65c 100755 --- a/src/router/index.js +++ b/src/router/index.js @@ -505,6 +505,16 @@ export const asyncRoutes = [ powerKey: 'warehouse', }, }, + { + path: 'waste-share', + name: 'WarehouseWasteShare', + component: () => import('@/views/base/WasteShareListed.vue'), + meta: { + title: '损耗分摊预估', + icon: 'mdi:package-variant-closed-minus', + powerKey: 'warehouse', + }, + }, ], }, /* diff --git a/src/styles/warehouseOpsFlexTable.css b/src/styles/warehouseOpsFlexTable.css index 0f53058..9c217bd 100644 --- a/src/styles/warehouseOpsFlexTable.css +++ b/src/styles/warehouseOpsFlexTable.css @@ -81,6 +81,10 @@ .wops-flex-table.is-cols-share-deploy { --wops-cols: 14fr 11fr 11fr 5fr 5fr 5fr 4fr 5fr 8fr; } +/* 损耗分摊预估(9 列) */ +.wops-flex-table.is-cols-share-waste { + --wops-cols: 14fr 11fr 5fr 5fr 4fr 5fr 5fr 5fr 8fr; +} /* 公关费用(10 列):费用编码略窄,集团 / 组织各加半份 */ .wops-flex-table.is-cols-pr-cost { --wops-cols: 7fr 10.5fr 10.5fr 7fr 8fr 8fr 6fr 9fr 6fr 8fr; @@ -191,6 +195,7 @@ .wops-flex-table.is-cols-share-pr .wops-flex-row, .wops-flex-table.is-cols-share-alloc .wops-flex-row, .wops-flex-table.is-cols-share-deploy .wops-flex-row, +.wops-flex-table.is-cols-share-waste .wops-flex-row, .wops-flex-table.is-cols-pr-cost .wops-flex-row, .wops-flex-table.is-cols-collect-whp .wops-flex-row, .wops-flex-table.is-cols-logs-whp .wops-flex-row, @@ -211,6 +216,8 @@ .wops-flex-table.is-cols-share-alloc .wops-flex-head .wops-col--num, .wops-flex-table.is-cols-share-deploy .wops-col--num, .wops-flex-table.is-cols-share-deploy .wops-flex-head .wops-col--num, +.wops-flex-table.is-cols-share-waste .wops-col--num, +.wops-flex-table.is-cols-share-waste .wops-flex-head .wops-col--num, .wops-flex-table.is-cols-pr-cost .wops-col--num, .wops-flex-table.is-cols-pr-cost .wops-flex-head .wops-col--num, .wops-flex-table.is-cols-collect-whp .wops-col--num, diff --git a/src/views/base/WasteShareListed.vue b/src/views/base/WasteShareListed.vue new file mode 100644 index 0000000..98d4b37 --- /dev/null +++ b/src/views/base/WasteShareListed.vue @@ -0,0 +1,419 @@ + + + + + diff --git a/src/views/base/WhPurchase.vue b/src/views/base/WhPurchase.vue index 6e5a8f0..ade89f2 100644 --- a/src/views/base/WhPurchase.vue +++ b/src/views/base/WhPurchase.vue @@ -481,8 +481,8 @@
{{ idx + 1 }}
- - {{ g._name ? g._name + '(' + g.goods_code + ')' : g.goods_code }} + + {{ formatOrderGoodsLabel(g) }}
@@ -631,6 +631,7 @@ import { expenseBillTabParts, normalizeFinanceBillType, } from '@/utils/financeBillTypes' +import { pickGoodsNameFromListedRow } from '@/utils/goodsCategorys' defineOptions({ name: 'WhPurchase' }) @@ -760,6 +761,13 @@ function fmtOrderAmount(v) { return Number.isFinite(n) ? n.toFixed(2) : '0.00' } +function formatOrderGoodsLabel(g) { + const code = String(g?.goods_code || '').trim() + const name = String(g?._name || g?.goods_name || '').trim() + if (name && name !== code) return `${name}(${code})` + return code || '—' +} + function resolveBillTaxRate(billCode) { const code = normBillCode(billCode) if (!code) return 0 @@ -1181,13 +1189,18 @@ async function onOrderPurchaseOrgChange() { await refreshOrderCollectionOptions() } -function onOrderCatalogAdd({ goods_code, name, payment_price, bill_tax }) { +function onOrderCatalogAdd({ goods_code, name, payment_price, bill_tax, raw }) { const code = String(goods_code || '').trim() if (!code) return if (orderForm.goods.some((g) => String(g.goods_code || '').trim() === code)) return const row = emptyOrderGoodsRow() row.goods_code = code - row._name = String(name || '').trim() + const source = raw?.raw || raw + let displayName = String(name || '').trim() + if (!displayName || displayName === code) { + displayName = pickGoodsNameFromListedRow(source) || displayName + } + row._name = displayName !== code ? displayName : '' if (payment_price != null && Number.isFinite(payment_price)) { row.purchase_amount = payment_price } diff --git a/src/views/base/WhWaste.vue b/src/views/base/WhWaste.vue index e446cd6..245695b 100644 --- a/src/views/base/WhWaste.vue +++ b/src/views/base/WhWaste.vue @@ -135,25 +135,18 @@ append-to-body destroy-on-close :close-on-click-modal="false" - @opened="loadAllocAttrOptions" > - - + - + 搜索 +
+

{{ addFormAllocAttrLabel }}

@@ -170,6 +163,106 @@ 保存 + + +
+ + + + + + + + 查询 +
+ +
+
+
+

未找到匹配的配货属性

+
+
+ +
+
+
+ +
+
+ + +
@@ -178,7 +271,7 @@ import { onMounted, reactive, ref } from 'vue' import WarehouseOpsNameCode from '@/components/warehouse/WarehouseOpsNameCode.vue' import { ElMessage } from 'element-plus' import { warehouseListed } from '@/api/warehouse' -import { allocAttrListed, pickAllocAttrCode } from '@/api/warehouseAlloc' +import { allocAttrSearch, pickAllocAttrCode } from '@/api/warehouseAlloc' import { pickApiErrorMessage, wasteCollectListed, @@ -189,6 +282,7 @@ import { enrichWarehouseOpsRows, formatOpsLogTime, } from '@/utils/warehouseOpsDisplay' +import { pickGoodsNameFromListedRow } from '@/utils/goodsCategorys' defineOptions({ name: 'WhWaste' }) @@ -201,8 +295,21 @@ const page = ref(1) const limit = ref(20) const warehouseOptions = ref([]) -const allocAttrOptions = ref([]) const allocAttrLoading = ref(false) +const allocAttrPickVisible = ref(false) +const allocAttrPickQueried = ref(false) +const allocAttrPickRows = ref([]) +const allocAttrPickSelected = ref('') +const allocAttrPickSelectedOption = ref(null) +const allocAttrPickPage = ref(1) +const allocAttrPickTotal = ref(0) +const ALLOC_ATTR_PICK_PAGE_SIZE = 50 +const addFormAllocAttrLabel = ref('') +const allocAttrQuery = reactive({ + goods_code: '', + rdc_warehouse_code: '', + fdc_warehouse_code: '', +}) const filters = reactive({ alloc_attr_code: '', @@ -284,27 +391,52 @@ function onViewModeChange() { loadData() } -function allocAttrHint(row) { +function formatPickGoodsText(row) { + const code = String(row?.goods_code || '').trim() + const name = String( + row?.goods_name + || pickGoodsNameFromListedRow(row) + || '', + ).trim() + if (name && name !== code) return `${name}(${code})` + return code || '—' +} + +function formatPickWhText(row) { const parts = [ - row?.goods_name || row?.goods_code, row?.rdc_warehouse_name || row?.rdc_warehouse_code, row?.fdc_warehouse_name || row?.fdc_warehouse_code, ].filter(Boolean) return parts.join(' · ') } +function allocAttrHint(row) { + const parts = [formatPickGoodsText(row), formatPickWhText(row)].filter((v) => v && v !== '—') + return parts.join(' · ') +} + function mapAllocAttrOption(row) { const alloc_attr_code = pickAllocAttrCode(row) if (!alloc_attr_code) return null + const goodsText = formatPickGoodsText(row) + const whText = formatPickWhText(row) const hint = allocAttrHint(row) const label = hint ? `${alloc_attr_code}(${hint})` : alloc_attr_code - return { alloc_attr_code, label } + return { alloc_attr_code, label, hint, goodsText, whText } } -async function loadAllocAttrOptions() { +async function searchAllocAttrPick(resetPage = false) { + if (resetPage) allocAttrPickPage.value = 1 allocAttrLoading.value = true + allocAttrPickQueried.value = true try { - const res = await allocAttrListed({ page: 1, limit: 50 }) + const res = await allocAttrSearch({ + goods_code: String(allocAttrQuery.goods_code || '').trim() || undefined, + rdc_warehouse_code: String(allocAttrQuery.rdc_warehouse_code || '').trim() || undefined, + fdc_warehouse_code: String(allocAttrQuery.fdc_warehouse_code || '').trim() || undefined, + page: allocAttrPickPage.value, + limit: ALLOC_ATTR_PICK_PAGE_SIZE, + }) const enriched = enrichWarehouseOpsRows(res.list || [], { warehouseOptions: warehouseOptions.value, }) @@ -314,14 +446,64 @@ async function loadAllocAttrOptions() { if (!opt || map.has(opt.alloc_attr_code)) continue map.set(opt.alloc_attr_code, opt) } - allocAttrOptions.value = [...map.values()] - } catch { - allocAttrOptions.value = [] + allocAttrPickRows.value = [...map.values()] + allocAttrPickTotal.value = res.total || allocAttrPickRows.value.length + } catch (e) { + allocAttrPickRows.value = [] + allocAttrPickTotal.value = 0 + ElMessage.error(pickApiErrorMessage(e, '搜索失败')) } finally { allocAttrLoading.value = false } } +function loadAllocAttrPickPage() { + void searchAllocAttrPick(false) +} + +function selectAllocAttrPick(opt) { + allocAttrPickSelected.value = opt?.alloc_attr_code || '' + allocAttrPickSelectedOption.value = opt || null +} + +function resetAllocAttrPick() { + allocAttrQuery.goods_code = '' + allocAttrQuery.rdc_warehouse_code = '' + allocAttrQuery.fdc_warehouse_code = '' + allocAttrPickRows.value = [] + allocAttrPickSelected.value = '' + allocAttrPickSelectedOption.value = null + allocAttrPickQueried.value = false + allocAttrPickPage.value = 1 + allocAttrPickTotal.value = 0 +} + +function openAllocAttrPickDlg() { + resetAllocAttrPick() + allocAttrPickSelected.value = String(addForm.alloc_attr_code || '').trim() + allocAttrPickVisible.value = true +} + +function onAllocAttrPickOpened() { + void searchAllocAttrPick(true) +} + +function confirmAllocAttrPick() { + const code = String(allocAttrPickSelected.value || '').trim() + if (!code) return + const hit = allocAttrPickSelectedOption.value + || allocAttrPickRows.value.find((opt) => opt.alloc_attr_code === code) + addForm.alloc_attr_code = code + addFormAllocAttrLabel.value = hit?.label || code + allocAttrPickVisible.value = false + addFormRef.value?.validateField?.('alloc_attr_code') +} + +function resetAllocAttrSearch() { + resetAllocAttrPick() + addFormAllocAttrLabel.value = '' +} + async function loadWarehouses() { try { const list = await warehouseListed({}) @@ -341,9 +523,13 @@ async function loadWarehouses() { function openAddDlg() { const preset = filters.alloc_attr_code || '' + resetAllocAttrSearch() Object.assign(addForm, emptyAddForm(), { alloc_attr_code: preset, }) + if (preset) { + addFormAllocAttrLabel.value = preset + } addDlgVisible.value = true } @@ -494,6 +680,120 @@ onMounted(async () => { .whw-form-num { width: 100%; } +.whw-attr-code-row { + display: flex; + align-items: center; + gap: 8px; + width: 100%; +} +.whw-attr-code-row :deep(.el-input) { + flex: 1; + min-width: 0; +} +.whw-attr-picked-label { + margin: 6px 0 0; + font-size: 12px; + color: #606266; + line-height: 1.4; + word-break: break-all; +} +.whw-attr-pick-filters { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 12px; +} +.whw-attr-pick-filter { + width: 180px; +} +.whw-attr-pick-panel { + display: flex; + flex-direction: column; + gap: 10px; +} +.whw-attr-pick-body { + height: 320px; + display: flex; + flex-direction: column; + border: 1px solid #ebeef5; + border-radius: 6px; + overflow: hidden; + background: #fff; +} +.whw-attr-pick-scroll { + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; + overflow-y: auto; + scrollbar-gutter: stable; +} +.whw-attr-pick-pager { + display: flex; + justify-content: flex-end; +} +.whw-attr-pick-empty-wrap { + flex: 1; + min-height: 0; + display: flex; + align-items: center; + justify-content: center; +} +.whw-attr-pick-empty { + margin: 0; + padding: 0 16px; + text-align: center; + color: #909399; + font-size: 13px; +} +.whw-attr-pick-item { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 4px; + width: 100%; + padding: 10px 12px; + border: none; + border-bottom: 1px solid #f0f2f5; + background: #fff; + text-align: left; + cursor: pointer; + transition: background .12s; +} +.whw-attr-pick-item:last-child { + border-bottom: none; +} +.whw-attr-pick-item:hover { + background: #f5f7fa; +} +.whw-attr-pick-item.is-active { + background: #ecf5ff; +} +.whw-attr-pick-code { + font-size: 13px; + font-weight: 600; + color: #303133; + font-family: ui-monospace, monospace; +} +.whw-attr-pick-code::before { + content: '配货属性 '; + font-family: inherit; + font-weight: 500; + color: #909399; +} +.whw-attr-pick-meta { + font-size: 12px; + color: #606266; + line-height: 1.4; +} +.whw-attr-pick-tag { + color: #909399; + margin-right: 4px; +} +.whw-attr-pick-sep { + margin: 0 4px; + color: #c0c4cc; +} .whw-form-num :deep(.el-input__inner) { text-align: left; }