1071 lines
36 KiB
JavaScript
Executable File
1071 lines
36 KiB
JavaScript
Executable File
import request from '@/utils/request'
|
||
import { collectWarehouseCodesFromRow } from '@/api/supplyCoordCollection'
|
||
import { syncPolicyFieldsToApi } from '@/utils/supplyCollectionGoodsCalc'
|
||
import { loadGoodsInScenes, sceneValuesObjectFromRaw } from '@/utils/rfSceneGoodsFilter'
|
||
import {
|
||
pickGoodsCategoryEntry,
|
||
pickGoodsDisplayName,
|
||
pickGoodsNameFromListedRow,
|
||
resolveGoodsCategoryCode,
|
||
} from '@/utils/goodsCategorys'
|
||
|
||
function unwrapResponseData(res) {
|
||
if (res == null) return res
|
||
if (typeof res === 'object' && 'data' in res && res.data !== undefined) return res.data
|
||
return res
|
||
}
|
||
|
||
function unwrapListedRows(data) {
|
||
if (Array.isArray(data)) return data
|
||
if (!data || typeof data !== 'object') return []
|
||
const row = data
|
||
const list =
|
||
row.list
|
||
|| row.rows
|
||
|| row.goods
|
||
|| row.products
|
||
|| row.records
|
||
|| row.items
|
||
|| row.goods_list
|
||
|| row.data
|
||
if (Array.isArray(list)) return list
|
||
const nested = row.data
|
||
if (nested && typeof nested === 'object' && !Array.isArray(nested)) {
|
||
return unwrapListedRows(nested)
|
||
}
|
||
return []
|
||
}
|
||
|
||
function parseCollectionGoodsSearchPayload(data) {
|
||
if (!data) return { list: [], total: 0, authorizedCount: null }
|
||
if (Array.isArray(data)) {
|
||
return { list: data, total: data.length, authorizedCount: null }
|
||
}
|
||
const list = unwrapListedRows(data)
|
||
const raw = data?.total ?? data?.count ?? data?.total_count
|
||
const n = raw != null && raw !== '' ? Number(raw) : NaN
|
||
const total = Number.isFinite(n) && n >= 0 ? n : list.length
|
||
const authRaw =
|
||
data?.authorized_count ?? data?.auth_count ?? data?.authorized_total ?? data?.purchasable_count
|
||
const authN = authRaw != null && authRaw !== '' ? Number(authRaw) : NaN
|
||
const authorizedCount = Number.isFinite(authN) && authN >= 0 ? authN : null
|
||
return { list, total, authorizedCount }
|
||
}
|
||
|
||
function normalizeCollectionSearchScenes(sceneRows) {
|
||
return (Array.isArray(sceneRows) ? sceneRows : [])
|
||
.map((row) => {
|
||
const scene_code = String(row?.scene_code || '').trim()
|
||
if (!scene_code) return null
|
||
const scene_values = sceneValuesObjectFromRaw(row?.scene_values)
|
||
return Object.keys(scene_values).length > 0
|
||
? { scene_code, scene_values }
|
||
: { scene_code }
|
||
})
|
||
.filter(Boolean)
|
||
}
|
||
|
||
/**
|
||
* POST /api/supply/collection/goods/search — 统筹集合录入商品·左侧物料池
|
||
* @param {{
|
||
* supply_collection_code: string,
|
||
* page?: number,
|
||
* limit?: number,
|
||
* keywords?: string,
|
||
* scenes?: { scene_code: string, scene_values?: Record<string, string> }[],
|
||
* }} body
|
||
*/
|
||
export async function searchCollectionGoods(body) {
|
||
const supply_collection_code = String(body?.supply_collection_code || '').trim()
|
||
if (!supply_collection_code) return { list: [], total: 0, authorizedCount: null }
|
||
|
||
const page = Math.max(1, Number(body?.page) || 1)
|
||
const limit = Math.max(1, Number(body?.limit) || 20)
|
||
const kw = String(body?.keywords ?? body?.keyworkds ?? '').trim()
|
||
const scenes = normalizeCollectionSearchScenes(body?.scenes)
|
||
|
||
const res = await request.post('/api/supply/collection/goods/search', {
|
||
supply_collection_code,
|
||
page,
|
||
limit,
|
||
keyworkds: kw,
|
||
keywords: kw,
|
||
scenes,
|
||
})
|
||
return parseCollectionGoodsSearchPayload(unwrapResponseData(res))
|
||
}
|
||
|
||
/**
|
||
* POST /api/supply/collection/in_scenes — 统筹集合·本集合内按场景筛查
|
||
* @param {{
|
||
* supply_collection_code: string,
|
||
* page?: number,
|
||
* limit?: number,
|
||
* keywords?: string,
|
||
* scenes?: { scene_code: string, scene_values?: Record<string, string> }[],
|
||
* }} body
|
||
*/
|
||
export async function searchCollectionInScenes(body) {
|
||
const supply_collection_code = String(body?.supply_collection_code || '').trim()
|
||
if (!supply_collection_code) return { list: [], total: 0, authorizedCount: null }
|
||
|
||
const page = Math.max(1, Number(body?.page) || 1)
|
||
const limit = Math.max(1, Number(body?.limit) || 20)
|
||
const kw = String(body?.keywords ?? body?.keyworkds ?? '').trim()
|
||
const scenes = normalizeCollectionSearchScenes(body?.scenes)
|
||
|
||
const res = await request.post('/api/supply/collection/in_scenes', {
|
||
supply_collection_code,
|
||
page,
|
||
limit,
|
||
keyworkds: kw,
|
||
keywords: kw,
|
||
scenes,
|
||
})
|
||
return parseCollectionGoodsSearchPayload(unwrapResponseData(res))
|
||
}
|
||
|
||
const COLLECTION_IN_SCENES_MAX_PAGES = 50
|
||
|
||
/** 翻页拉齐本集合内场景筛查命中的 goods_code(供右侧本地表格过滤) */
|
||
export async function fetchCollectionInScenesCodes(body, options = {}) {
|
||
const limit = Math.max(1, Number(body?.limit) || 50)
|
||
const maxPages = Math.max(1, Number(options?.maxPages) || COLLECTION_IN_SCENES_MAX_PAGES)
|
||
const codes = new Set()
|
||
let page = 1
|
||
let total = Infinity
|
||
|
||
while (page <= maxPages && codes.size < total) {
|
||
const { list, total: pageTotal } = await searchCollectionInScenes({ ...body, page, limit })
|
||
if (Number.isFinite(pageTotal) && pageTotal >= 0) total = pageTotal
|
||
if (!list.length) break
|
||
for (const row of list) {
|
||
const code = String(row?.goods_code || row?.code || '').trim()
|
||
if (code) codes.add(code)
|
||
}
|
||
if (list.length < limit) break
|
||
page += 1
|
||
}
|
||
|
||
return {
|
||
codes,
|
||
total: Number.isFinite(total) && total >= 0 ? total : codes.size,
|
||
}
|
||
}
|
||
|
||
/**
|
||
* listed / update 政策字段(与 goods/update 一致)
|
||
* - 销售:sales_type, sales_mode, sales_rate(%), sales_amount(¥)
|
||
* - 返利:rebate_type, rebate_mode, rebate_rate(%), rebate_amount(¥)
|
||
* - 手工基准:rebate_manual_base(仅 rebate_type=3)
|
||
*/
|
||
export const COLLECTION_GOODS_POLICY_FIELD_KEYS = [
|
||
'sales_type',
|
||
'sales_mode',
|
||
'sales_rate',
|
||
'sales_amount',
|
||
'rebate_type',
|
||
'rebate_mode',
|
||
'rebate_rate',
|
||
'rebate_amount',
|
||
'rebate_manual_base',
|
||
]
|
||
|
||
export const COLLECTION_GOODS_REBATE_MANUAL_BASE_KEY = 'rebate_manual_base'
|
||
|
||
/** 额外费用 · 单位商品各票种成本(统筹录入弹窗) */
|
||
export const COLLECTION_GOODS_EXTRA_FEE_FIELDS = [
|
||
{ key: 'extra_cost_vat_13', label: '13%专票成本', group: 'special', bill_tax: '13' },
|
||
{ key: 'extra_cost_vat_9', label: '9%专票成本', group: 'special', bill_tax: '9' },
|
||
{ key: 'extra_cost_vat_6', label: '6%专票成本', group: 'special', bill_tax: '6' },
|
||
{ key: 'extra_cost_vat_5', label: '5%专票成本', group: 'special', bill_tax: '5' },
|
||
{ key: 'extra_cost_vat_3', label: '3%专票成本', group: 'special', bill_tax: '3' },
|
||
{ key: 'extra_cost_general', label: '普票成本', group: 'other', bill_tax: 'general' },
|
||
{ key: 'extra_cost_no_invoice', label: '无票成本', group: 'other', bill_tax: 'no_invoice' },
|
||
]
|
||
|
||
const EXTRA_FEE_FIELD_BY_BILL_TAX = Object.fromEntries(
|
||
COLLECTION_GOODS_EXTRA_FEE_FIELDS.map((f) => [f.bill_tax, f]),
|
||
)
|
||
|
||
/** @param {unknown} raw */
|
||
export function normalizeBillTaxKey(raw) {
|
||
const t = String(raw ?? '').trim()
|
||
if (!t) return ''
|
||
if (EXTRA_FEE_FIELD_BY_BILL_TAX[t]) return t
|
||
if (t === '普票' || t === 'general_ticket') return 'general'
|
||
if (t === '无票' || t === 'no_ticket') return 'no_invoice'
|
||
const n = Number(t)
|
||
if (Number.isFinite(n)) {
|
||
const intKey = String(Math.trunc(n))
|
||
if (EXTRA_FEE_FIELD_BY_BILL_TAX[intKey]) return intKey
|
||
}
|
||
return t
|
||
}
|
||
|
||
/** @param {unknown} raw */
|
||
function coerceExtraFeesSource(raw) {
|
||
if (raw == null) return null
|
||
if (typeof raw === 'string') {
|
||
const s = raw.trim()
|
||
if (!s) return null
|
||
try {
|
||
return JSON.parse(s)
|
||
} catch {
|
||
return null
|
||
}
|
||
}
|
||
return raw
|
||
}
|
||
|
||
/** @param {Record<string, unknown>} src @param {{ key: string, bill_tax: string }} field */
|
||
function readExtraFeeValue(src, field) {
|
||
if (!src || typeof src !== 'object') return null
|
||
const keys = [field.key, field.bill_tax]
|
||
for (const k of keys) {
|
||
const v = src[k]
|
||
if (v != null && v !== '') return v
|
||
}
|
||
const n = Number(field.bill_tax)
|
||
if (Number.isFinite(n)) {
|
||
const numVal = src[n] ?? src[String(n)]
|
||
if (numVal != null && numVal !== '') return numVal
|
||
}
|
||
return null
|
||
}
|
||
|
||
/** @param {unknown} extraFees */
|
||
export function hasAnyExtraFeeValue(extraFees) {
|
||
if (!extraFees || typeof extraFees !== 'object') return false
|
||
return COLLECTION_GOODS_EXTRA_FEE_FIELDS.some((f) => {
|
||
const v = String(/** @type {Record<string, unknown>} */ (extraFees)[f.key] ?? '').trim()
|
||
return v !== '' && v !== '—'
|
||
})
|
||
}
|
||
|
||
/** @param {unknown} billTax */
|
||
export function extraFeeBillTaxLabel(billTax) {
|
||
const t = normalizeBillTaxKey(billTax)
|
||
if (!t) return '—'
|
||
const field = EXTRA_FEE_FIELD_BY_BILL_TAX[t]
|
||
if (field) return field.label.replace(/成本$/, '')
|
||
if (/^\d+(\.\d+)?$/.test(t)) return `${t}%专票`
|
||
return t
|
||
}
|
||
|
||
/** @param {unknown} value */
|
||
export function formatExtraFeeLogAmount(value) {
|
||
if (value == null || value === '') return '+¥—'
|
||
const n = Number(value)
|
||
if (!Number.isNaN(n)) return `+¥${n.toFixed(2)}`
|
||
const s = String(value).trim()
|
||
return s ? `+¥${s}` : '+¥—'
|
||
}
|
||
|
||
/** @param {unknown} raw */
|
||
export function formatExtraFeeLogTime(raw) {
|
||
const s = String(raw ?? '').trim()
|
||
if (!s) return ''
|
||
const d = new Date(s)
|
||
if (!Number.isNaN(d.getTime())) {
|
||
const pad = (n) => String(n).padStart(2, '0')
|
||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||
}
|
||
return s.replace('T', ' ').replace(/\.\d+$/, '')
|
||
}
|
||
|
||
/** @param {unknown} raw */
|
||
export function formatExtraFeeLogDate(raw) {
|
||
const s = formatExtraFeeLogTime(raw)
|
||
if (!s) return ''
|
||
return s.split(' ')[0] || ''
|
||
}
|
||
|
||
/** @param {unknown} raw */
|
||
export function formatExtraFeeLogClock(raw) {
|
||
const s = formatExtraFeeLogTime(raw)
|
||
if (!s || !s.includes(' ')) return ''
|
||
return s.split(' ')[1] || ''
|
||
}
|
||
|
||
/** @param {{ bill_tax?: string, add_amount?: unknown, time?: string, operator?: string, operator_code?: string }} log */
|
||
export function extraFeeLogActionText(log) {
|
||
const clock = formatExtraFeeLogClock(log?.time)
|
||
const tax = extraFeeBillTaxLabel(log?.bill_tax)
|
||
const amt = formatExtraFeeLogAmount(log?.add_amount)
|
||
const op = extraFeeLogOperator(log)
|
||
const parts = []
|
||
if (clock) parts.push(clock)
|
||
parts.push(`${tax} 追加 ${amt}`)
|
||
if (op) parts.push(op)
|
||
return parts.join(' · ')
|
||
}
|
||
|
||
/** @param {{ operator?: string, operator_code?: string }} log */
|
||
export function extraFeeLogOperator(log) {
|
||
const name = String(log?.operator || '').trim()
|
||
const code = String(log?.operator_code || '').trim()
|
||
return name || code || ''
|
||
}
|
||
|
||
/** @param {Record<string, unknown>} row */
|
||
function pickExtraFeeEntryTotal(row) {
|
||
const v = row.after_amount ?? row.amount ?? row.total ?? row.value
|
||
if (v == null || v === '') return null
|
||
return v
|
||
}
|
||
|
||
/**
|
||
* 日志条目 → 各票种累计(同 bill_tax 取 ctime 最新一条的 after_amount/amount)
|
||
* @param {unknown[]} entries
|
||
*/
|
||
export function extraFeesTotalsFromLatestLogEntries(entries) {
|
||
const out = emptyExtraFees()
|
||
const latestByTax = new Map()
|
||
for (const row of entries || []) {
|
||
if (!row || typeof row !== 'object') continue
|
||
const tax = normalizeBillTaxKey(row.bill_tax)
|
||
const field = EXTRA_FEE_FIELD_BY_BILL_TAX[tax]
|
||
if (!field) continue
|
||
const total = pickExtraFeeEntryTotal(row)
|
||
if (total == null) continue
|
||
const time = String(row.time ?? row.ctime ?? '').trim()
|
||
const prev = latestByTax.get(tax)
|
||
if (!prev || time >= prev.time) {
|
||
latestByTax.set(tax, { time, total })
|
||
}
|
||
}
|
||
for (const [tax, { total }] of latestByTax) {
|
||
const field = EXTRA_FEE_FIELD_BY_BILL_TAX[tax]
|
||
if (field) out[field.key] = toDisplayNum(total)
|
||
}
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* 日志条目 after_amount → 各票种累计(同批同 bill_tax 取最后一条)
|
||
* @param {unknown[]} entries
|
||
*/
|
||
export function extraFeesTotalsFromLogEntries(entries) {
|
||
const out = emptyExtraFees()
|
||
const byTax = new Map()
|
||
for (const row of entries || []) {
|
||
if (!row || typeof row !== 'object') continue
|
||
const tax = normalizeBillTaxKey(row.bill_tax)
|
||
const field = EXTRA_FEE_FIELD_BY_BILL_TAX[tax]
|
||
if (!field) continue
|
||
const total = pickExtraFeeEntryTotal(row)
|
||
if (total == null) continue
|
||
byTax.set(tax, total)
|
||
}
|
||
for (const [tax, val] of byTax) {
|
||
const field = EXTRA_FEE_FIELD_BY_BILL_TAX[tax]
|
||
if (field) out[field.key] = toDisplayNum(val)
|
||
}
|
||
return out
|
||
}
|
||
|
||
/** @param {unknown} current @param {unknown} patch */
|
||
export function mergeExtraFeesPatch(current, patch) {
|
||
const out = normalizeExtraFees(current)
|
||
const p = normalizeExtraFees(patch)
|
||
for (const f of COLLECTION_GOODS_EXTRA_FEE_FIELDS) {
|
||
const v = String(p[f.key] ?? '').trim()
|
||
if (v !== '') out[f.key] = v
|
||
}
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, string>} addDraft
|
||
* @param {string} [remark]
|
||
*/
|
||
export function buildExtraFeesAddList(addDraft, remark = '') {
|
||
const src = addDraft && typeof addDraft === 'object' && !Array.isArray(addDraft)
|
||
? addDraft
|
||
: {}
|
||
const rm = String(remark || '').trim()
|
||
const list = []
|
||
for (const f of COLLECTION_GOODS_EXTRA_FEE_FIELDS) {
|
||
const str = String(src[f.key] ?? '').trim()
|
||
if (str === '') continue
|
||
const n = Number(str)
|
||
if (Number.isNaN(n)) continue
|
||
const item = { bill_tax: f.bill_tax, amount: n }
|
||
if (rm) item.remark = rm
|
||
list.push(item)
|
||
}
|
||
return list
|
||
}
|
||
|
||
function normalizeExtraFeeLogRow(row) {
|
||
if (!row || typeof row !== 'object') return null
|
||
const addAmount = row.add_amount
|
||
return {
|
||
id: row.id,
|
||
operator: String(row.cname || '').trim(),
|
||
operator_code: String(row.cuser || '').trim(),
|
||
time: String(row.ctime || '').trim(),
|
||
bill_tax: normalizeBillTaxKey(row.bill_tax),
|
||
add_amount: addAmount,
|
||
before_amount: row.before_amount,
|
||
after_amount: row.after_amount,
|
||
remark: String(row.remark || '').trim(),
|
||
}
|
||
}
|
||
|
||
function parseExtraFeesLogPayload(data) {
|
||
if (!data) return { list: [], total: 0 }
|
||
if (Array.isArray(data)) {
|
||
const rows = data.map(normalizeExtraFeeLogRow).filter(Boolean)
|
||
return { list: rows, total: rows.length }
|
||
}
|
||
let list = unwrapListedRows(data)
|
||
if (!list.length && Array.isArray(data.logs)) list = data.logs
|
||
if (!list.length && Array.isArray(data.log_list)) list = data.log_list
|
||
if (!list.length && Array.isArray(data.records)) list = data.records
|
||
const rows = list.map(normalizeExtraFeeLogRow).filter(Boolean)
|
||
const raw = data?.total ?? data?.count ?? data?.total_count
|
||
const n = raw != null && raw !== '' ? Number(raw) : NaN
|
||
const total = Number.isFinite(n) && n >= 0 ? n : rows.length
|
||
return { list: rows, total }
|
||
}
|
||
|
||
/** @param {unknown} data */
|
||
export function extractExtraFeesFromAddResponse(data) {
|
||
const coerced = coerceExtraFeesSource(data)
|
||
if (!coerced || typeof coerced !== 'object') return null
|
||
const row = coerced
|
||
|
||
if (row.extra_fees != null) {
|
||
const normalized = normalizeExtraFees(row.extra_fees)
|
||
if (hasAnyExtraFeeValue(normalized)) return normalized
|
||
}
|
||
|
||
if (Array.isArray(row.list) && row.list.length) {
|
||
const fromLogs = extraFeesTotalsFromLatestLogEntries(row.list)
|
||
if (hasAnyExtraFeeValue(fromLogs)) return fromLogs
|
||
}
|
||
|
||
const direct = normalizeExtraFees(row)
|
||
if (hasAnyExtraFeeValue(direct)) return direct
|
||
|
||
return null
|
||
}
|
||
|
||
/**
|
||
* POST /api/supply/collection/goods/extra_fees/add
|
||
* @param {{ supply_collection_code: string, goods_code: string, list: object[] }} body
|
||
*/
|
||
export async function supplyCollectionGoodsExtraFeesAdd(body) {
|
||
const res = await request.post('/api/supply/collection/goods/extra_fees/add', body)
|
||
return unwrapResponseData(res)
|
||
}
|
||
|
||
/**
|
||
* POST /api/supply/collection/goods/extra_fees/log
|
||
* @param {string} supplyCollectionCode
|
||
* @param {string} goodsCode
|
||
* @param {{ page?: number, limit?: number }} [options]
|
||
*/
|
||
export async function fetchCollectionGoodsExtraFeesLog(
|
||
supplyCollectionCode,
|
||
goodsCode,
|
||
options = {},
|
||
) {
|
||
const supply_collection_code = String(supplyCollectionCode || '').trim()
|
||
const goods_code = String(goodsCode || '').trim()
|
||
const page = Math.max(1, Number(options.page) || 1)
|
||
const limit = Math.min(50, Math.max(1, Number(options.limit) || 20))
|
||
const res = await request.post('/api/supply/collection/goods/extra_fees/log', {
|
||
supply_collection_code,
|
||
goods_code,
|
||
page,
|
||
limit,
|
||
})
|
||
return parseExtraFeesLogPayload(unwrapResponseData(res))
|
||
}
|
||
|
||
export function emptyExtraFees() {
|
||
return Object.fromEntries(
|
||
COLLECTION_GOODS_EXTRA_FEE_FIELDS.map((f) => [f.key, '']),
|
||
)
|
||
}
|
||
|
||
/** @deprecated 使用 emptyExtraFees */
|
||
export const emptyExtraFeeCosts = emptyExtraFees
|
||
|
||
export function normalizeExtraFees(raw) {
|
||
const coerced = coerceExtraFeesSource(raw)
|
||
if (!coerced) return emptyExtraFees()
|
||
|
||
if (Array.isArray(coerced)) {
|
||
return extraFeesTotalsFromLatestLogEntries(coerced)
|
||
}
|
||
|
||
const src = typeof coerced === 'object' ? coerced : {}
|
||
const out = emptyExtraFees()
|
||
for (const f of COLLECTION_GOODS_EXTRA_FEE_FIELDS) {
|
||
const v = readExtraFeeValue(src, f)
|
||
if (v != null && v !== '') out[f.key] = toDisplayNum(v)
|
||
}
|
||
return out
|
||
}
|
||
|
||
export function buildExtraFeesPayload(extraFees) {
|
||
const src = extraFees && typeof extraFees === 'object' && !Array.isArray(extraFees)
|
||
? extraFees
|
||
: {}
|
||
const out = {}
|
||
for (const f of COLLECTION_GOODS_EXTRA_FEE_FIELDS) {
|
||
const str = String(src[f.key] ?? '').trim()
|
||
if (str === '') continue
|
||
const n = Number(str)
|
||
if (!Number.isNaN(n)) out[f.key] = n
|
||
}
|
||
return out
|
||
}
|
||
|
||
export function hydrateExtraFeesOnRow(row, source) {
|
||
const r = source && typeof source === 'object' ? source : {}
|
||
row.extra_fees = normalizeExtraFees(r.extra_fees ?? r.raw?.extra_fees)
|
||
}
|
||
|
||
/** @deprecated 使用 hydrateExtraFeesOnRow */
|
||
export function hydrateExtraFeeCostsFromApi(target, source) {
|
||
hydrateExtraFeesOnRow(target, source)
|
||
}
|
||
|
||
/** 保底返利 rebate_type(与 demo 下拉一致) */
|
||
export const SUPPLY_COLLECTION_REBATE_TYPE_OPTIONS = [
|
||
{ value: 0, label: '不设置' },
|
||
{ value: 1, label: '基于网价' },
|
||
{ value: 2, label: '基于付款单价' },
|
||
{ value: 3, label: '手工录入基准值' },
|
||
{ value: 4, label: '固定金额返利' },
|
||
]
|
||
|
||
/** @deprecated 使用 @/utils/supplyCollectionGoodsCalc COLLECTION_SALES_HAS_OPTIONS */
|
||
export { COLLECTION_SALES_HAS_OPTIONS as SUPPLY_COLLECTION_SALES_YES_NO_OPTIONS } from '@/utils/supplyCollectionGoodsCalc'
|
||
|
||
function toDisplayNum(v) {
|
||
if (v == null || v === '') return ''
|
||
const n = Number(v)
|
||
return Number.isNaN(n) ? String(v) : String(v)
|
||
}
|
||
|
||
function strNum(v) {
|
||
if (v == null || v === '') return ''
|
||
return String(v).trim()
|
||
}
|
||
|
||
function parseIsAuthorized(v) {
|
||
if (v === true || v === 1 || v === '1') return true
|
||
if (v === false || v === 0 || v === '0') return false
|
||
return null
|
||
}
|
||
|
||
function parseStableSupply(v) {
|
||
return v === true || v === 1 || v === '1'
|
||
}
|
||
|
||
/** @param {unknown} row */
|
||
export function normalizeCollectionGoodsRow(row) {
|
||
if (!row || typeof row !== 'object') return null
|
||
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
|
||
|| r.goodsCode
|
||
|| r.product_code
|
||
|| r.material_code
|
||
|| r.sku
|
||
|| r.item_code
|
||
|| r.code
|
||
|| nested?.goods_code
|
||
|| nested?.code
|
||
|| '',
|
||
).trim()
|
||
if (!goods_code) return null
|
||
const category_code = resolveGoodsCategoryCode(r) || String(
|
||
r.category_code
|
||
|| r.classify_code
|
||
|| r.categoryCode
|
||
|| nested?.category_code
|
||
|| nested?.classify_code
|
||
|| '',
|
||
).trim()
|
||
const catEntry = pickGoodsCategoryEntry(r, category_code)
|
||
const category_name = String(
|
||
catEntry?.category_name || r.category_name || r.classify_name || '',
|
||
).trim()
|
||
const name = pickGoodsDisplayName(r, category_code, { allowTopLevelName: true })
|
||
|| String(
|
||
r.name
|
||
|| r.goods_name
|
||
|| r.goodsName
|
||
|| r.product_name
|
||
|| r.material_name
|
||
|| nested?.goods_name
|
||
|| nested?.name
|
||
|| goods_code,
|
||
).trim()
|
||
return {
|
||
goods_code,
|
||
name,
|
||
category_code,
|
||
category_name,
|
||
brand: String(r.brand || r.brand_name || '').trim(),
|
||
model: String(r.model || r.spec || r.specification || '').trim(),
|
||
scene_code: String(r.scene_code || '').trim(),
|
||
web_price: toDisplayNum(r.net_price ?? r.web_price ?? r.price ?? ''),
|
||
warehouse_codes: collectWarehouseCodesFromRow(r),
|
||
rebate_type: r.rebate_type ?? '',
|
||
rebate_mode: r.rebate_mode ?? '',
|
||
rebate_rate: toDisplayNum(r.rebate_rate ?? ''),
|
||
rebate_amount: toDisplayNum(r.rebate_amount ?? ''),
|
||
sales_type: r.sales_type ?? '',
|
||
sales_mode: r.sales_mode ?? '',
|
||
sales_rate: toDisplayNum(r.sales_rate ?? ''),
|
||
sales_amount: toDisplayNum(r.sales_amount ?? ''),
|
||
rebate_manual_base: toDisplayNum(r.rebate_manual_base ?? r.manual_base ?? ''),
|
||
is_authorized: parseIsAuthorized(r.is_authorized),
|
||
is_stable: parseStableSupply(r.is_stable ?? r.is_stable_supply ?? r.stable_supply),
|
||
extra_fees: normalizeExtraFees(r.extra_fees),
|
||
business_type_code: String(r.business_type_code || '').trim(),
|
||
business_type_name: String(r.business_type_name || '').trim(),
|
||
bills: Array.isArray(r.bills) ? r.bills : [],
|
||
raw: row,
|
||
}
|
||
}
|
||
|
||
const COLLECTION_GOODS_LISTED_PAGE_SIZE = 100
|
||
const COLLECTION_GOODS_LISTED_MAX_PAGES = 200
|
||
|
||
function normalizeCollectionGoodsListedPayload(data) {
|
||
const { list, total } = parseCollectionGoodsSearchPayload(data)
|
||
const rows = []
|
||
const seen = new Set()
|
||
for (const row of list) {
|
||
const g = normalizeCollectionGoodsRow(row)
|
||
if (!g || seen.has(g.goods_code)) continue
|
||
seen.add(g.goods_code)
|
||
rows.push(g)
|
||
}
|
||
return { rows, total }
|
||
}
|
||
|
||
/** 从统筹商品 bills[] 取首选进项税率(默认 bills[0]) */
|
||
export function pickCollectionGoodsPreferredTax(row) {
|
||
const normalized = normalizeCollectionGoodsRow(row) || (row && typeof row === 'object' ? row : null)
|
||
if (!normalized) return null
|
||
const raw = normalized.raw || row
|
||
const bills = Array.isArray(normalized.bills) ? normalized.bills : (Array.isArray(raw?.bills) ? raw.bills : [])
|
||
const first = bills[0]
|
||
if (!first) return null
|
||
const v = first.tax ?? first.rate
|
||
if (v == null || v === '') return null
|
||
const n = Number(v)
|
||
return Number.isFinite(n) ? n : null
|
||
}
|
||
|
||
/** 选品弹窗:从集合商品行提取 goods_code / name / payment_price / 进项税率 */
|
||
export function pickCollectionGoodsPickFields(row) {
|
||
const normalized = normalizeCollectionGoodsRow(row) || (row && typeof row === 'object' ? row : null)
|
||
if (!normalized) return null
|
||
const sourceRow = normalized.raw || row
|
||
const goods_code = String(normalized.goods_code || '').trim()
|
||
if (!goods_code) return null
|
||
const payRaw = sourceRow?.payment_price ?? row?.payment_price ?? ''
|
||
const payment_price = payRaw !== '' && payRaw != null ? Number(payRaw) : null
|
||
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,
|
||
payment_price: Number.isFinite(payment_price) ? payment_price : null,
|
||
bill_tax,
|
||
raw: normalized,
|
||
}
|
||
}
|
||
|
||
/** 按税率在 type=4 费用票据列表中匹配 bill_code;无匹配则返回 fallback */
|
||
export function matchExpenseBillCodeByTax(tax, expenseBills, fallbackCode = '') {
|
||
if (!Number.isFinite(tax)) return String(fallbackCode || '').trim()
|
||
const list = Array.isArray(expenseBills) ? expenseBills : []
|
||
const hit = list.find((b) => {
|
||
const v = b?.tax ?? b?.rate
|
||
if (v == null || v === '') return false
|
||
const n = Number(v)
|
||
return Number.isFinite(n) && n === tax
|
||
})
|
||
const code = String(hit?.bill_code ?? '').trim()
|
||
return code || String(fallbackCode || '').trim()
|
||
}
|
||
|
||
/**
|
||
* 统筹集合商品分页(无关键词走 listed,有关键词走 search)
|
||
* @param {{ supply_collection_code: string, page?: number, limit?: number, keywords?: string }} body
|
||
*/
|
||
export async function listCollectionGoodsPage(body = {}) {
|
||
const supply_collection_code = String(body?.supply_collection_code || '').trim()
|
||
if (!supply_collection_code) return { list: [], total: 0 }
|
||
const page = Math.max(1, Number(body?.page) || 1)
|
||
const limit = Math.max(1, Number(body?.limit) || 20)
|
||
const kw = String(body?.keywords ?? body?.keyworkds ?? '').trim()
|
||
|
||
if (kw) {
|
||
const res = await searchCollectionGoods({
|
||
supply_collection_code,
|
||
keywords: kw,
|
||
page,
|
||
limit,
|
||
})
|
||
const list = (res.list || []).map(normalizeCollectionGoodsRow).filter(Boolean)
|
||
return { list, total: res.total || list.length }
|
||
}
|
||
|
||
const res = await request.post('/api/supply/collection/goods/listed', {
|
||
supply_collection_code,
|
||
scene_code: '',
|
||
page,
|
||
limit,
|
||
})
|
||
const { rows, total } = normalizeCollectionGoodsListedPayload(unwrapResponseData(res))
|
||
return { list: rows, total }
|
||
}
|
||
|
||
/**
|
||
* POST /api/supply/collection/goods/listed
|
||
* 接口已分页:supply_collection_code + scene_code(空=全部场景)+ page + limit
|
||
* @param {string} supplyCollectionCode
|
||
* @param {{ silent?: boolean, scene_code?: string, pageSize?: number }} [options]
|
||
*/
|
||
export async function fetchCollectionGoods(supplyCollectionCode, options = {}) {
|
||
const {
|
||
silent = false,
|
||
scene_code = '',
|
||
pageSize = COLLECTION_GOODS_LISTED_PAGE_SIZE,
|
||
} = options
|
||
const supply_collection_code = String(supplyCollectionCode || '').trim()
|
||
if (!supply_collection_code) return []
|
||
const sceneCode = String(scene_code || '').trim()
|
||
const limit = Math.max(1, Number(pageSize) || COLLECTION_GOODS_LISTED_PAGE_SIZE)
|
||
|
||
try {
|
||
const all = []
|
||
const seen = new Set()
|
||
let page = 1
|
||
let total = Infinity
|
||
|
||
while (page <= COLLECTION_GOODS_LISTED_MAX_PAGES && all.length < total) {
|
||
const res = await request.post('/api/supply/collection/goods/listed', {
|
||
supply_collection_code,
|
||
scene_code: sceneCode,
|
||
page,
|
||
limit,
|
||
})
|
||
const d = unwrapResponseData(res)
|
||
const { rows, total: pageTotal } = normalizeCollectionGoodsListedPayload(d)
|
||
if (Number.isFinite(pageTotal) && pageTotal >= 0) total = pageTotal
|
||
if (!rows.length) break
|
||
for (const g of rows) {
|
||
if (seen.has(g.goods_code)) continue
|
||
seen.add(g.goods_code)
|
||
all.push(g)
|
||
}
|
||
if (rows.length < limit) break
|
||
page += 1
|
||
}
|
||
|
||
return all
|
||
} catch (e) {
|
||
if (silent) return []
|
||
throw e
|
||
}
|
||
}
|
||
|
||
/** POST /api/supply/collection/goods/add */
|
||
export function supplyCollectionGoodsAdd(body) {
|
||
return request.post('/api/supply/collection/goods/add', body)
|
||
}
|
||
|
||
/** POST /api/supply/collection/goods/remove */
|
||
export function supplyCollectionGoodsRemove(body) {
|
||
return request.post('/api/supply/collection/goods/remove', body)
|
||
}
|
||
|
||
/** POST /api/supply/collection/goods/batch_add */
|
||
export function supplyCollectionGoodsBatchAdd(body) {
|
||
return request.post('/api/supply/collection/goods/batch_add', body)
|
||
}
|
||
|
||
/** POST /api/supply/collection/goods/update */
|
||
export function supplyCollectionGoodsUpdate(body) {
|
||
return request.post('/api/supply/collection/goods/update', body)
|
||
}
|
||
|
||
/** POST /api/supply/collection/copy_goods — 复制物料到其他统筹集合 */
|
||
export const COPY_COLLECTION_GOODS_FIELD_KEYS = [
|
||
'net_price',
|
||
'warehouses',
|
||
'rebate_type',
|
||
'rebate_mode',
|
||
'rebate_rate',
|
||
'rebate_amount',
|
||
'sales_type',
|
||
'sales_mode',
|
||
'sales_rate',
|
||
'sales_amount',
|
||
'rebate_manual_base',
|
||
]
|
||
|
||
export const COPY_COLLECTION_GOODS_FIELD_LABELS = {
|
||
net_price: '网价',
|
||
warehouses: '直达仓',
|
||
rebate_type: '基础返利类型',
|
||
rebate_mode: '基础返利方式',
|
||
rebate_rate: '基础返利比例',
|
||
rebate_amount: '基础返利金额',
|
||
sales_type: '销售折让类型',
|
||
sales_mode: '销售折让方式',
|
||
sales_rate: '销售折让比例',
|
||
sales_amount: '销售折让金额',
|
||
rebate_manual_base: '返利手工基准',
|
||
}
|
||
|
||
/** 复制弹窗表头短标签 */
|
||
export const COPY_COLLECTION_GOODS_FIELD_SHORT_LABELS = {
|
||
net_price: '网价',
|
||
warehouses: '直达仓',
|
||
rebate_type: '返利类型',
|
||
rebate_mode: '返利方式',
|
||
rebate_rate: '返利比例',
|
||
rebate_amount: '返利金额',
|
||
sales_type: '折让类型',
|
||
sales_mode: '折让方式',
|
||
sales_rate: '折让比例',
|
||
sales_amount: '折让金额',
|
||
rebate_manual_base: '手工基准',
|
||
}
|
||
|
||
/**
|
||
* @param {{
|
||
* source_supply_collection_code: string,
|
||
* target_supply_collection_codes: string[],
|
||
* goods_list: { goods_code: string, category_code?: string }[],
|
||
* fields?: string[],
|
||
* }} body fields 为空表示复制全部可支持字段
|
||
*/
|
||
export function buildCollectionCopyGoodsBody(body = {}) {
|
||
const source_supply_collection_code = String(
|
||
body.source_supply_collection_code || '',
|
||
).trim()
|
||
const target_supply_collection_codes = [
|
||
...new Set(
|
||
(Array.isArray(body.target_supply_collection_codes) ? body.target_supply_collection_codes : [])
|
||
.map((c) => String(c ?? '').trim())
|
||
.filter(Boolean),
|
||
),
|
||
]
|
||
const goods_list = (Array.isArray(body.goods_list) ? body.goods_list : [])
|
||
.map((row) => ({
|
||
goods_code: String(row?.goods_code ?? '').trim(),
|
||
category_code: String(row?.category_code ?? '').trim(),
|
||
}))
|
||
.filter((row) => row.goods_code)
|
||
const fields = (Array.isArray(body.fields) ? body.fields : [])
|
||
.map((k) => String(k ?? '').trim())
|
||
.filter((k) => COPY_COLLECTION_GOODS_FIELD_KEYS.includes(k))
|
||
const payload = {
|
||
source_supply_collection_code,
|
||
target_supply_collection_codes,
|
||
goods_list,
|
||
fields,
|
||
}
|
||
if (!source_supply_collection_code) {
|
||
throw new Error('缺少 source_supply_collection_code')
|
||
}
|
||
if (!target_supply_collection_codes.length) {
|
||
throw new Error('请选择目标统筹集合')
|
||
}
|
||
if (!goods_list.length) {
|
||
throw new Error('请选择要复制的商品')
|
||
}
|
||
return payload
|
||
}
|
||
|
||
/** POST /api/supply/collection/copy_goods */
|
||
export function supplyCollectionCopyGoods(body) {
|
||
return request.post('/api/supply/collection/copy_goods', buildCollectionCopyGoodsBody(body))
|
||
}
|
||
|
||
/**
|
||
* 按各商品 fields 分组,便于一次请求只带同一套 fields(接口为整批 goods 共用 fields)
|
||
* @param {{ goods_code: string, category_code?: string }[]} goodsList
|
||
* @param {Record<string, string[]>} fieldMap goods_code → 勾选的字段
|
||
*/
|
||
export function groupCollectionCopyGoodsByFields(goodsList, fieldMap = {}) {
|
||
const groups = new Map()
|
||
for (const row of goodsList || []) {
|
||
const goods_code = String(row?.goods_code ?? '').trim()
|
||
if (!goods_code) continue
|
||
const fields = (Array.isArray(fieldMap[goods_code]) ? fieldMap[goods_code] : [])
|
||
.map((k) => String(k ?? '').trim())
|
||
.filter((k) => COPY_COLLECTION_GOODS_FIELD_KEYS.includes(k))
|
||
const sig = fields.length ? fields.slice().sort().join('|') : '__all__'
|
||
if (!groups.has(sig)) {
|
||
groups.set(sig, {
|
||
fields: fields.length ? fields : [],
|
||
goods_list: [],
|
||
})
|
||
}
|
||
groups.get(sig).goods_list.push({
|
||
goods_code,
|
||
category_code: String(row?.category_code ?? '').trim(),
|
||
})
|
||
}
|
||
return [...groups.values()]
|
||
}
|
||
|
||
export function buildCollectionGoodsUpdateBody(supplyCollectionCode, row) {
|
||
const supply_collection_code = String(supplyCollectionCode || '').trim()
|
||
const goods_code = String(row?.goods_code || '').trim()
|
||
const category_code = String(row?.category_code || '').trim()
|
||
const r = { ...row }
|
||
syncPolicyFieldsToApi(r)
|
||
const body = { supply_collection_code, category_code, goods_code }
|
||
|
||
const priceStr = String(r.web_price ?? '').trim()
|
||
if (priceStr !== '') {
|
||
const n = Number(priceStr)
|
||
if (!Number.isNaN(n)) body.net_price = n
|
||
}
|
||
|
||
const wh = Array.isArray(r.warehouse_codes)
|
||
? r.warehouse_codes.map((c) => String(c || '').trim()).filter(Boolean)
|
||
: []
|
||
body.warehouses = wh
|
||
|
||
const putNum = (key, val) => {
|
||
if (val == null || val === '') return
|
||
const n = Number(val)
|
||
if (!Number.isNaN(n)) body[key] = n
|
||
}
|
||
|
||
for (const key of COLLECTION_GOODS_POLICY_FIELD_KEYS) {
|
||
if (key === COLLECTION_GOODS_REBATE_MANUAL_BASE_KEY) continue
|
||
putNum(key, r[key])
|
||
}
|
||
|
||
const rt = Number(r.rebate_type) || 0
|
||
if (rt === 3) {
|
||
const manualBase = strNum(r.rebate_manual_base ?? r.gr_manual_base)
|
||
if (manualBase !== '') {
|
||
const m = Number(manualBase)
|
||
if (!Number.isNaN(m)) body[COLLECTION_GOODS_REBATE_MANUAL_BASE_KEY] = m
|
||
}
|
||
}
|
||
|
||
body.extra_fees = buildExtraFeesPayload(r.extra_fees)
|
||
|
||
body.is_stable = r.is_stable === true
|
||
|
||
return body
|
||
}
|
||
|
||
/** listed/update 行是否带回 warehouses 或 warehouse_codes 字段(区分「未配置」与「已存空」) */
|
||
export function goodsRowHasWarehouseSnapshot(row) {
|
||
const r = row && typeof row === 'object' ? row : {}
|
||
return Array.isArray(r.warehouses) || Array.isArray(r.warehouse_codes)
|
||
}
|
||
|
||
/**
|
||
* @param {string} supplyCollectionCode
|
||
* @param {ReturnType<typeof normalizeCollectionGoodsRow>[]} nextItems
|
||
*/
|
||
export async function persistCollectionGoods(supplyCollectionCode, nextItems) {
|
||
const supply_collection_code = String(supplyCollectionCode || '').trim()
|
||
if (!supply_collection_code) throw new Error('缺少统筹集合编码')
|
||
|
||
const makeCompositeKey = (g) => {
|
||
const cc = String(g?.category_code || '').trim()
|
||
const gc = String(g?.goods_code || '').trim()
|
||
return cc ? `${cc}:${gc}` : gc
|
||
}
|
||
|
||
const existing = await fetchCollectionGoods(supply_collection_code, { silent: true })
|
||
const existingMap = new Map(existing.map((g) => [makeCompositeKey(g), g]))
|
||
const nextMap = new Map()
|
||
for (const g of nextItems || []) {
|
||
if (!g?.goods_code) continue
|
||
nextMap.set(makeCompositeKey(g), g)
|
||
}
|
||
|
||
const removeKeys = [...existingMap.keys()].filter((k) => !nextMap.has(k))
|
||
const addKeys = [...nextMap.keys()].filter((k) => !existingMap.has(k))
|
||
|
||
for (const key of removeKeys) {
|
||
const row = existingMap.get(key)
|
||
await supplyCollectionGoodsRemove({
|
||
supply_collection_code,
|
||
goods_code: row?.goods_code || '',
|
||
category_code: row?.category_code || '',
|
||
})
|
||
}
|
||
if (addKeys.length) {
|
||
await supplyCollectionGoodsBatchAdd({
|
||
supply_collection_code,
|
||
list: addKeys.map((key) => {
|
||
const row = nextMap.get(key)
|
||
return {
|
||
goods_code: row?.goods_code || '',
|
||
category_code: row?.category_code || '',
|
||
}
|
||
}),
|
||
})
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 按统筹集合已关联场景拉物料池(逐场景 goods/in_scenes,无筛查时不传 scene_values)
|
||
* @param {string[]} sceneCodes
|
||
*/
|
||
export async function loadGoodsPoolByScenes(sceneCodes) {
|
||
const codes = [...new Set(sceneCodes.map((c) => String(c || '').trim()).filter(Boolean))]
|
||
if (!codes.length) return []
|
||
|
||
const map = new Map()
|
||
const results = await Promise.allSettled(
|
||
codes.map(async (scene_code) => {
|
||
const rows = await loadGoodsInScenes(scene_code, [])
|
||
return rows.map((row) => {
|
||
const g = normalizeCollectionGoodsRow(row)
|
||
return g ? { ...g, scene_code } : null
|
||
}).filter(Boolean)
|
||
}),
|
||
)
|
||
for (const r of results) {
|
||
if (r.status !== 'fulfilled') continue
|
||
for (const g of r.value || []) {
|
||
if (!g.goods_code || map.has(g.goods_code)) continue
|
||
map.set(g.goods_code, g)
|
||
}
|
||
}
|
||
return [...map.values()]
|
||
}
|