feat(finance): 人工支出改为从客服备用金扣款

This commit is contained in:
yml2213
2026-10-23 11:31:47 +08:00
parent cd9bb1419c
commit 2aeb789e20
7 changed files with 396 additions and 124 deletions
@@ -7,6 +7,7 @@ import { resolveFinanceDateRange } from '../../utils/finance-date-range.js'
import { generateManualExpenseNo } from '../../utils/random.js'
import { nowIso } from '../../utils/time.js'
import type { AdminSession } from '../admin/admin-auth-service.js'
import { writeFundExpense } from '../admin/admin-fund-service.js'
const EXPENSE_STATUSES = new Set([
'draft',
@@ -70,6 +71,10 @@ type ManualExpenseRow = {
sku_code: string | null
sku_name: string | null
work_order_no: string | null
fund_expense_no?: string | null
fund_account_id?: number | null
fund_account_no?: string | null
fund_account_owner_name?: string | null
}
export async function listManualExpenses(input: Record<string, unknown> = {}) {
@@ -144,12 +149,18 @@ export async function listManualExpenses(input: Record<string, unknown> = {}) {
oi.sku_name,
ci.name AS cost_item_name,
ci.code AS cost_item_code,
wo.work_order_no
wo.work_order_no,
me.expense_no AS fund_expense_no,
me.account_id AS fund_account_id,
fa.account_no AS fund_account_no,
fa.owner_name AS fund_account_owner_name
FROM manual_expense_records r
LEFT JOIN orders o ON o.id = r.order_id
LEFT JOIN order_items oi ON oi.id = r.order_item_id
LEFT JOIN cost_items ci ON ci.id = r.cost_item_id
LEFT JOIN work_orders wo ON wo.id = r.work_order_id
LEFT JOIN manual_expenses me ON me.id = r.fund_expense_id
LEFT JOIN fund_accounts fa ON fa.id = me.account_id
WHERE ${where}
ORDER BY r.paid_at DESC, r.id DESC
LIMIT $${listParams.length - 1} OFFSET $${listParams.length}
@@ -361,15 +372,39 @@ export async function createManualExpense(payload: JsonObject, session: AdminSes
inferExpenseCategory(costItem?.costType),
'支出分类无效',
)
const paidByAdminUserId =
normalizeOptionalId(payload.paidByAdminUserId ?? payload.paid_by_admin_user_id) ||
session.userId
const paidByName =
optionalText(payload.paidByName ?? payload.paid_by_name, 80) || session.displayName
const fundAccountId = normalizeOptionalId(
payload.fundAccountId ?? payload.fund_account_id ?? payload.agentAccountId,
)
if (!fundAccountId) {
throw createHttpError('请选择备用金账户,人工支出必须从备用金扣款', {
statusCode: 400,
errorCode: 'manual_expense_fund_account_required',
})
}
const expenseNo = generateManualExpenseNo()
const now = nowIso()
return withTransaction(async (client) => {
const fundWritten = await writeFundExpense(client, {
agentAccountId: fundAccountId,
amountFen,
category: mapFundExpenseCategory(expenseCategory),
payeeType: mapFundPayeeType(payeeType),
payeeName,
payeeAccount: optionalText(payload.payeeAccount ?? payload.payee_account, 120),
orderId,
orderItemId,
paidAt,
evidence: toFundEvidence(evidenceJson),
note: buildFundExpenseNote(
optionalText(payload.note, 500),
optionalText(evidenceJson.reference, 200),
expenseNo,
),
actorUserId: session.userId,
})
const fundExpenseId = Number(fundWritten.expense.id)
const result = await client.query<ManualExpenseRow>(
`
INSERT INTO manual_expense_records (
@@ -392,12 +427,12 @@ export async function createManualExpense(payload: JsonObject, session: AdminSes
orderItemId,
orderContext?.workOrderId ||
normalizeOptionalId(payload.workOrderId ?? payload.work_order_id),
normalizeOptionalId(payload.fundExpenseId ?? payload.fund_expense_id),
fundExpenseId,
costItem?.id || null,
expenseCategory,
amountFen,
paidByAdminUserId,
paidByName,
fundWritten.ownerAdminUserId,
fundWritten.ownerName || session.displayName,
payeeType,
payeeName,
optionalText(payload.payeeAccount ?? payload.payee_account, 120),
@@ -417,7 +452,7 @@ export async function createManualExpense(payload: JsonObject, session: AdminSes
fromStatus: '',
toStatus: status,
actor: session,
payload: { amountFen, costItemId: costItem?.id || null },
payload: { amountFen, costItemId: costItem?.id || null, fundExpenseId },
now,
})
return getManualExpenseByIdWithClient(client, Number(record.id))
@@ -427,6 +462,15 @@ export async function createManualExpense(payload: JsonObject, session: AdminSes
export async function createManualOrderCost(payload: JsonObject, session: AdminSession) {
const orderId = normalizeId(payload.orderId ?? payload.order_id)
const requestedItemId = normalizeOptionalId(payload.orderItemId ?? payload.order_item_id)
const fundAccountId = normalizeOptionalId(
payload.fundAccountId ?? payload.fund_account_id ?? payload.agentAccountId,
)
if (!fundAccountId) {
throw createHttpError('请选择备用金账户,人工录单必须从备用金扣款', {
statusCode: 400,
errorCode: 'manual_order_cost_fund_account_required',
})
}
const itemsResult = await query<{ id: number }>(
'SELECT id FROM order_items WHERE order_id = $1 ORDER BY id ASC',
[orderId],
@@ -491,6 +535,7 @@ export async function createManualOrderCost(payload: JsonObject, session: AdminS
orderId,
orderItemId,
costItemId,
fundAccountId,
amountFen,
expenseCategory,
payeeName,
@@ -652,12 +697,18 @@ async function getManualExpenseByIdWithClient(client: PoolClient | null, expense
oi.sku_name,
ci.name AS cost_item_name,
ci.code AS cost_item_code,
wo.work_order_no
wo.work_order_no,
me.expense_no AS fund_expense_no,
me.account_id AS fund_account_id,
fa.account_no AS fund_account_no,
fa.owner_name AS fund_account_owner_name
FROM manual_expense_records r
LEFT JOIN orders o ON o.id = r.order_id
LEFT JOIN order_items oi ON oi.id = r.order_item_id
LEFT JOIN cost_items ci ON ci.id = r.cost_item_id
LEFT JOIN work_orders wo ON wo.id = r.work_order_id
LEFT JOIN manual_expenses me ON me.id = r.fund_expense_id
LEFT JOIN fund_accounts fa ON fa.id = me.account_id
WHERE r.id = $1
LIMIT 1
`
@@ -976,6 +1027,10 @@ function mapManualExpense(row: ManualExpenseRow) {
orderItemId: row.order_item_id ? Number(row.order_item_id) : null,
workOrderId: row.work_order_id ? Number(row.work_order_id) : null,
fundExpenseId: row.fund_expense_id ? Number(row.fund_expense_id) : null,
fundExpenseNo: row.fund_expense_no || '',
fundAccountId: row.fund_account_id ? Number(row.fund_account_id) : null,
fundAccountNo: row.fund_account_no || '',
fundAccountOwnerName: row.fund_account_owner_name || '',
costItemId: row.cost_item_id ? Number(row.cost_item_id) : null,
costItemName: row.cost_item_name || '',
costItemCode: row.cost_item_code || '',
@@ -1021,6 +1076,58 @@ function inferExpenseCategory(costType: string | undefined) {
return 'other'
}
function mapFundExpenseCategory(category: string): string {
switch (category) {
case 'goods_purchase':
return 'purchase'
case 'compensation':
return 'compensation'
case 'freight':
return 'freight'
case 'recharge':
return 'platform_recharge'
default:
return 'other'
}
}
function mapFundPayeeType(payeeType: string): string {
switch (payeeType) {
case 'worker':
case 'upstream':
return 'supplier'
case 'customer':
return 'customer'
case 'platform':
return 'platform'
default:
return 'other'
}
}
function toFundEvidence(evidence: JsonObject): JsonObject[] {
const items: JsonObject[] = []
const urls = Array.isArray(evidence.urls) ? evidence.urls : []
for (const value of urls) {
const url = String(value || '').trim()
if (url) {
items.push({ url, filename: '人工支出凭证', contentType: 'image/*' })
}
}
const reference = String(evidence.reference || '').trim()
if (/^https?:\/\//i.test(reference) && !items.some((item) => item.url === reference)) {
items.push({ url: reference, filename: '人工支出凭证', contentType: 'image/*' })
}
return items.slice(0, 6)
}
function buildFundExpenseNote(note: string, reference: string, expenseNo: string): string {
const parts = [`人工支出 ${expenseNo}`]
if (reference) parts.push(`凭证:${reference}`)
if (note) parts.push(note)
return parts.join(' · ').slice(0, 500)
}
function normalizeEvidence(value: unknown): JsonObject {
if (value && typeof value === 'object' && !Array.isArray(value)) {
return value as JsonObject