Files
yunhaifinance/backend/routes/payment-execution.js
T

500 lines
16 KiB
JavaScript

/**
* 付款执行统一路由
* 遵循设计方案:采购-付款-物流-退库一体化流程设计方案.md
* 章节:七、付款执行统一页面
*
* 功能:
* - 整合所有支付类型:材料采购、一次运费、二次运费、预支、报销等
* - 执行付款时必须上传付款凭证
* - 统一的付款执行列表和操作界面
*/
const express = require('express');
const db = require('../db');
const router = express.Router();
/**
* 获取待执行付款列表
* 整合所有支付类型
*/
router.get('/pending', async (req, res) => {
try {
const { payment_type } = req.query;
const results = [];
// 1. 材料采购付款(来自付款计划)
if (!payment_type || payment_type === 'material') {
const materialPayments = await db.query(`
SELECT
'material' as payment_type,
pp.id as source_id,
pp.stage as description,
pp.planned_amount as amount,
po.currency,
pp.planned_date as due_date,
s.name as payee_name,
po.code as order_code,
pp.status,
'付款计划' as source_type
FROM payment_plans pp
LEFT JOIN purchase_orders po ON pp.purchase_order_id = po.id
LEFT JOIN suppliers s ON po.supplier_id = s.id
WHERE pp.status IN ('pending', 'requested')
`);
results.push(...materialPayments.rows);
}
// 2. 一次运费付款
if (!payment_type || payment_type === 'primary_freight') {
const primaryFreight = await db.query(`
SELECT
'primary_freight' as payment_type,
lr.id as source_id,
'一次运费' as description,
lr.primary_freight as amount,
lr.primary_freight_currency as currency,
lr.ship_date as due_date,
lc.name as payee_name,
po.code as order_code,
lr.primary_freight_status as status,
'物流单' as source_type
FROM logistics_records lr
LEFT JOIN purchase_orders po ON lr.purchase_order_id = po.id
LEFT JOIN logistics_companies lc ON lr.logistics_company_id = lc.id
WHERE lr.primary_freight_status IN ('pending', 'requested')
AND lr.primary_freight > 0
`);
results.push(...primaryFreight.rows);
}
// 3. 二次运费付款
if (!payment_type || payment_type === 'secondary_freight') {
const secondaryFreight = await db.query(`
SELECT
'secondary_freight' as payment_type,
lr.id as source_id,
'二次运费' as description,
lr.secondary_freight as amount,
lr.secondary_freight_currency as currency,
lr.second_ship_date as due_date,
lr.driver_phone as payee_name,
po.code as order_code,
lr.secondary_freight_status as status,
'物流单' as source_type
FROM logistics_records lr
LEFT JOIN purchase_orders po ON lr.purchase_order_id = po.id
WHERE lr.secondary_freight_status IN ('pending', 'requested')
AND lr.secondary_freight > 0
`);
results.push(...secondaryFreight.rows);
}
// 4. 预支款
if (!payment_type || payment_type === 'advance') {
const advances = await db.query(`
SELECT
'advance' as payment_type,
a.id as source_id,
a.purpose as description,
a.amount,
a.currency,
a.request_date as due_date,
a.applicant as payee_name,
NULL as order_code,
a.status,
'预支申请' as source_type
FROM advances a
WHERE a.status = 'approved'
`);
results.push(...advances.rows);
}
// 5. 报销
if (!payment_type || payment_type === 'reimbursement') {
const reimbursements = await db.query(`
SELECT
'reimbursement' as payment_type,
r.id as source_id,
r.description,
r.total_amount as amount,
r.currency,
r.request_date as due_date,
r.applicant as payee_name,
NULL as order_code,
r.status,
'报销申请' as source_type
FROM reimbursements r
WHERE r.status = 'approved'
`);
results.push(...reimbursements.rows);
}
// 按日期排序
results.sort((a, b) => {
if (!a.due_date) return 1;
if (!b.due_date) return -1;
return new Date(a.due_date) - new Date(b.due_date);
});
res.json({
success: true,
data: results,
count: results.length
});
} catch (error) {
console.error('获取待执行付款列表失败:', error);
res.status(500).json({
success: false,
message: '获取待执行付款列表失败',
error: error.message
});
}
});
/**
* 获取已执行付款记录
*/
router.get('/executed', async (req, res) => {
try {
const results = [];
// 1. 材料采购付款记录
const materialPayments = await db.query(`
SELECT
'material' as payment_type,
pp.id as source_id,
pp.stage as description,
pp.actual_amount as amount,
po.currency,
pp.actual_date as payment_date,
s.name as payee_name,
po.code as order_code,
'已支付' as status,
'付款计划' as source_type
FROM payment_plans pp
LEFT JOIN purchase_orders po ON pp.purchase_order_id = po.id
LEFT JOIN suppliers s ON po.supplier_id = s.id
WHERE pp.status = 'paid'
`);
results.push(...materialPayments.rows);
// 2. 一次运费付款记录
const primaryFreight = await db.query(`
SELECT
'primary_freight' as payment_type,
lr.id as source_id,
'一次运费' as description,
lr.primary_freight as amount,
lr.primary_freight_currency as currency,
lr.final_arrival_date as payment_date,
lc.name as payee_name,
po.code as order_code,
'已支付' as status,
'物流单' as source_type
FROM logistics_records lr
LEFT JOIN purchase_orders po ON lr.purchase_order_id = po.id
LEFT JOIN logistics_companies lc ON lr.logistics_company_id = lc.id
WHERE lr.primary_freight_status = 'paid'
`);
results.push(...primaryFreight.rows);
// 3. 二次运费付款记录
const secondaryFreight = await db.query(`
SELECT
'secondary_freight' as payment_type,
lr.id as source_id,
'二次运费' as description,
lr.secondary_freight as amount,
lr.secondary_freight_currency as currency,
lr.final_arrival_date as payment_date,
lr.driver_phone as payee_name,
po.code as order_code,
'已支付' as status,
'物流单' as source_type
FROM logistics_records lr
LEFT JOIN purchase_orders po ON lr.purchase_order_id = po.id
WHERE lr.secondary_freight_status = 'paid'
`);
results.push(...secondaryFreight.rows);
// 按日期排序(最新的在前)
results.sort((a, b) => {
if (!a.payment_date) return 1;
if (!b.payment_date) return -1;
return new Date(b.payment_date) - new Date(a.payment_date);
});
res.json({
success: true,
data: results,
count: results.length
});
} catch (error) {
console.error('获取已执行付款记录失败:', error);
res.status(500).json({
success: false,
message: '获取已执行付款记录失败',
error: error.message
});
}
});
/**
* 执行付款
* 遵循设计方案:必须上传付款凭证
*/
router.post('/execute', async (req, res) => {
try {
const { payment_type, source_id, amount, payment_date, voucher_url, remark, payee_account } = req.body;
if (!payment_type || !source_id) {
return res.status(400).json({ success: false, message: '缺少付款类型或来源ID' });
}
if (!voucher_url) {
return res.status(400).json({ success: false, message: '执行付款必须上传付款凭证' });
}
await db.query('BEGIN TRANSACTION');
try {
const paymentDate = payment_date || new Date().toISOString().slice(0, 10);
switch (payment_type) {
case 'material':
await db.query(`
UPDATE payment_plans
SET status = 'paid', actual_amount = ?, actual_date = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`, [amount, paymentDate, source_id]);
const planResult = await db.query('SELECT purchase_order_id FROM payment_plans WHERE id = $1', [source_id]);
if (planResult.rows.length > 0) {
const orderId = planResult.rows[0].purchase_order_id;
const orderStats = await db.query(`
SELECT
SUM(CASE WHEN status = 'paid' THEN COALESCE(actual_amount, 0) ELSE 0 END) as paid_amount,
SUM(planned_amount) as total_amount
FROM payment_plans WHERE purchase_order_id = ?
`, [orderId]);
const { paid_amount, total_amount } = orderStats.rows[0];
let newStatus = 'confirmed';
if (paid_amount >= total_amount) {
newStatus = 'paid';
} else if (paid_amount > 0) {
newStatus = 'partial_paid';
}
await db.query(`
UPDATE purchase_orders SET paid_amount = ?, status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?
`, [paid_amount, newStatus, orderId]);
}
break;
case 'primary_freight':
await db.query(`
UPDATE logistics_records
SET primary_freight_status = 'paid', primary_freight_document = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`, [voucher_url, source_id]);
break;
case 'secondary_freight':
await db.query(`
UPDATE logistics_records
SET secondary_freight_status = 'paid', updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`, [source_id]);
break;
case 'advance':
await db.query(`
UPDATE advances SET status = 'paid', updated_at = CURRENT_TIMESTAMP WHERE id = ?
`, [source_id]);
break;
case 'reimbursement':
await db.query(`
UPDATE reimbursements SET status = 'paid', updated_at = CURRENT_TIMESTAMP WHERE id = ?
`, [source_id]);
break;
default:
throw new Error('未知的付款类型');
}
const recordCode = 'PAY-REC-' + Date.now();
await db.query(`
INSERT INTO payment_records
(code, payment_type, source_id, amount, currency, payment_date, voucher_url, payee_account, remark, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
`, [recordCode, payment_type, source_id, amount, 'CNY', paymentDate, voucher_url, payee_account, remark]);
await db.query('COMMIT');
res.json({
success: true,
message: '付款执行成功',
data: { record_code: recordCode }
});
} catch (innerError) {
await db.query('ROLLBACK');
throw innerError;
}
} catch (error) {
console.error('执行付款失败:', error);
res.status(500).json({
success: false,
message: '执行付款失败',
error: error.message
});
}
});
/**
* 获取付款详情
*/
router.get('/detail/:payment_type/:source_id', async (req, res) => {
try {
const { payment_type, source_id } = req.params;
let result;
switch (payment_type) {
case 'material':
result = await db.query(`
SELECT pp.*, po.code as order_code, s.name as supplier_name, s.country as supplier_country
FROM payment_plans pp
LEFT JOIN purchase_orders po ON pp.purchase_order_id = po.id
LEFT JOIN suppliers s ON po.supplier_id = s.id
WHERE pp.id = ?
`, [source_id]);
break;
case 'primary_freight':
case 'secondary_freight':
result = await db.query(`
SELECT lr.*, po.code as order_code, lc.name as logistics_company_name
FROM logistics_records lr
LEFT JOIN purchase_orders po ON lr.purchase_order_id = po.id
LEFT JOIN logistics_companies lc ON lr.logistics_company_id = lc.id
WHERE lr.id = ?
`, [source_id]);
break;
case 'advance':
result = await db.query('SELECT * FROM advances WHERE id = $1', [source_id]);
break;
case 'reimbursement':
result = await db.query('SELECT * FROM reimbursements WHERE id = $1', [source_id]);
break;
default:
return res.status(400).json({ success: false, message: '未知的付款类型' });
}
if (result.rows.length === 0) {
return res.status(404).json({ success: false, message: '付款记录不存在' });
}
res.json({
success: true,
data: result.rows[0]
});
} catch (error) {
console.error('获取付款详情失败:', error);
res.status(500).json({
success: false,
message: '获取付款详情失败',
error: error.message
});
}
});
/**
* 获取付款统计
*/
router.get('/statistics', async (req, res) => {
try {
const stats = {
pending_count: 0,
pending_amount: 0,
executed_count: 0,
executed_amount: 0,
by_type: {}
};
// 统计待执行付款
const pendingResult = await db.query(`
SELECT payment_type, COUNT(*) as count, SUM(amount) as total_amount
FROM (
SELECT 'material' as payment_type, pp.planned_amount as amount
FROM payment_plans pp WHERE pp.status IN ('pending', 'requested')
UNION ALL
SELECT 'primary_freight', lr.primary_freight
FROM logistics_records lr WHERE lr.primary_freight_status IN ('pending', 'requested') AND lr.primary_freight > 0
UNION ALL
SELECT 'secondary_freight', lr.secondary_freight
FROM logistics_records lr WHERE lr.secondary_freight_status IN ('pending', 'requested') AND lr.secondary_freight > 0
UNION ALL
SELECT 'advance', a.amount
FROM advances a WHERE a.status = 'approved'
UNION ALL
SELECT 'reimbursement', r.total_amount
FROM reimbursements r WHERE r.status = 'approved'
)
GROUP BY payment_type
`);
for (const row of pendingResult.rows) {
stats.pending_count += row.count;
stats.pending_amount += row.total_amount || 0;
stats.by_type[row.payment_type] = {
pending_count: row.count,
pending_amount: row.total_amount || 0
};
}
// 统计已执行付款
const executedResult = await db.query(`
SELECT payment_type, COUNT(*) as count, SUM(amount) as total_amount
FROM (
SELECT 'material' as payment_type, pp.actual_amount as amount
FROM payment_plans pp WHERE pp.status = 'paid'
UNION ALL
SELECT 'primary_freight', lr.primary_freight
FROM logistics_records lr WHERE lr.primary_freight_status = 'paid'
UNION ALL
SELECT 'secondary_freight', lr.secondary_freight
FROM logistics_records lr WHERE lr.secondary_freight_status = 'paid'
)
GROUP BY payment_type
`);
for (const row of executedResult.rows) {
stats.executed_count += row.count;
stats.executed_amount += row.total_amount || 0;
if (!stats.by_type[row.payment_type]) {
stats.by_type[row.payment_type] = {};
}
stats.by_type[row.payment_type].executed_count = row.count;
stats.by_type[row.payment_type].executed_amount = row.total_amount || 0;
}
res.json({
success: true,
data: stats
});
} catch (error) {
console.error('获取付款统计失败:', error);
res.status(500).json({
success: false,
message: '获取付款统计失败',
error: error.message
});
}
});
module.exports = router;