feat(finance): link bill lines to inbound details
This commit is contained in:
@@ -34,6 +34,7 @@ export interface FinanceBillLineWire {
|
|||||||
sourceVersion: number
|
sourceVersion: number
|
||||||
orderId: WireScalar | null
|
orderId: WireScalar | null
|
||||||
inboundId: WireScalar | null
|
inboundId: WireScalar | null
|
||||||
|
inboundCode: string | null
|
||||||
feeItemCode: string
|
feeItemCode: string
|
||||||
feeItemName: string
|
feeItemName: string
|
||||||
amount: WireScalar
|
amount: WireScalar
|
||||||
@@ -89,6 +90,7 @@ function mapBillLine(line: FinanceBillLineWire): FinanceBillLine {
|
|||||||
return {
|
return {
|
||||||
id: text(line.id),
|
id: text(line.id),
|
||||||
orderId: line.orderId == null ? null : text(line.orderId),
|
orderId: line.orderId == null ? null : text(line.orderId),
|
||||||
|
inboundCode: line.inboundCode?.trim() || null,
|
||||||
feeNo: `${line.sourceType}:${text(line.sourceId)}/v${line.sourceVersion}`,
|
feeNo: `${line.sourceType}:${text(line.sourceId)}/v${line.sourceVersion}`,
|
||||||
feeName: line.feeItemName,
|
feeName: line.feeItemName,
|
||||||
amount: text(line.amount),
|
amount: text(line.amount),
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export interface FinanceCustomerConfirmationResult {
|
|||||||
export interface FinanceBillLine {
|
export interface FinanceBillLine {
|
||||||
id: string
|
id: string
|
||||||
orderId: string | null
|
orderId: string | null
|
||||||
|
inboundCode: string | null
|
||||||
feeNo: string | null
|
feeNo: string | null
|
||||||
feeName: string
|
feeName: string
|
||||||
amount: string
|
amount: string
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ test('maps nested bill detail and customer-safe receipt allocations', () => {
|
|||||||
receivedAmount: '40.00', outstandingAmount: '60.00',
|
receivedAmount: '40.00', outstandingAmount: '60.00',
|
||||||
version: 3, customerConfirmationStatus: 'PENDING_CONFIRMATION', customerConfirmedByName: null, customerConfirmedAt: null,
|
version: 3, customerConfirmationStatus: 'PENDING_CONFIRMATION', customerConfirmedByName: null, customerConfirmedAt: null,
|
||||||
},
|
},
|
||||||
lines: [{ id: 21, sourceType: 'INBOUND_FEE', sourceId: 81, sourceVersion: 3, orderId: 31, inboundId: 41, feeItemCode: 'FREIGHT', feeItemName: '运费', amount: '100.00', currency: 'CNY' }],
|
lines: [{ id: 21, sourceType: 'INBOUND_FEE', sourceId: 81, sourceVersion: 3, orderId: 31, inboundId: 41, inboundCode: 'NYEXX2608180003', feeItemCode: 'FREIGHT', feeItemName: '运费', amount: '100.00', currency: 'CNY' }],
|
||||||
})
|
})
|
||||||
const receipts = financeAdapters.mapFinanceReceipts([{
|
const receipts = financeAdapters.mapFinanceReceipts([{
|
||||||
id: 51, receiptNo: 'RCPT-51', receivedAmount: '40.00', receivedAt: '2026-08-12T10:00:00',
|
id: 51, receiptNo: 'RCPT-51', receivedAmount: '40.00', receivedAt: '2026-08-12T10:00:00',
|
||||||
@@ -101,7 +101,7 @@ test('maps nested bill detail and customer-safe receipt allocations', () => {
|
|||||||
}], '2')
|
}], '2')
|
||||||
|
|
||||||
assert.equal(detail.billNo, 'B-2')
|
assert.equal(detail.billNo, 'B-2')
|
||||||
assert.deepEqual(detail.lines[0], { id: '21', orderId: '31', feeNo: 'INBOUND_FEE:81/v3', feeName: '运费', amount: '100.00', currencyCode: 'CNY' })
|
assert.deepEqual(detail.lines[0], { id: '21', orderId: '31', inboundCode: 'NYEXX2608180003', feeNo: 'INBOUND_FEE:81/v3', feeName: '运费', amount: '100.00', currencyCode: 'CNY' })
|
||||||
assert.deepEqual(receipts[0], { id: '51', receiptNo: 'RCPT-51', receivedAt: '2026-08-12T10:00:00', amount: '40.00', currencyCode: 'CNY', method: 'BANK_TRANSFER', allocatedAmount: '40.00' })
|
assert.deepEqual(receipts[0], { id: '51', receiptNo: 'RCPT-51', receivedAt: '2026-08-12T10:00:00', amount: '40.00', currencyCode: 'CNY', method: 'BANK_TRANSFER', allocatedAmount: '40.00' })
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -122,6 +122,10 @@ test('keeps actions first and exposes list, balance, detail, confirmation, recei
|
|||||||
}
|
}
|
||||||
assert.match(viewSource, /BANK_TRANSFER: '银行转账'/)
|
assert.match(viewSource, /BANK_TRANSFER: '银行转账'/)
|
||||||
assert.match(viewSource, /paymentMethodLabel\(row\.method\)/)
|
assert.match(viewSource, /paymentMethodLabel\(row\.method\)/)
|
||||||
|
assert.match(viewSource, /label="入仓码"/)
|
||||||
|
assert.match(viewSource, /openInboundAssociation\(row\.inboundCode\)/)
|
||||||
|
assert.match(viewSource, /InboundCodeAssociationDialog/)
|
||||||
|
assert.match(viewSource, /:inbound-code="inboundAssociationCode"/)
|
||||||
assert.match(viewSource, /auth\.hasPermission\('finance:bill:confirm'\)/)
|
assert.match(viewSource, /auth\.hasPermission\('finance:bill:confirm'\)/)
|
||||||
assert.match(viewSource, /row\.customerConfirmationStatus === 'PENDING_CONFIRMATION' && canConfirm/)
|
assert.match(viewSource, /row\.customerConfirmationStatus === 'PENDING_CONFIRMATION' && canConfirm/)
|
||||||
assert.match(viewSource, /size="small" type="success" plain/)
|
assert.match(viewSource, /size="small" type="success" plain/)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { CircleDollarSign, Eye, RefreshCcw, Search } from '@lucide/vue'
|
|||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { ApiError } from '@/api/http'
|
import { ApiError } from '@/api/http'
|
||||||
import { useAuthStore } from '@/modules/auth/public'
|
import { useAuthStore } from '@/modules/auth/public'
|
||||||
|
import { InboundCodeAssociationDialog } from '@/modules/first-leg/public'
|
||||||
import { confirmMerchantFinanceStatement, getMerchantFinanceBill, getMerchantFinanceBills, getMerchantFinanceReceipts } from '../api/finance'
|
import { confirmMerchantFinanceStatement, getMerchantFinanceBill, getMerchantFinanceBills, getMerchantFinanceReceipts } from '../api/finance'
|
||||||
import type { FinanceBillDetail, FinanceBillQuery, FinanceBillStatus, FinanceBillSummary, FinanceCustomerConfirmationStatus } from '../contracts'
|
import type { FinanceBillDetail, FinanceBillQuery, FinanceBillStatus, FinanceBillSummary, FinanceCustomerConfirmationStatus } from '../contracts'
|
||||||
|
|
||||||
@@ -16,6 +17,8 @@ const bills = ref<FinanceBillSummary[]>([])
|
|||||||
const detail = ref<FinanceBillDetail | null>(null)
|
const detail = ref<FinanceBillDetail | null>(null)
|
||||||
const selectedBillId = ref('')
|
const selectedBillId = ref('')
|
||||||
const detailVisible = ref(false)
|
const detailVisible = ref(false)
|
||||||
|
const inboundAssociationVisible = ref(false)
|
||||||
|
const inboundAssociationCode = ref('')
|
||||||
const confirmingBillId = ref('')
|
const confirmingBillId = ref('')
|
||||||
const confirmationKeys = new Map<string, string>()
|
const confirmationKeys = new Map<string, string>()
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
@@ -160,6 +163,13 @@ function handlePageChange(nextPage: number) {
|
|||||||
void loadBills(nextPage)
|
void loadBills(nextPage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openInboundAssociation(inboundCode: string | null) {
|
||||||
|
const code = inboundCode?.trim()
|
||||||
|
if (!code) return
|
||||||
|
inboundAssociationCode.value = code
|
||||||
|
inboundAssociationVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => { void loadBills(1) })
|
onMounted(() => { void loadBills(1) })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -211,12 +221,13 @@ onMounted(() => { void loadBills(1) })
|
|||||||
<template v-else-if="detail">
|
<template v-else-if="detail">
|
||||||
<div class="detail-heading"><div><h2>{{ detail.billNo }}</h2><p>出账 {{ formatDate(detail.issueDate) }} · 到期 {{ formatDate(detail.dueDate) }} · 未收 <strong class="amount-warning">¥ {{ formatAmount(detail.outstandingAmount) }}</strong></p></div><div class="detail-statuses"><el-tag :type="statusType(detail.status)">{{ statusLabel(detail.status) }}</el-tag><el-tag :type="confirmationType(detail.customerConfirmationStatus)">{{ confirmationLabel(detail.customerConfirmationStatus) }}</el-tag></div></div>
|
<div class="detail-heading"><div><h2>{{ detail.billNo }}</h2><p>出账 {{ formatDate(detail.issueDate) }} · 到期 {{ formatDate(detail.dueDate) }} · 未收 <strong class="amount-warning">¥ {{ formatAmount(detail.outstandingAmount) }}</strong></p></div><div class="detail-statuses"><el-tag :type="statusType(detail.status)">{{ statusLabel(detail.status) }}</el-tag><el-tag :type="confirmationType(detail.customerConfirmationStatus)">{{ confirmationLabel(detail.customerConfirmationStatus) }}</el-tag></div></div>
|
||||||
<section class="confirmation-panel" aria-label="客户确认信息"><div><strong>{{ confirmationLabel(detail.customerConfirmationStatus) }}</strong><p v-if="detail.customerConfirmationStatus === 'CONFIRMED'">{{ detail.customerConfirmedByName || '当前企业成员' }} · {{ formatDateTime(detail.customerConfirmedAt) }}</p><p v-else>请核对本期金额和费用明细,确认后将记录当前登录成员与确认时间。</p></div><el-button v-if="detail.customerConfirmationStatus === 'PENDING_CONFIRMATION' && canConfirm" type="primary" :loading="confirmingBillId === detail.id" :disabled="Boolean(confirmingBillId)" @click="confirmStatement(detail)">确认账单</el-button></section>
|
<section class="confirmation-panel" aria-label="客户确认信息"><div><strong>{{ confirmationLabel(detail.customerConfirmationStatus) }}</strong><p v-if="detail.customerConfirmationStatus === 'CONFIRMED'">{{ detail.customerConfirmedByName || '当前企业成员' }} · {{ formatDateTime(detail.customerConfirmedAt) }}</p><p v-else>请核对本期金额和费用明细,确认后将记录当前登录成员与确认时间。</p></div><el-button v-if="detail.customerConfirmationStatus === 'PENDING_CONFIRMATION' && canConfirm" type="primary" :loading="confirmingBillId === detail.id" :disabled="Boolean(confirmingBillId)" @click="confirmStatement(detail)">确认账单</el-button></section>
|
||||||
<section class="detail-section"><h3>费用明细</h3><el-table :data="detail.lines" size="small"><el-table-column prop="feeName" label="费用项目" min-width="180" /><el-table-column label="金额" width="130" align="right"><template #default="{ row }">¥ {{ formatAmount(row.amount) }}</template></el-table-column></el-table></section>
|
<section class="detail-section"><h3>费用明细</h3><el-table :data="detail.lines" size="small"><el-table-column label="入仓码" min-width="170"><template #default="{ row }"><el-button v-if="row.inboundCode" link type="primary" :aria-label="`查看入仓码 ${row.inboundCode} 详情`" @click="openInboundAssociation(row.inboundCode)">{{ row.inboundCode }}</el-button><span v-else>—</span></template></el-table-column><el-table-column prop="feeName" label="费用项目" min-width="180" /><el-table-column label="金额" width="130" align="right"><template #default="{ row }">¥ {{ formatAmount(row.amount) }}</template></el-table-column></el-table></section>
|
||||||
<section class="detail-section"><h3>收款记录</h3><el-table v-if="detail.receipts.length" :data="detail.receipts" size="small"><el-table-column prop="receivedAt" label="收款日期" width="110"><template #default="{ row }">{{ formatDate(row.receivedAt) }}</template></el-table-column><el-table-column prop="receiptNo" label="收款编号" min-width="130" /><el-table-column label="本次金额" width="130" align="right"><template #default="{ row }">¥ {{ formatAmount(row.allocatedAmount) }}</template></el-table-column><el-table-column label="方式" min-width="100"><template #default="{ row }">{{ paymentMethodLabel(row.method) }}</template></el-table-column></el-table><div v-else class="sub-empty">暂无收款记录</div></section>
|
<section class="detail-section"><h3>收款记录</h3><el-table v-if="detail.receipts.length" :data="detail.receipts" size="small"><el-table-column prop="receivedAt" label="收款日期" width="110"><template #default="{ row }">{{ formatDate(row.receivedAt) }}</template></el-table-column><el-table-column prop="receiptNo" label="收款编号" min-width="130" /><el-table-column label="本次金额" width="130" align="right"><template #default="{ row }">¥ {{ formatAmount(row.allocatedAmount) }}</template></el-table-column><el-table-column label="方式" min-width="100"><template #default="{ row }">{{ paymentMethodLabel(row.method) }}</template></el-table-column></el-table><div v-else class="sub-empty">暂无收款记录</div></section>
|
||||||
<p class="readonly-notice">如对账单内容有疑问,请先线下联系平台财务,核对无误后再确认。</p>
|
<p class="readonly-notice">如对账单内容有疑问,请先线下联系平台财务,核对无误后再确认。</p>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</el-drawer>
|
</el-drawer>
|
||||||
|
<InboundCodeAssociationDialog v-model="inboundAssociationVisible" :inbound-code="inboundAssociationCode" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
export { useFirstLegOrdersStore } from './store/firstLegOrders'
|
export { useFirstLegOrdersStore } from './store/firstLegOrders'
|
||||||
export { getFirstLegStatusLabel, getFirstLegStatusTone } from './utils/firstLegOrder'
|
export { getFirstLegStatusLabel, getFirstLegStatusTone } from './utils/firstLegOrder'
|
||||||
|
export { default as InboundCodeAssociationDialog } from './components/InboundCodeAssociationDialog.vue'
|
||||||
|
|||||||
Reference in New Issue
Block a user