diff --git a/src/modules/finance/api/finance.ts b/src/modules/finance/api/finance.ts index c9aaf947..8b5f7173 100644 --- a/src/modules/finance/api/finance.ts +++ b/src/modules/finance/api/finance.ts @@ -247,7 +247,11 @@ function normalizeFeeSourceSummary(item: RawResource): FinanceFeeSourceSummary { customerId: stringValue(item.customerId), customerName: stringValue(item.customerName), supplierId: nullableString(item.supplierId), supplierName: stringValue(item.supplierName), orderId: nullableString(item.orderId), inboundId: nullableString(item.inboundId), inboundCode: stringValue(item.inboundCode), containerId: nullableString(item.containerId), containerNo: stringValue(item.containerNo), businessDate: stringValue(item.businessDate), - feeItemName: stringValue(item.feeItemName), quantity: nullableNumber(item.quantity), unitPrice: nullableNumber(item.unitPrice), amount: numberValue(item.amount), currency: 'CNY', + feeItemName: stringValue(item.feeItemName), quantity: nullableNumber(item.quantity), quantityUnit: nullableString(item.quantityUnit), + unitPrice: nullableNumber(item.unitPrice), periodStart: nullableString(item.periodStart), periodEnd: nullableString(item.periodEnd), + amount: numberValue(item.amount), currency: 'CNY', originalCurrency: (stringValue(item.originalCurrency) || 'CNY') as 'CNY' | 'USD', + originalAmount: item.originalAmount == null ? numberValue(item.amount) : numberValue(item.originalAmount), + exchangeRate: item.exchangeRate == null ? 1 : numberValue(item.exchangeRate), exchangeRateMonth: nullableString(item.exchangeRateMonth), reviewStatus: stringValue(item.reviewStatus) as FinanceFeeSourceDetail['reviewStatus'], sourceStatus: stringValue(item.sourceStatus), status: stringValue(item.sourceStatus), statementGenerated: Boolean(item.statementGenerated), createdAt: stringValue(item.businessDate), diff --git a/src/modules/finance/pages/FinanceCustomerArapView.vue b/src/modules/finance/pages/FinanceCustomerArapView.vue index f6c10082..8dc9922b 100644 --- a/src/modules/finance/pages/FinanceCustomerArapView.vue +++ b/src/modules/finance/pages/FinanceCustomerArapView.vue @@ -114,7 +114,7 @@ const queryFields = computed(() => [ options: activeCustomers.value.map((item) => ({ label: item.tenantName, value: String(item.id) })), }, { key: 'direction', type: 'select', label: '收付方向', width: 140, placeholder: '请选择收付方向', options: [{ label: '应收', value: 'RECEIVABLE' }, { label: '应付', value: 'PAYABLE' }] }, - { key: 'status', type: 'select', label: '来源状态', width: 150, placeholder: '请选择来源状态', options: [{ label: '可结算', value: 'AVAILABLE' }, { label: '已占用', value: 'LOCKED' }, { label: '已生成对账单', value: 'BILLED' }, { label: '已作废', value: 'VOIDED' }, { label: '已被调整替代', value: 'SUPERSEDED' }] }, + { key: 'status', type: 'select', label: '来源状态', width: 150, placeholder: '请选择来源状态', options: [{ label: '可结算', value: 'AVAILABLE' }, { label: '已占用', value: 'LOCKED' }, { label: '已生成对账单', value: 'BILLED' }, { label: '资金账户已结清', value: 'SETTLED' }, { label: '已作废', value: 'VOIDED' }, { label: '已被调整替代', value: 'SUPERSEDED' }] }, { key: 'reviewStatus', type: 'select', label: '审核状态', width: 150, placeholder: '请选择审核状态', options: financeArapStatusOptions }, { key: 'statementGenerated', type: 'select', label: '已生成对账单', width: 160, placeholder: '请选择对账单状态', options: [{ label: '已生成', value: 'true' }, { label: '未生成', value: 'false' }] }, { key: 'businessRange', type: 'daterange', label: '业务日期', width: 280 }, @@ -149,7 +149,9 @@ function sourceTypeDisplay(value: unknown) { MANUAL_COST: '人工成本', STORAGE_FEE: '仓储费用', OVERSEAS_INBOUND_FEE: '海外入仓费用', OVERSEAS_PROCUREMENT_FEE: '海外采购费用', OVERSEAS_TRUCK_FEE: '本土卡车费用', OVERSEAS_LOOSEBOARD_FEE: '本土散板费用', - OVERSEAS_WAREHOUSE_FEE: '海外仓服务费', + OVERSEAS_WAREHOUSE_FEE: '海外仓服务费', OVERSEAS_CONTAINER_FEE: '海外柜子费用', + OVERSEAS_ORDER_FEE: '尾程订单费用', OVERSEAS_DELIVERY_FEE: '尾程派送费用', + OVERSEAS_RELABEL_FEE: '换标费用', } return labels[code] || (code ? '其他费用来源' : '来源类型暂不可用') } @@ -177,7 +179,7 @@ function businessReference(row: ArapRow) { } function supplierDisplay(row: ArapRow) { return row.direction === 'RECEIVABLE' ? '不适用' : row.supplierName.trim() || '供应商待补充' } function sourceStatusDisplay(value: unknown) { - const labels: Record = { AVAILABLE: '可结算', LOCKED: '已占用', BILLED: '已生成对账单', VOIDED: '已作废', SUPERSEDED: '已被调整替代' } + const labels: Record = { AVAILABLE: '可结算', LOCKED: '已占用', BILLED: '已生成对账单', SETTLED: '资金账户已结清', VOIDED: '已作废', SUPERSEDED: '已被调整替代' } return labels[String(value || '').toUpperCase()] || '状态待确认' } function formatDateTime(value: unknown) { @@ -384,9 +386,9 @@ watch(() => [route.name, String(route.params.feeSourceId || '')], ([name, feeSou
- + - +
diff --git a/src/modules/finance/types.ts b/src/modules/finance/types.ts index 490f07a9..94caae36 100644 --- a/src/modules/finance/types.ts +++ b/src/modules/finance/types.ts @@ -74,8 +74,15 @@ export interface FinanceFeeSourceSummary extends FinanceFeePoolItem { inboundCode: string containerNo: string businessDate: string - quantity: number | null - unitPrice: number | null + quantity?: number | null + quantityUnit?: string | null + unitPrice?: number | null + periodStart?: string | null + periodEnd?: string | null + originalCurrency?: 'CNY' | 'USD' + originalAmount?: number + exchangeRate?: number + exchangeRateMonth?: string | null reviewStatus: FinanceReviewStatus sourceStatus: string statementGenerated: boolean diff --git a/src/modules/overseas-warehouse/api/dispatches.test.ts b/src/modules/overseas-warehouse/api/dispatches.test.ts index 9a326fd7..6f0d730e 100644 --- a/src/modules/overseas-warehouse/api/dispatches.test.ts +++ b/src/modules/overseas-warehouse/api/dispatches.test.ts @@ -81,7 +81,7 @@ describe('dispatch overseas API', () => { it('transfers to delivery only by dispatchNo without customerId', async () => { vi.mocked(overseasHttp.post).mockResolvedValue({ code: '200', message: 'success', data: { dispatchNo: 'PC/1', outboundNo: 'OUT-1', deliveryNo: 'DEL-1' } }) - const payload = { idempotencyKey: 'transfer-1', plannedDeliveryAt: '2026-09-01T10:00:00', deliveryMethod: 'TRUCK_PALLET', pickupMethod: 'WAREHOUSE_PICKUP', loadingType: 'PALLET', fees: [{ feeType: 'DELIVERY_FEE', currency: 'USD', amount: 10 }] } + const payload = { idempotencyKey: 'transfer-1', plannedDeliveryAt: '2026-09-01T10:00:00', deliveryMethod: 'TRUCK_PALLET', pickupMethod: 'WAREHOUSE_PICKUP', loadingType: 'PALLET', fees: [{ feeType: 'DELIVERY_FEE', feeName: '派送费用', direction: 'RECEIVABLE' as const, currency: 'USD' as const, amount: 10 }] } await transferDispatchToDelivery('PC/1', payload) expect(overseasHttp.post).toHaveBeenCalledWith('/platform/v1/last-leg/dispatches/PC%2F1/transfer-delivery', payload) }) diff --git a/src/modules/overseas-warehouse/api/dispatches.ts b/src/modules/overseas-warehouse/api/dispatches.ts index 9fa660bb..e027f736 100644 --- a/src/modules/overseas-warehouse/api/dispatches.ts +++ b/src/modules/overseas-warehouse/api/dispatches.ts @@ -214,7 +214,11 @@ export interface TransferDeliveryPayload { appointmentAccount?: string sellerName?: string remark?: string - fees: Array<{ feeType: string; currency: string; amount: number; remark?: string }> + fees: Array<{ feeType: string; feeName: string; direction: 'RECEIVABLE' | 'PAYABLE'; currency: 'CNY' | 'USD'; amount: number + supplierId?: string; supplierName?: string; supplierCategory?: string; settlementMode?: 'ON_ACCOUNT' | 'DIRECT_PAID' + paidAt?: string; paymentMethod?: 'ONLINE' | 'BANK_TRANSFER' | 'CASH' | 'OTHER'; paymentReferenceNo?: string + paymentVoucherFileIds?: string[]; chargeableQuantity?: number; quantityUnit?: string; unitPrice?: number + periodStart?: string; periodEnd?: string; remark?: string }> } export async function transferDispatchToDelivery(dispatchNo: string, payload: TransferDeliveryPayload) { const response = await overseasHttp.post, ApiEnvelope>( diff --git a/src/modules/overseas-warehouse/api/lastMileOrders.ts b/src/modules/overseas-warehouse/api/lastMileOrders.ts index 601fc96b..f6f74abe 100644 --- a/src/modules/overseas-warehouse/api/lastMileOrders.ts +++ b/src/modules/overseas-warehouse/api/lastMileOrders.ts @@ -125,17 +125,14 @@ export interface LastMileStatusHistoryDto { } export interface LastMileFeeDto { - id: string - customerId: string - orderId: string - feeItem: string - currency: string - amount: number - status: string - supplierDescription?: string | null - createdBy: string - createdAt: string - updatedAt: string + id: string; sourceFeeId: string; sourceVersion: number; customerId: string; orderId?: string | null + feeItemCode: string; feeItemName: string; direction: 'RECEIVABLE' | 'PAYABLE' + currency: 'CNY'; amount: number; originalCurrency: 'CNY' | 'USD'; originalAmount: number + exchangeRate: number; exchangeRateMonth: string; reviewStatus: 'DRAFT' | 'PENDING' | 'APPROVED' | 'REJECTED' + publicationStatus: string; sourceStatus: string; supplierId?: string | null; supplierName?: string | null + settlementMode?: 'ON_ACCOUNT' | 'DIRECT_PAID' | null; businessDate: string + chargeableQuantity?: number | null; quantityUnit?: string | null; unitPrice?: number | null + periodStart?: string | null; periodEnd?: string | null; createdAt: string; updatedAt: string } export interface LastMileOperationDto { @@ -147,11 +144,11 @@ export interface LastMileOperationDto { } export interface CreateLastMileFeePayload { - feeItem: string - currency: string - amount: number - status: string - supplierDescription?: string + feeItemCode: string; feeItemName: string; direction: 'RECEIVABLE' | 'PAYABLE'; currency: 'CNY' | 'USD' + amount: number; businessDate: string; supplierId?: string; supplierName?: string; supplierCategory?: string + settlementMode?: 'ON_ACCOUNT' | 'DIRECT_PAID'; paidAt?: string; paymentMethod?: 'ONLINE' | 'BANK_TRANSFER' | 'CASH' | 'OTHER' + paymentReferenceNo?: string; paymentVoucherFileIds?: string[]; chargeableQuantity?: number + quantityUnit?: string; unitPrice?: number; periodStart?: string; periodEnd?: string } export interface TransferDestinationPayload { @@ -378,6 +375,7 @@ export async function downloadLastMileOrders(query: Partial) export async function createLastMileOrderFee(orderId: string, payload: CreateLastMileFeePayload) { const response = await overseasHttp.post, ApiEnvelope>( `/platform/v1/last-leg/orders/${encodeURIComponent(orderId)}/fees`, payload, + { headers: { 'Idempotency-Key': `last-mile-fee-${orderId}-${Date.now()}-${Math.random().toString(36).slice(2)}` } }, ) return unwrapApiResponse(response) } diff --git a/src/modules/overseas-warehouse/pages/DispatchesView.vue b/src/modules/overseas-warehouse/pages/DispatchesView.vue index f13727b2..521e651e 100644 --- a/src/modules/overseas-warehouse/pages/DispatchesView.vue +++ b/src/modules/overseas-warehouse/pages/DispatchesView.vue @@ -201,7 +201,7 @@ async function submitTransfer() { if (needsLoadingFee.value && transferForm.loadingFee == null) { ElMessage.warning('当前装车类型需要填写装车费'); return } if (isBuyAppointment.value && (!transferForm.sellerName.trim() || transferForm.appointmentFee == null)) { ElMessage.warning('买约时请填写卖家姓名和买约费用'); return } const feeInputs = [['DELIVERY_FEE', transferForm.deliveryFee, '派送费用'], ['LOADING_FEE', needsLoadingFee.value ? transferForm.loadingFee : undefined, '装车费'], ['APPOINTMENT_FEE', isBuyAppointment.value ? transferForm.appointmentFee : undefined, '买约费用']] as const - const fees = feeInputs.filter(([, amount]) => amount != null).map(([feeType, amount, remark]) => ({ feeType, currency: 'USD', amount: Number(amount), remark })) + const fees = feeInputs.filter(([, amount]) => amount != null).map(([feeType, amount, feeName]) => ({ feeType, feeName, direction: 'RECEIVABLE' as const, currency: 'USD' as const, amount: Number(amount), remark: feeName })) transferSaving.value = true try { await transferDispatchToDelivery(transferRow.value.dispatchNo, { idempotencyKey: `web-transfer-${transferRow.value.dispatchNo}-${Date.now()}`, plannedDeliveryAt: transferForm.plannedDeliveryAt, deliveryMethod: transferForm.deliveryMethod, waybillNo: transferForm.waybillNo.trim() || undefined, carrierName: transferForm.carrierName.trim() || undefined, pickupMethod: transferForm.pickupMethod, loadingType: transferForm.loadingType, vehicleType: transferForm.vehicleType || undefined, vehicleSize: transferForm.vehicleSize || undefined, driverName: transferForm.driverName.trim() || undefined, driverPhone: transferForm.driverPhone.trim() || undefined, vehiclePlate: transferForm.vehiclePlate.trim() || undefined, appointmentAccount: transferForm.appointmentAccount || undefined, sellerName: isBuyAppointment.value ? transferForm.sellerName.trim() : undefined, remark: transferForm.remark.trim() || undefined, fees }); transferOpen.value = false; ElMessage.success('已生成出库单和派送单'); await load() } catch (cause) { mutationError.value = getApiErrorDetails(cause, '转派送失败,请重试') } finally { transferSaving.value = false } } diff --git a/src/modules/overseas-warehouse/pages/LastMileOrderDetailView.vue b/src/modules/overseas-warehouse/pages/LastMileOrderDetailView.vue index 4ff14c13..1aa1314c 100644 --- a/src/modules/overseas-warehouse/pages/LastMileOrderDetailView.vue +++ b/src/modules/overseas-warehouse/pages/LastMileOrderDetailView.vue @@ -4,6 +4,7 @@ import { Plus } from '@lucide/vue' import { ElMessage } from 'element-plus' import { useRoute, useRouter } from 'vue-router' import { useAccessPreviewStore } from '@/core/access/accessPreview' +import { listSuppliers, type Supplier } from '@/modules/suppliers' import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http' import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue' import AppDialog from '@/shared/components/AppDialog.vue' @@ -33,8 +34,9 @@ const loadError = ref(null) const mutationError = ref(null) const activeTab = ref('basic') const feeDialogOpen = ref(false) -const feeForm = reactive({ item: '', currency: 'USD', amount: 0, status: 'PENDING_REVIEW', description: '' }) -const feeItemOptions = ['仓储费', '入库操作费', '出库操作费', '换标费', '托盘费', '派送费', '转仓费', '异常处理费', '其他'] +const supplierOptions = ref([]) +const feeForm = reactive({ itemCode: '', itemName: '', direction: 'RECEIVABLE' as 'RECEIVABLE' | 'PAYABLE', currency: 'USD' as 'CNY' | 'USD', amount: 0, businessDate: new Date().toISOString().slice(0, 10), supplierId: '', quantity: undefined as number | undefined, quantityUnit: '', periodStart: '', periodEnd: '' }) +const feeItemOptions = [{ code: 'STORAGE_FEE', name: '仓租' }, { code: 'INBOUND_OPERATION', name: '入库操作费' }, { code: 'OUTBOUND_OPERATION', name: '出库操作费' }, { code: 'RELABEL_FEE', name: '换标费' }, { code: 'REBOX_FEE', name: '换箱费' }, { code: 'PALLET_FEE', name: '托盘费' }, { code: 'DELIVERY_FEE', name: '派送费' }, { code: 'TRANSFER_FEE', name: '转仓费' }, { code: 'EXCEPTION_FEE', name: '异常处理费' }, { code: 'OTHER', name: '其他' }] function text(value: unknown) { return String(value ?? '').trim() || '—' } function yesNo(value: boolean | null | undefined) { return value == null ? '—' : value ? '是' : '否' } @@ -93,8 +95,8 @@ const cargo = computed(() => (detail.value?.cargo || []).map((item) cartons: Number(item.packageCount || 0), quantity: Number(item.productQuantity || 0), weight: Number(item.grossWeightKg || 0), volume: Number(item.volumeCbm || 0), }))) const fees = computed(() => (detail.value?.fees || []).map((item) => ({ - id: String(item.id), item: item.feeItem, currency: item.currency, amount: `${item.currency} ${Number(item.amount).toFixed(2)}`, - status: feeStatusLabel(item.status), description: text(item.supplierDescription), + id: String(item.id), item: item.feeItemName, currency: item.originalCurrency, amount: `${item.originalCurrency} ${Number(item.originalAmount).toFixed(2)}`, + status: feeStatusLabel(item.reviewStatus === 'PENDING' ? 'PENDING_REVIEW' : item.reviewStatus), description: text(item.supplierName || (item.chargeableQuantity == null ? null : `${item.chargeableQuantity} ${item.quantityUnit || ''}`)), }))) const logs = computed(() => { const statusRows = (detail.value?.statusHistory || []).map((item) => ({ @@ -134,16 +136,22 @@ async function load() { catch (error) { loadError.value = error instanceof Error && error.message === '当前环境未签发海外访问令牌' ? { message: error.message } : getApiErrorDetails(error, '尾程订单详情加载失败,请重试') } finally { loading.value = false } } -function openFeeDialog() { - Object.assign(feeForm, { item: '', currency: 'USD', amount: 0, status: 'PENDING_REVIEW', description: '' }) +async function openFeeDialog() { + Object.assign(feeForm, { itemCode: '', itemName: '', direction: 'RECEIVABLE', currency: 'USD', amount: 0, businessDate: new Date().toISOString().slice(0, 10), supplierId: '', quantity: undefined, quantityUnit: '', periodStart: '', periodEnd: '' }) mutationError.value = null; feeDialogOpen.value = true + if (access.can('platform:supplier:list') && supplierOptions.value.length === 0) { + try { supplierOptions.value = (await listSuppliers({ page: 1, size: 200, status: 'ACTIVE' })).records.filter((item) => item.status === 'ACTIVE') } + catch (error) { mutationError.value = getApiErrorDetails(error, '供应商列表加载失败') } + } } async function saveFee() { - if (!feeForm.item.trim() || feeForm.amount <= 0) { ElMessage.warning('请填写费用项目和大于 0 的金额'); return } + if (!feeForm.itemCode || !feeForm.itemName.trim() || feeForm.amount <= 0 || !feeForm.businessDate) { ElMessage.warning('请填写费用项目、业务日期和大于 0 的金额'); return } + const supplier = supplierOptions.value.find((item) => String(item.id) === feeForm.supplierId) + if (feeForm.direction === 'PAYABLE' && !supplier) { ElMessage.warning('应付费用必须选择供应商'); return } saving.value = true; mutationError.value = null try { - await createLastMileOrderFee(orderId.value, { feeItem: feeForm.item.trim(), currency: feeForm.currency, amount: feeForm.amount, status: feeForm.status, supplierDescription: feeForm.description.trim() || undefined }) - feeDialogOpen.value = false; ElMessage.success('费用已新增'); await load(); activeTab.value = 'fees' + await createLastMileOrderFee(orderId.value, { feeItemCode: feeForm.itemCode, feeItemName: feeForm.itemName.trim(), direction: feeForm.direction, currency: feeForm.currency, amount: feeForm.amount, businessDate: feeForm.businessDate, supplierId: supplier ? String(supplier.id) : undefined, supplierName: supplier?.name, supplierCategory: supplier?.category, settlementMode: supplier ? 'ON_ACCOUNT' : undefined, chargeableQuantity: feeForm.quantity, quantityUnit: feeForm.quantity == null ? undefined : feeForm.quantityUnit.trim(), unitPrice: feeForm.quantity ? feeForm.amount / feeForm.quantity : undefined, periodStart: feeForm.periodStart || undefined, periodEnd: feeForm.periodEnd || undefined }) + feeDialogOpen.value = false; ElMessage.success('费用已提交财务审核'); await load(); activeTab.value = 'fees' } catch (error) { mutationError.value = getApiErrorDetails(error, '费用新增失败,请重试') } finally { saving.value = false } } @@ -175,11 +183,14 @@ onMounted(load)
- - - + + + + + - + +