Files
weihai_erp/src/api/supplyCollectionGoods.js
T
xiaojibeier fcf6aa2df6 20260706001
2026-07-06 12:46:54 +08:00

1071 lines
36 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()]
}