500 lines
16 KiB
JavaScript
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;
|