备份:修复前完整项目快照 2026-04-19

This commit is contained in:
root
2026-04-19 19:15:01 +08:00
parent 5266d7732b
commit d00f41a120
449 changed files with 89577 additions and 23251 deletions
+228
View File
@@ -0,0 +1,228 @@
const express = require('express');
const db = require('../db');
const { authenticate, requireAdmin } = require('../middleware/auth');
const { body, validationResult } = require('express-validator');
const router = express.Router();
// 验证错误处理中间件
const validate = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
errors: errors.array()
});
}
next();
};
router.get('/', async (req, res) => {
try {
const result = await db.query(`
SELECT a.*, u.name as user_name, p.name as project_name
FROM advances a
LEFT JOIN users u ON a.applicant_id = u.id
LEFT JOIN projects p ON a.project_id = p.id
ORDER BY a.created_at DESC
`);
// 解析每个预支申请的 attachments 字段为数组
const data = result.rows.map(item => {
if (item.attachments) {
try {
item.attachments = JSON.parse(item.attachments);
} catch (error) {
item.attachments = [];
}
} else {
item.attachments = [];
}
return item;
});
res.json({ success: true, data, count: data.length });
} catch (error) {
console.error('获取预支款失败:', error);
res.status(500).json({
success: false,
message: '获取预支款失败',
error: error.message
});
}
});
router.post('/', [
body('amount').isFloat({ min: 0.01 }),
body('reason').notEmpty()
], validate, async (req, res) => {
try {
const { amount, reason, project_id, currency, advance_date, attachments, amount_cny, applicant, status } = req.body;
const user_id = 1; // 临时使用admin用户
// 生成预支编号
const advanceCode = `ADV-${Date.now()}`;
const result = await db.query(
'INSERT INTO advances (applicant_id, project_id, amount, currency, amount_cny, reason, advance_date, advance_code, status, applicant, attachments) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)',
[applicant_id, project_id, amount, currency || 'CNY', amount_cny || 0, reason, advance_date, advanceCode, status || 'pending', applicant, JSON.stringify(attachments || [])]
);
// SQLite不支持RETURNING,所以需要查询刚插入的数据
const lastInsert = await db.query('SELECT * FROM advances ORDER BY id DESC LIMIT 1');
const data = lastInsert.rows[0];
// 解析 attachments 字段为数组
if (data.attachments) {
try {
data.attachments = JSON.parse(data.attachments);
} catch (error) {
data.attachments = [];
}
} else {
data.attachments = [];
}
res.json({ success: true, data });
} catch (error) {
console.error('创建预支申请失败:', error);
res.status(500).json({ success: false, message: '创建预支申请失败', error: error.message });
}
});
router.get('/:id', async (req, res) => {
try {
const { id } = req.params;
const result = await db.query('SELECT * FROM advances WHERE id = $1', [id]);
if (result.rows.length > 0) {
const data = result.rows[0];
// 解析 attachments 字段为数组
if (data.attachments) {
try {
data.attachments = JSON.parse(data.attachments);
} catch (error) {
data.attachments = [];
}
} else {
data.attachments = [];
}
res.json({ success: true, data });
} else {
res.status(404).json({ success: false, message: '预支申请不存在' });
}
} catch (error) {
console.error('获取预支申请失败:', error);
res.status(500).json({ success: false, message: '获取预支申请失败', error: error.message });
}
});
router.put('/:id', async (req, res) => {
try {
const { id } = req.params;
const { amount, reason, project_id, currency, advance_date, attachments, amount_cny, applicant, status } = req.body;
const result = await db.query(
'UPDATE advances SET amount = $1, reason = $2, project_id = $3, currency = $4, advance_date = $5, attachments = $6, amount_cny = $7, applicant = $8, status = $9 WHERE id = $10',
[amount, reason, project_id, currency, advance_date, JSON.stringify(attachments || []), amount_cny, applicant, status, id]
);
if (result.changes > 0) {
res.json({ success: true, message: '更新成功' });
} else {
res.status(404).json({ success: false, message: '预支申请不存在' });
}
} catch (error) {
console.error('更新预支申请失败:', error);
res.status(500).json({ success: false, message: '更新预支申请失败', error: error.message });
}
});
router.delete('/:id', async (req, res) => {
try {
const { id } = req.params;
const result = await db.query('DELETE FROM advances WHERE id = $1', [id]);
if (result.changes > 0) {
res.json({ success: true, message: '删除成功' });
} else {
res.status(404).json({ success: false, message: '预支申请不存在' });
}
} catch (error) {
console.error('删除预支申请失败:', error);
res.status(500).json({ success: false, message: '删除预支申请失败', error: error.message });
}
});
router.post('/:id/submit', async (req, res) => {
try {
const { id } = req.params;
const result = await db.query('UPDATE advances SET status = $1 WHERE id = $2', ['pending', id]);
if (result.changes > 0) {
res.json({ success: true, message: '提交成功' });
} else {
res.status(404).json({ success: false, message: '预支申请不存在' });
}
} catch (error) {
console.error('提交预支申请失败:', error);
res.status(500).json({ success: false, message: '提交预支申请失败', error: error.message });
}
});
router.post('/:id/withdraw', async (req, res) => {
try {
const { id } = req.params;
const result = await db.query('UPDATE advances SET status = $1 WHERE id = $2', ['withdrawn', id]);
if (result.changes > 0) {
res.json({ success: true, message: '撤回成功' });
} else {
res.status(404).json({ success: false, message: '预支申请不存在' });
}
} catch (error) {
console.error('撤回预支申请失败:', error);
res.status(500).json({ success: false, message: '撤回预支申请失败', error: error.message });
}
});
router.post('/:id/approve', async (req, res) => {
try {
const { id } = req.params;
const { remark } = req.body;
const result = await db.query('UPDATE advances SET status = $1, approval_remark = $2 WHERE id = $3', ['approved', remark, id]);
if (result.changes > 0) {
res.json({ success: true, message: '审批通过成功' });
} else {
res.status(404).json({ success: false, message: '预支申请不存在' });
}
} catch (error) {
console.error('审批预支申请失败:', error);
res.status(500).json({ success: false, message: '审批预支申请失败', error: error.message });
}
});
router.post('/:id/reject', async (req, res) => {
try {
const { id } = req.params;
const { rejectReason } = req.body;
const result = await db.query('UPDATE advances SET status = $1 WHERE id = $2', ['pending_edit', id]);
if (result.changes > 0) {
res.json({ success: true, message: '退回成功' });
} else {
res.status(404).json({ success: false, message: '预支申请不存在' });
}
} catch (error) {
console.error('退回预支申请失败:', error);
res.status(500).json({ success: false, message: '退回预支申请失败', error: error.message });
}
});
module.exports = router;
+87
View File
@@ -0,0 +1,87 @@
const express = require('express');
const router = express.Router();
const db = require('../db');
const { hashPassword, verifyPassword, generateToken } = require('../utils/auth');
const { authenticate } = require('../middleware/auth');
router.post('/login', async (req, res) => {
try {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({
success: false,
message: '用户名和密码不能为空'
});
}
const result = await db.query(
'SELECT id, username, name, email, phone, role, password_hash FROM users WHERE username = $1',
[username]
);
if (!result || result.rows.length === 0) {
return res.status(401).json({
success: false,
message: '用户名或密码错误'
});
}
const user = result.rows[0];
if (!user.password_hash || !verifyPassword(password, user.password_hash)) {
return res.status(401).json({
success: false,
message: '用户名或密码错误'
});
}
const token = generateToken({
id: user.id,
username: user.username,
role: user.role
});
console.log('用户 ' + username + ' 登录成功');
res.json({
success: true,
data: {
id: user.id,
username: user.username,
name: user.name,
email: user.email,
phone: user.phone,
role: user.role,
department: '',
token: token
}
});
} catch (error) {
console.error('登录失败:', error);
res.status(500).json({
success: false,
message: '登录失败',
error: error.message
});
}
});
router.get('/verify', authenticate, (req, res) => {
res.json({
success: true,
data: {
user: req.user
}
});
});
router.post('/logout', authenticate, (req, res) => {
console.log('用户 ' + req.user.username + ' 登出');
res.json({
success: true,
message: '登出成功'
});
});
module.exports = router;
+55
View File
@@ -0,0 +1,55 @@
const express = require('express');
const db = require('../db');
const { authenticate, requireAdmin } = require('../middleware/auth');
const router = express.Router();
router.get('/', async (req, res) => {
try {
const { customer_id } = req.query;
let query = `
SELECT b.*,
c.name as customer_name,
u.name as manager_name
FROM budget_projects b
LEFT JOIN customers c ON b.customer_id = c.id
LEFT JOIN users u ON b.project_manager_id = u.id
`;
const params = [];
if (customer_id) {
query += ` WHERE b.customer_id = $1`;
params.push(customer_id);
}
query += ` ORDER BY b.created_at DESC`;
const result = await db.query(query, params);
const projects = result.rows.map(project => {
try {
return {
...project,
attachments: project.attachments ? JSON.parse(project.attachments) : [],
survey_photos: project.survey_photos ? JSON.parse(project.survey_photos) : [],
quotations: []
};
} catch (error) {
console.error('解析项目数据失败:', error);
return {
...project,
attachments: [],
survey_photos: [],
quotations: []
};
}
});
res.json({ success: true, data: projects, count: projects.length });
} catch (error) {
console.error('获取预算项目失败:', error);
res.status(500).json({ success: false, message: error.message });
}
});
module.exports = router;
+109
View File
@@ -0,0 +1,109 @@
const express = require('express');
const db = require('../db');
const router = express.Router();
router.get('/tree', async (req, res) => {
try {
const result = await db.query('SELECT * FROM product_categories ORDER BY id');
const buildTree = (categories, parentId = null) => {
return categories
.filter(cat => cat.parent_id === parentId)
.map(cat => ({ ...cat, children: buildTree(categories, cat.id) }));
};
res.json({ success: true, data: buildTree(result.rows) });
} catch (error) {
console.error('获取分类树失败:', error);
res.status(500).json({ success: false, message: '获取分类树失败', error: error.message });
}
});
router.get('/', async (req, res) => {
try {
const { level } = req.query;
let query = 'SELECT * FROM product_categories';
const params = [];
if (level === '1') {
query += ' WHERE parent_id IS NULL';
} else if (level === '2') {
query += ' WHERE parent_id IS NOT NULL';
}
query += ' ORDER BY id';
const result = await db.query(query, params);
res.json({ success: true, data: result.rows });
} catch (error) {
console.error('获取分类失败:', error);
res.status(500).json({ success: false, message: '获取分类失败', error: error.message });
}
});
router.get('/:id', async (req, res) => {
try {
const { id } = req.params;
const result = await db.query('SELECT * FROM product_categories WHERE id = $1', [id]);
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.post('/', async (req, res) => {
try {
const { name, parent_id } = req.body;
if (!name) {
return res.status(400).json({ success: false, message: '分类名称不能为空' });
}
const result = await db.query(
'INSERT INTO product_categories (name, parent_id) VALUES ($1, $2) RETURNING *',
[name, parent_id || null]
);
res.json({ success: true, data: result.rows[0], message: '创建成功' });
} catch (error) {
console.error('创建分类失败:', error);
res.status(500).json({ success: false, message: '创建分类失败', error: error.message });
}
});
router.put('/:id', async (req, res) => {
try {
const { id } = req.params;
const { name, parent_id } = req.body;
const updates = [];
const params = [];
let i = 1;
if (name !== undefined) { updates.push(`name = $${i++}`); params.push(name); }
if (parent_id !== undefined) { updates.push(`parent_id = $${i++}`); params.push(parent_id || null); }
if (updates.length === 0) {
return res.status(400).json({ success: false, message: '没有提供更新数据' });
}
updates.push(`updated_at = CURRENT_TIMESTAMP`);
params.push(id);
const result = await db.query(`UPDATE product_categories SET ${updates.join(', ')} WHERE id = $${i} RETURNING *`, params);
if (result.rows.length === 0) {
return res.status(404).json({ success: false, message: '分类不存在' });
}
res.json({ success: true, data: result.rows[0], message: '更新成功' });
} catch (error) {
console.error('更新分类失败:', error);
res.status(500).json({ success: false, message: '更新分类失败', error: error.message });
}
});
router.delete('/:id', async (req, res) => {
try {
const { id } = req.params;
const result = await db.query('DELETE FROM product_categories WHERE id = $1 RETURNING id', [id]);
if (result.rows.length === 0) {
return res.status(404).json({ success: false, message: '分类不存在' });
}
res.json({ success: true, message: '删除成功' });
} catch (error) {
console.error('删除分类失败:', error);
res.status(500).json({ success: false, message: '删除分类失败', error: error.message });
}
});
module.exports = router;
+33
View File
@@ -0,0 +1,33 @@
const express = require('express');
const db = require('../db');
const { authenticate, requireAdmin } = require('../middleware/auth');
const router = express.Router();
router.get('/my-projects', async (req, res) => {
try {
const result = await db.query(`
SELECT p.*,
c.name as customer_name,
u.name as manager_name
FROM projects p
LEFT JOIN customers c ON p.customer_id = c.id
LEFT JOIN users u ON p.manager_id = u.id
WHERE p.status IN ('active', 'pending')
ORDER BY p.created_at DESC
`);
const projects = result.rows.map(project => ({
...project,
latest_log: null,
progress: 0
}));
res.json({ success: true, data: projects, count: projects.length });
} catch (error) {
console.error('获取施工项目失败:', error);
res.status(500).json({ success: false, message: error.message });
}
});
module.exports = router;
+326
View File
@@ -0,0 +1,326 @@
const express = require('express');
const db = require('../db');
const { authenticate, requireAdmin } = require('../middleware/auth');
const LedgerService = require('../services/ledgerService');
const router = express.Router();
router.get('/', async (req, res) => {
try {
const result = await db.query(`
SELECT * FROM customers
ORDER BY created_at DESC
LIMIT 50
`);
// 为每个客户获取联系人和收款信息
const customersWithDetails = await Promise.all(
result.rows.map(async (customer) => {
// 获取联系人信息
const contactsResult = await db.query(
`SELECT * FROM contacts WHERE entity_id = $1 AND entity_type = 'customer' ORDER BY is_primary DESC`,
[customer.id]
);
const contacts = contactsResult.rows.map(contact => ({
name: contact.name || '未命名',
position: contact.position || '',
phone: contact.phone || '',
is_primary: contact.is_primary === 1
}));
// 获取收款信息
const paymentInfosResult = await db.query(
`SELECT * FROM supplier_payment_infos WHERE supplier_id = $1 ORDER BY is_default DESC`,
[customer.id]
);
const paymentInfos = paymentInfosResult.rows.map(payment => ({
id: payment.id,
account_name: payment.account_name,
bank_account: payment.account_number,
bank_name: payment.bank_name,
qr_code: payment.qr_code,
is_primary: payment.is_default === 1
}));
return {
...customer,
contacts: contacts.length > 0 ? contacts : [],
payment_infos: paymentInfos.length > 0 ? paymentInfos : []
};
})
);
res.json({
success: true,
data: customersWithDetails,
count: customersWithDetails.length
});
} catch (error) {
console.error('获取客户失败:', error);
res.status(500).json({
success: false,
message: '获取客户失败',
error: error.message
});
}
});
router.get('/:id', async (req, res) => {
try {
const { id } = req.params;
// 获取客户基本信息
const customerResult = await db.query(`
SELECT * FROM customers
WHERE id = ?
`, [id]);
if (customerResult.rows.length > 0) {
const customer = customerResult.rows[0];
// 获取客户的所有联系人
const contactsResult = await db.query(`
SELECT * FROM contacts
WHERE entity_id = ? AND entity_type = 'customer'
ORDER BY is_primary DESC
`, [id]);
// 转换联系人数据结构
const contacts = contactsResult.rows.map(contact => ({
name: contact.name || '未命名',
position: contact.position || '',
phone: contact.phone || '',
is_primary: contact.is_primary === 1
}));
// 获取客户的所有收款信息
const paymentInfosResult = await db.query(`
SELECT * FROM supplier_payment_infos
WHERE supplier_id = ?
ORDER BY is_default DESC
`, [id]);
// 转换收款信息数据结构
const payment_infos = paymentInfosResult.rows.map(info => ({
id: info.id,
account_name: info.account_name || '',
bank_name: info.bank_name || '',
bank_account: info.account_number || '',
qr_code: info.qr_code || '',
is_primary: info.is_default === 1
}));
const ledger = await LedgerService.getCustomerLedger(id);
const formattedCustomer = {
id: customer.id,
code: `C${String(customer.id).padStart(4, '0')}`,
name: customer.name,
address: customer.address,
contacts: contacts.length > 0 ? contacts : [],
payment_infos: payment_infos.length > 0 ? payment_infos : [],
remark: customer.remark || '',
total_contract_amount: ledger.summary.total_contract_amount,
total_received: ledger.summary.total_received_amount,
total_receivable: ledger.summary.total_receivable_amount,
ledger: ledger,
created_at: customer.created_at
};
res.json({
success: true,
data: formattedCustomer
});
} else {
res.status(404).json({
success: false,
message: '客户不存在'
});
}
} catch (error) {
console.error('获取客户详情失败:', error);
res.status(500).json({
success: false,
message: '获取客户详情失败',
error: error.message
});
}
});
router.post('/', async (req, res) => {
try {
const { name, address, remark, contacts, payment_infos } = req.body;
// 从contacts中获取主联系人信息
const primaryContact = contacts?.find(c => c.is_primary) || contacts?.[0];
const contact = primaryContact?.name || '';
const position = primaryContact?.position || '';
const phone = primaryContact?.phone || '';
const email = ''; // 前端没有email字段
const result = await db.query(
`INSERT INTO customers (name, address, contact, position, phone, email, remark, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
[name, address, contact, position, phone, email, remark]
);
const customerId = (result.rows[0]?.id || result.rows?.[0]?.id);
// 插入联系人数据
if (contacts && contacts.length > 0) {
for (const contactItem of contacts) {
await db.query(
`INSERT INTO contacts (entity_id, entity_type, name, position, phone, is_primary, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
[customerId, 'customer', contactItem.name, contactItem.position, contactItem.phone, contactItem.is_primary ? 1 : 0]
);
}
}
// 插入收款信息数据
if (payment_infos && payment_infos.length > 0) {
for (const paymentItem of payment_infos) {
await db.query(
`INSERT INTO supplier_payment_infos (supplier_id, account_name, bank_name, bank_account, qr_code, is_primary, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
[customerId, paymentItem.account_name, paymentItem.bank_name, paymentItem.bank_account, paymentItem.qr_code, paymentItem.is_primary ? 1 : 0]
);
}
}
res.json({
success: true,
message: '客户创建成功',
data: {
id: customerId,
code: `C${String(customerId).padStart(4, '0')}`,
name,
address,
contacts: contacts || [],
payment_infos: payment_infos || [],
remark,
total_contract_amount: 0,
total_received: 0,
total_receivable: 0,
created_at: new Date().toISOString()
}
});
} catch (error) {
console.error('创建客户失败:', error);
res.status(500).json({
success: false,
message: '创建客户失败',
error: error.message
});
}
});
router.put('/:id', async (req, res) => {
try {
const { id } = req.params;
const { name, address, remark, contacts, payment_infos } = req.body;
// 从contacts中获取主联系人信息
const primaryContact = contacts?.find(c => c.is_primary) || contacts?.[0];
const contact = primaryContact?.name || '';
const position = primaryContact?.position || '';
const phone = primaryContact?.phone || '';
const email = ''; // 前端没有email字段
await db.query(
`UPDATE customers
SET name = ?, address = ?, contact = ?, position = ?, phone = ?, email = ?, remark = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?`,
[name, address, contact, position, phone, email, remark, id]
);
// 删除旧的联系人数据
await db.query(`DELETE FROM contacts WHERE entity_id = $1 AND entity_type = 'customer'`, [id]);
// 插入新的联系人数据
if (contacts && contacts.length > 0) {
for (const contactItem of contacts) {
await db.query(
`INSERT INTO contacts (entity_id, entity_type, name, position, phone, is_primary, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
[id, 'customer', contactItem.name, contactItem.position, contactItem.phone, contactItem.is_primary ? 1 : 0]
);
}
}
// 删除旧的收款信息数据
await db.query(`DELETE FROM supplier_payment_infos WHERE supplier_id = $1`, [id]);
// 插入新的收款信息数据
if (payment_infos && payment_infos.length > 0) {
for (const paymentItem of payment_infos) {
await db.query(
`INSERT INTO supplier_payment_infos (supplier_id, account_name, bank_name, bank_account, qr_code, is_primary, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
[id, paymentItem.account_name, paymentItem.bank_name, paymentItem.bank_account, paymentItem.qr_code, paymentItem.is_primary ? 1 : 0]
);
}
}
res.json({
success: true,
message: '客户更新成功',
data: {
id,
code: `C${String(id).padStart(4, '0')}`,
name,
address,
contacts: contacts || [],
payment_infos: payment_infos || [],
remark,
total_contract_amount: 0,
total_received: 0,
total_receivable: 0,
created_at: new Date().toISOString()
}
});
} catch (error) {
console.error('更新客户失败:', error);
res.status(500).json({
success: false,
message: '更新客户失败',
error: error.message
});
}
});
router.delete('/:id', async (req, res) => {
try {
const { id } = req.params;
// 先删除关联的联系人数据
await db.query(`DELETE FROM contacts WHERE entity_id = $1 AND entity_type = 'customer'`, [id]);
// 再删除客户数据
const result = await db.query(`DELETE FROM customers WHERE id = $1`, [id]);
if (result.changes > 0) {
res.json({
success: true,
message: '客户删除成功'
});
} else {
res.status(404).json({
success: false,
message: '客户不存在'
});
}
} catch (error) {
console.error('删除客户失败:', error);
res.status(500).json({
success: false,
message: '删除客户失败',
error: error.message
});
}
});
module.exports = router;
+101
View File
@@ -0,0 +1,101 @@
const express = require('express');
const db = require('../db');
const router = express.Router();
router.get('/latest', async (req, res) => {
try {
const result = await db.query(`
SELECT e1.currency_code, e1.to_currency_code, e1.rate, e1.effective_date, e1.created_at
FROM exchange_rates e1
JOIN (
SELECT currency_code, to_currency_code, MAX(effective_date) as max_date
FROM exchange_rates
WHERE effective_date <= CURRENT_DATE
GROUP BY currency_code, to_currency_code
) e2 ON e1.currency_code = e2.currency_code AND e1.to_currency_code = e2.to_currency_code AND e1.effective_date = e2.max_date
`);
const data = {};
let latestUpdateTime = null;
result.rows.forEach(row => {
const pairKey = `${row.currency_code}_${row.to_currency_code}`;
data[pairKey] = parseFloat(row.rate);
if (!latestUpdateTime || new Date(row.created_at) > new Date(latestUpdateTime)) {
latestUpdateTime = row.created_at;
}
});
if (Object.keys(data).length === 0) {
data.CNY_LAK = 2900;
data.CNY_USD = 0.143;
data.CNY_THB = 4.8;
data.USD_LAK = 20300;
data.THB_LAK = 604;
}
res.json({
success: true,
data: data,
updated_at: latestUpdateTime || new Date().toISOString(),
date: new Date().toISOString().split('T')[0]
});
} catch (error) {
console.error('获取汇率失败:', error);
res.status(500).json({ success: false, message: '获取汇率失败', error: error.message });
}
});
router.get('/', async (req, res) => {
try {
const result = await db.query(`
SELECT * FROM exchange_rates ORDER BY effective_date DESC LIMIT 20
`);
res.json({ success: true, data: result.rows, count: result.rows.length });
} catch (error) {
console.error('获取汇率失败:', error);
res.status(500).json({ success: false, message: '获取汇率失败', error: error.message });
}
});
router.get('/history', async (req, res) => {
try {
const limit = parseInt(req.query.limit) || 20;
const result = await db.query(`
SELECT * FROM exchange_rates ORDER BY created_at DESC LIMIT $1
`, [limit]);
const formattedData = result.rows.map(row => ({
...row,
pair_key: `${row.currency_code}_${row.to_currency_code}`,
from_currency: row.currency_code,
to_currency: row.to_currency_code
}));
res.json({ success: true, data: formattedData });
} catch (error) {
console.error('获取历史汇率失败:', error);
res.status(500).json({ success: false, message: '获取历史汇率失败', error: error.message });
}
});
router.post('/', async (req, res) => {
try {
const { pair_key, rate, effective_date, from_currency, to_currency } = req.body;
let currencyCode = from_currency;
let toCurrencyCode = to_currency;
if (!currencyCode && pair_key) {
const parts = pair_key.split('_');
currencyCode = parts[0];
toCurrencyCode = parts[1];
}
if (!currencyCode || !toCurrencyCode || rate === undefined || !effective_date) {
return res.status(400).json({ success: false, message: '缺少必要参数' });
}
const result = await db.query(
`INSERT INTO exchange_rates (currency_code, to_currency_code, rate, effective_date, created_at)
VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP) RETURNING *`,
[currencyCode, toCurrencyCode, rate, effective_date]
);
res.json({ success: true, message: '汇率保存成功', data: result.rows[0] });
} catch (error) {
console.error('保存汇率失败:', error);
res.status(500).json({ success: false, message: '保存汇率失败', error: error.message });
}
});
module.exports = router;
+142
View File
@@ -0,0 +1,142 @@
const express = require('express');
const db = require('../db');
const { authenticate, requireAdmin } = require('../middleware/auth');
const router = express.Router();
router.get('/', async (req, res) => {
try {
const result = await db.query(`
SELECT * FROM executions
ORDER BY created_at DESC
`);
res.json({ success: true, data: result.rows, count: result.rows.length });
} catch (error) {
console.error('获取执行记录失败:', error);
res.status(500).json({ success: false, message: '获取执行记录失败', error: error.message });
}
});
router.get('/pending', async (req, res) => {
try {
const advances = await db.query('SELECT * FROM advances WHERE status = $1', ['approved']);
const reimbursements = await db.query('SELECT * FROM reimbursements WHERE status = $1', ['approved']);
const payments = await db.query('SELECT * FROM payment_requests WHERE status = $1', ['approved']);
const verifications = await db.query('SELECT * FROM verifications WHERE status = $1', ['approved']);
const purchaseRequests = await db.query('SELECT * FROM purchase_requests WHERE status = $1', ['approved']);
const pendingData = [
...advances.rows.map(item => ({ ...item, type: '预支申请', code: item.advance_code })),
...reimbursements.rows.map(item => ({ ...item, type: '报销申请', code: item.reimbursement_code })),
...payments.rows.map(item => ({ ...item, type: '付款申请', code: item.request_code })),
...verifications.rows.map(item => ({ ...item, type: '核销申请', code: item.verification_code })),
...purchaseRequests.rows.map(item => ({ ...item, type: '采购申请', code: item.request_code, amount: item.total_amount, date: item.request_date, reason: item.brief_description || item.remark || '采购申请' }))
];
res.json({ success: true, data: pendingData, count: pendingData.length });
} catch (error) {
console.error('获取待执行列表失败:', error);
res.status(500).json({ success: false, message: '获取待执行列表失败', error: error.message });
}
});
router.get('/executed', async (req, res) => {
try {
const advances = await db.query('SELECT * FROM advances WHERE status = $1', ['executed']);
const reimbursements = await db.query('SELECT * FROM reimbursements WHERE status = $1', ['executed']);
const payments = await db.query('SELECT * FROM payment_requests WHERE status = $1', ['executed']);
const verifications = await db.query('SELECT * FROM verifications WHERE status = $1', ['executed']);
const purchaseRequests = await db.query('SELECT * FROM purchase_requests WHERE status = $1', ['executed']);
const executedData = [
...advances.rows.map(item => ({ ...item, type: '预支申请', code: item.advance_code })),
...reimbursements.rows.map(item => ({ ...item, type: '报销申请', code: item.reimbursement_code })),
...payments.rows.map(item => ({ ...item, type: '付款申请', code: item.request_code })),
...verifications.rows.map(item => ({ ...item, type: '核销申请', code: item.verification_code })),
...purchaseRequests.rows.map(item => ({
...item,
type: '采购申请',
code: item.request_code,
amount: item.total_amount,
date: item.request_date,
reason: item.brief_description || item.remark || '采购申请',
executeDate: item.execute_date,
executeMethod: item.execute_method
}))
];
res.json({ success: true, data: executedData, count: executedData.length });
} catch (error) {
console.error('获取已执行列表失败:', error);
res.status(500).json({ success: false, message: '获取已执行列表失败', error: error.message });
}
});
router.post('/', async (req, res) => {
try {
const { apply_id, apply_type, action, execute_method, voucher_no, remark, reject_reason, voucher_files } = req.body;
const operator = '系统管理员';
const operator_role = 'admin';
await db.query(
'INSERT INTO executions (apply_id, apply_type, action, execute_method, voucher_no, remark, reject_reason, voucher_files, operator, operator_role, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW())',
[apply_id, apply_type, action, execute_method, voucher_no, remark, reject_reason, JSON.stringify(voucher_files || []), operator, operator_role]
);
let status = action === 'execute' ? 'executed' : 'rejected';
if (action === 'reject') {
status = 'pending_edit';
}
const executeDate = new Date().toISOString().split('T')[0];
switch (apply_type) {
case 'advance':
await db.query('UPDATE advances SET status = $1, execute_date = $2, execute_method = $3 WHERE id = $4', [status, executeDate, execute_method, apply_id]);
break;
case 'reimbursement':
await db.query('UPDATE reimbursements SET status = $1, execute_date = $2, execute_method = $3 WHERE id = $4', [status, executeDate, execute_method, apply_id]);
break;
case 'payment':
await db.query('UPDATE payment_requests SET status = $1, execute_date = $2, execute_method = $3 WHERE id = $4', [status, executeDate, execute_method, apply_id]);
break;
case 'verification':
await db.query('BEGIN');
try {
await db.query('UPDATE verifications SET status = $1, execute_date = $2, execute_method = $3 WHERE id = $4', [status, executeDate, execute_method, apply_id]);
const verification = await db.query('SELECT advance_id, settlement, amount FROM verifications WHERE id = $1', [apply_id]);
const advanceId = verification.rows[0]?.advance_id;
const isSettlement = verification.rows[0]?.settlement === 1;
const verificationAmount = verification.rows[0]?.amount || 0;
if (advanceId && status === 'executed') {
await db.query('UPDATE advances SET total_reimbursed = total_reimbursed + $1 WHERE id = $2', [verificationAmount, advanceId]);
if (isSettlement) {
await db.query('UPDATE advances SET status = $1 WHERE id = $2', ['completed', advanceId]);
} else {
await db.query('UPDATE advances SET status = $1 WHERE id = $2', ['partial_verification', advanceId]);
}
}
await db.query('COMMIT');
} catch (error) {
await db.query('ROLLBACK');
throw error;
}
break;
case 'purchase':
await db.query('UPDATE purchase_requests SET status = $1, execute_date = $2, execute_method = $3 WHERE id = $4', [status, executeDate, execute_method, apply_id]);
break;
}
res.json({ success: true, message: '执行操作成功' });
} catch (error) {
console.error('执行操作失败:', error);
res.status(500).json({ success: false, message: '执行操作失败', error: error.message });
}
});
module.exports = router;
+35
View File
@@ -0,0 +1,35 @@
const express = require('express');
const db = require('../db');
const { authenticate, requireAdmin } = require('../middleware/auth');
const router = express.Router();
router.get('/', async (req, res) => {
try {
const [customers, suppliers, projects, paymentNodes, paymentRecords] = await Promise.all([
db.query('SELECT COUNT(*) as count FROM customers'),
db.query('SELECT COUNT(*) as count FROM suppliers'),
db.query('SELECT COUNT(*) as count FROM projects'),
db.query('SELECT COUNT(*) as count FROM payment_nodes'),
db.query('SELECT COUNT(*) as count FROM payment_records')
]);
res.json({
success: true,
data: {
summary: {
customers: parseInt(customers.rows[0].count) || 0,
suppliers: parseInt(suppliers.rows[0].count) || 0,
projects: parseInt(projects.rows[0].count) || 0,
payment_nodes: parseInt(paymentNodes.rows[0].count) || 0,
payment_records: parseInt(paymentRecords.rows[0].count) || 0
},
timestamp: new Date().toISOString()
}
});
} catch (error) {
res.json({ success: false, message: '获取财务统计失败', error: error.message });
}
});
module.exports = router;
+34
View File
@@ -0,0 +1,34 @@
const express = require('express');
const db = require('../db');
const { authenticate, requireAdmin } = require('../middleware/auth');
const router = express.Router();
router.get('/', (req, res) => {
res.json({
success: true,
message: '公司财务管理系统 API',
version: '1.0.0',
timestamp: new Date().toISOString(),
endpoints: {
upload: "/api/upload",
health: '/api/health',
auth: '/api/auth',
customers: '/api/customers',
suppliers: '/api/suppliers',
projects: '/api/projects',
products: '/api/products',
payment_nodes: '/api/payment-nodes',
payment_records: '/api/payment-records',
exchange_rates: '/api/exchange-rates',
advances: '/api/advances',
reimbursements: '/api/reimbursements',
purchase_requests: '/api/purchase-requests',
inventory: '/api/inventory',
finance_stats: '/api/finance-stats'
}
});
});
module.exports = router;
+110
View File
@@ -0,0 +1,110 @@
const express = require('express');
const db = require('../db');
const { authenticate, requireAdmin } = require('../middleware/auth');
const router = express.Router();
router.get('/', async (req, res) => {
try {
const { product_id, project_id, record_type } = req.query;
let query = `
SELECT ir.*, p.name as product_name, prj.name as project_name
FROM inventory_records ir
LEFT JOIN products p ON ir.product_id = p.id
LEFT JOIN projects prj ON ir.project_id = prj.id
`;
const params = [];
const conditions = [];
if (product_id) {
conditions.push('ir.product_id = $1');
params.push(product_id);
}
if (project_id) {
conditions.push('ir.project_id = $1');
params.push(project_id);
}
if (record_type) {
conditions.push('ir.record_type = $1');
params.push(record_type);
}
if (conditions.length > 0) {
query += ' WHERE ' + conditions.join(' AND ');
}
query += ' ORDER BY ir.created_at DESC';
const result = await db.query(query, params);
res.json({
success: true,
data: result.rows,
count: result.rows.length
});
} catch (error) {
console.error('获取库存记录失败:', error);
res.status(500).json({
success: false,
message: '获取库存记录失败',
error: error.message
});
}
});
router.get('/summary', async (req, res) => {
try {
const result = await db.query(`
SELECT
p.id as product_id,
p.name as product_name,
p.unit,
SUM(CASE WHEN ir.record_type = 'in' THEN ir.quantity ELSE 0 END) as total_in,
SUM(CASE WHEN ir.record_type = 'out' THEN ir.quantity ELSE 0 END) as total_out,
SUM(CASE WHEN ir.record_type = 'in' THEN ir.quantity ELSE -ir.quantity END) as current_quantity
FROM products p
LEFT JOIN inventory_records ir ON p.id = ir.product_id
GROUP BY p.id, p.name, p.unit
`);
res.json({
success: true,
data: result.rows
});
} catch (error) {
console.error('获取库存汇总失败:', error);
res.status(500).json({
success: false,
message: '获取库存汇总失败',
error: error.message
});
}
});
router.post('/out', async (req, res) => {
try {
const { project_id, product_id, quantity, unit_price, total_amount, operator, remark } = req.body;
const result = await db.query(`
INSERT INTO inventory_records
(record_type, project_id, product_id, quantity, unit_price, total_amount, record_date, operator, remark)
VALUES (?, ?, ?, ?, ?, ?, CURRENT_DATE, ?, ?)
`, ['out', project_id || null, product_id, quantity, unit_price || null, total_amount || null, operator || '系统', remark || null]);
res.json({
success: true,
message: '出库成功',
data: { id: (result.rows[0]?.id || result.rows?.[0]?.id) }
});
} catch (error) {
console.error('出库失败:', error);
res.status(500).json({
success: false,
message: '出库失败',
error: error.message
});
}
});
module.exports = router;
+592
View File
@@ -0,0 +1,592 @@
/**
* 物流管理路由
* 遵循设计方案:采购-付款-物流-退库一体化流程设计方案.md
* 章节:四、物流管理
*
* 统一合作伙伴界面规范:
* - 基本信息:公司名称、地址、联系方式、报价描述
* - 联系人:支持多个联系人,标记主联系人
* - 收款信息:支持多个银行账户,标记默认账户
* - 业务台账:订单列表、运费总额、已付/未付金额
*/
const express = require('express');
const db = require('../db');
const LedgerService = require('../services/ledgerService');
const router = express.Router();
/**
* 获取物流公司列表
*/
router.get('/', async (req, res) => {
try {
const { status } = req.query;
let query = 'SELECT id, code, name, address, phone, email, status, remark, created_at FROM logistics_companies';
const params = [];
if (status) {
query += ' WHERE status = $1';
params.push(status);
}
query += ' ORDER BY created_at DESC';
const result = await db.query(query, params);
res.json({
success: true,
data: result.rows,
count: result.rows.length
});
} catch (error) {
console.error('获取物流公司列表失败:', error);
res.status(500).json({
success: false,
error: error.message
});
}
});
/**
* 获取物流公司详情(包含所有TAB数据)
*/
router.get('/:id', async (req, res) => {
try {
const { id } = req.params;
const companyResult = await db.query('SELECT id, code, name, address, phone, email, status, remark, created_at FROM logistics_companies WHERE id = $1', [id]);
if (companyResult.rows.length === 0) {
return res.status(404).json({ success: false, message: '物流公司不存在' });
}
const company = companyResult.rows[0];
const contactsResult = await db.query(`
SELECT * FROM logistics_company_contacts
WHERE logistics_company_id = $1
ORDER BY is_primary DESC, id
`, [id]);
company.contacts = contactsResult.rows;
const paymentInfosResult = await db.query(`
SELECT * FROM logistics_company_payment_infos
WHERE logistics_company_id = $1
ORDER BY is_default DESC, id
`, [id]);
company.payment_infos = paymentInfosResult.rows;
const ordersResult = await db.query(`
SELECT lr.id, lr.code, lr.purchase_order_id, lr.ship_date, lr.status,
lr.primary_freight, lr.primary_freight_currency, lr.primary_freight_status,
lr.secondary_freight, lr.secondary_freight_currency, lr.secondary_freight_status,
po.code as order_code
FROM logistics_records lr
LEFT JOIN purchase_orders po ON lr.purchase_order_id = po.id
WHERE lr.logistics_company_id = $1
ORDER BY lr.created_at DESC
`, [id]);
company.orders = ordersResult.rows;
const totalFreightResult = await db.query(`
SELECT
COALESCE(SUM(primary_freight), 0) as total_primary_freight,
COALESCE(SUM(secondary_freight), 0) as total_secondary_freight
FROM logistics_records
WHERE logistics_company_id = $1
`, [id]);
company.total_primary_freight = totalFreightResult.rows[0]?.total_primary_freight || 0;
company.total_secondary_freight = totalFreightResult.rows[0]?.total_secondary_freight || 0;
const paidFreightResult = await db.query(`
SELECT
COALESCE(SUM(CASE WHEN primary_freight_status = 'paid' THEN primary_freight ELSE 0 END), 0) as paid_primary,
COALESCE(SUM(CASE WHEN secondary_freight_status = 'paid' THEN secondary_freight ELSE 0 END), 0) as paid_secondary
FROM logistics_records
WHERE logistics_company_id = $1
`, [id]);
company.paid_primary_freight = paidFreightResult.rows[0]?.paid_primary || 0;
company.paid_secondary_freight = paidFreightResult.rows[0]?.paid_secondary || 0;
const ledger = await LedgerService.getLogisticsCompanyLedger(id);
company.ledger = ledger;
res.json({
success: true,
data: company
});
} catch (error) {
console.error('获取物流公司详情失败:', error);
res.status(500).json({
success: false,
message: '获取物流公司详情失败',
error: error.message
});
}
});
/**
* 创建物流公司
*/
router.post('/', async (req, res) => {
try {
const { code, name, address, phone, email, remark } = req.body;
const companyCode = code || 'LC' + new Date().toISOString().slice(0, 10).replace(/-/g, '') +
String(Math.floor(Math.random() * 10000)).padStart(4, '0');
const result = await db.query(`
INSERT INTO logistics_companies
(code, name, address, phone, email, status, remark, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, 'active', $6, NOW(), NOW())
RETURNING id
`, [companyCode, name, address, phone, email, remark]);
res.json({
success: true,
message: '物流公司创建成功',
data: { id: result.rows[0].id, code: companyCode }
});
} catch (error) {
console.error('创建物流公司失败:', error);
res.status(500).json({
success: false,
message: '创建物流公司失败',
error: error.message
});
}
});
/**
* 更新物流公司基本信息
*/
router.put('/:id', async (req, res) => {
try {
const { id } = req.params;
const { name, address, phone, email, status, remark } = req.body;
const result = await db.query(`
UPDATE logistics_companies
SET name = $1, address = $2, phone = $3, email = $4, status = $5, remark = $6, updated_at = NOW()
WHERE id = $7
`, [name, address, phone, email, status, remark, id]);
if (result.rowCount === 0) {
return res.status(404).json({ success: false, message: '物流公司不存在' });
}
res.json({
success: true,
message: '物流公司更新成功'
});
} catch (error) {
console.error('更新物流公司失败:', error);
res.status(500).json({
success: false,
message: '更新物流公司失败',
error: error.message
});
}
});
/**
* 删除物流公司
*/
router.delete('/:id', async (req, res) => {
try {
const { id } = req.params;
const logisticsRecordsResult = await db.query(
'SELECT COUNT(*) as count FROM logistics_records WHERE logistics_company_id = $1',
[id]
);
const hasLogisticsRecords = parseInt(logisticsRecordsResult.rows[0].count) > 0;
if (hasLogisticsRecords) {
return res.status(400).json({
success: false,
message: '该公司已有物流订单关联,无法删除'
});
}
await db.query('BEGIN');
try {
await db.query('DELETE FROM logistics_company_payment_infos WHERE logistics_company_id = $1', [id]);
await db.query('DELETE FROM logistics_company_contacts WHERE logistics_company_id = $1', [id]);
await db.query('DELETE FROM logistics_companies WHERE id = $1', [id]);
await db.query('COMMIT');
res.json({ success: true, message: '物流公司删除成功' });
} 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('/:id/contacts', async (req, res) => {
try {
const { id } = req.params;
const result = await db.query(`
SELECT * FROM logistics_company_contacts
WHERE logistics_company_id = $1
ORDER BY is_primary DESC, id
`, [id]);
res.json({
success: true,
data: result.rows
});
} catch (error) {
console.error('获取联系人列表失败:', error);
res.status(500).json({
success: false,
message: '获取联系人列表失败',
error: error.message
});
}
});
/**
* 添加联系人
*/
router.post('/:id/contacts', async (req, res) => {
try {
const { id } = req.params;
const { name, phone, position, is_primary } = req.body;
if (is_primary) {
await db.query(
'UPDATE logistics_company_contacts SET is_primary = 0 WHERE logistics_company_id = $1',
[id]
);
}
const result = await db.query(`
INSERT INTO logistics_company_contacts
(logistics_company_id, name, phone, position, is_primary, created_at)
VALUES ($1, $2, $3, $4, $5, NOW())
RETURNING id
`, [id, name, phone, position, is_primary ? 1 : 0]);
res.json({
success: true,
message: '联系人添加成功',
data: { id: result.rows[0].id }
});
} catch (error) {
console.error('添加联系人失败:', error);
res.status(500).json({
success: false,
message: '添加联系人失败',
error: error.message
});
}
});
/**
* 更新联系人
*/
router.put('/:id/contacts/:contactId', async (req, res) => {
try {
const { id, contactId } = req.params;
const { name, phone, position, is_primary } = req.body;
if (is_primary) {
await db.query(
'UPDATE logistics_company_contacts SET is_primary = 0 WHERE logistics_company_id = $1',
[id]
);
}
const result = await db.query(`
UPDATE logistics_company_contacts
SET name = $1, phone = $2, position = $3, is_primary = $4
WHERE id = $5 AND logistics_company_id = $6
`, [name, phone, position, is_primary ? 1 : 0, contactId, id]);
if (result.rowCount === 0) {
return res.status(404).json({ success: false, message: '联系人不存在' });
}
res.json({ success: true, message: '联系人更新成功' });
} catch (error) {
console.error('更新联系人失败:', error);
res.status(500).json({
success: false,
message: '更新联系人失败',
error: error.message
});
}
});
/**
* 删除联系人
*/
router.delete('/:id/contacts/:contactId', async (req, res) => {
try {
const { id, contactId } = req.params;
const result = await db.query(
'DELETE FROM logistics_company_contacts WHERE id = $1 AND logistics_company_id = $2',
[contactId, id]
);
if (result.rowCount === 0) {
return res.status(404).json({ success: false, message: '联系人不存在' });
}
res.json({ success: true, message: '联系人删除成功' });
} catch (error) {
console.error('删除联系人失败:', error);
res.status(500).json({
success: false,
message: '删除联系人失败',
error: error.message
});
}
});
/**
* 获取物流公司收款信息列表
*/
router.get('/:id/payment-infos', async (req, res) => {
try {
const { id } = req.params;
const result = await db.query(`
SELECT * FROM logistics_company_payment_infos
WHERE logistics_company_id = $1
ORDER BY is_default DESC, id
`, [id]);
res.json({
success: true,
data: result.rows
});
} catch (error) {
console.error('获取收款信息列表失败:', error);
res.status(500).json({
success: false,
message: '获取收款信息列表失败',
error: error.message
});
}
});
/**
* 添加收款信息
*/
router.post('/:id/payment-infos', async (req, res) => {
try {
const { id } = req.params;
const { account_name, account_number, bank_name, qr_code, is_default } = req.body;
if (is_default) {
await db.query(
'UPDATE logistics_company_payment_infos SET is_default = 0 WHERE logistics_company_id = $1',
[id]
);
}
const result = await db.query(`
INSERT INTO logistics_company_payment_infos
(logistics_company_id, account_name, account_number, bank_name, qr_code, is_default, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW())
RETURNING id
`, [id, account_name, account_number, bank_name, qr_code, is_default ? 1 : 0]);
res.json({
success: true,
message: '收款信息添加成功',
data: { id: result.rows[0].id }
});
} catch (error) {
console.error('添加收款信息失败:', error);
res.status(500).json({
success: false,
message: '添加收款信息失败',
error: error.message
});
}
});
/**
* 更新收款信息
*/
router.put('/:id/payment-infos/:infoId', async (req, res) => {
try {
const { id, infoId } = req.params;
const { account_name, account_number, bank_name, qr_code, is_default } = req.body;
if (is_default) {
await db.query(
'UPDATE logistics_company_payment_infos SET is_default = 0 WHERE logistics_company_id = $1',
[id]
);
}
const result = await db.query(`
UPDATE logistics_company_payment_infos
SET account_name = $1, account_number = $2, bank_name = $3, qr_code = $4, is_default = $5, updated_at = NOW()
WHERE id = $6 AND logistics_company_id = $7
`, [account_name, account_number, bank_name, qr_code, is_default ? 1 : 0, infoId, id]);
if (result.rowCount === 0) {
return res.status(404).json({ success: false, message: '收款信息不存在' });
}
res.json({ success: true, message: '收款信息更新成功' });
} catch (error) {
console.error('更新收款信息失败:', error);
res.status(500).json({
success: false,
message: '更新收款信息失败',
error: error.message
});
}
});
/**
* 删除收款信息
*/
router.delete('/:id/payment-infos/:infoId', async (req, res) => {
try {
const { id, infoId } = req.params;
const result = await db.query(
'DELETE FROM logistics_company_payment_infos WHERE id = $1 AND logistics_company_id = $2',
[infoId, id]
);
if (result.rowCount === 0) {
return res.status(404).json({ success: false, message: '收款信息不存在' });
}
res.json({ success: true, message: '收款信息删除成功' });
} catch (error) {
console.error('删除收款信息失败:', error);
res.status(500).json({
success: false,
message: '删除收款信息失败',
error: error.message
});
}
});
/**
* 获取物流公司业务台账
*/
router.get('/:id/orders', async (req, res) => {
try {
const { id } = req.params;
const result = await db.query(`
SELECT lr.id, lr.code, lr.purchase_order_id, lr.ship_date, lr.status,
lr.primary_freight, lr.primary_freight_currency, lr.primary_freight_status,
lr.secondary_freight, lr.secondary_freight_currency, lr.secondary_freight_status,
po.code as order_code, s.name as supplier_name
FROM logistics_records lr
LEFT JOIN purchase_orders po ON lr.purchase_order_id = po.id
LEFT JOIN suppliers s ON po.supplier_id = s.id
WHERE lr.logistics_company_id = $1
ORDER BY lr.created_at DESC
`, [id]);
res.json({
success: true,
data: result.rows
});
} catch (error) {
console.error('获取业务台账失败:', error);
res.status(500).json({
success: false,
message: '获取业务台账失败',
error: error.message
});
}
});
/**
* 获取物流公司财务台账(汇总+订单列表)
*/
router.get('/:id/ledger', async (req, res) => {
try {
const { id } = req.params;
const summaryResult = await db.query(`
SELECT
COUNT(*) as order_count,
COALESCE(SUM(primary_freight), 0) as total_primary_freight,
COALESCE(SUM(secondary_freight), 0) as total_secondary_freight,
COALESCE(SUM(primary_freight + secondary_freight), 0) as total_freight,
COALESCE(SUM(CASE WHEN primary_freight_status = 'paid' THEN primary_freight ELSE 0 END), 0) as paid_primary_freight,
COALESCE(SUM(CASE WHEN secondary_freight_status = 'paid' THEN secondary_freight ELSE 0 END), 0) as paid_secondary_freight
FROM logistics_records
WHERE logistics_company_id = $1
`, [id]);
const ordersResult = await db.query(`
SELECT lr.id, lr.code, lr.purchase_order_id, lr.ship_date, lr.status,
lr.primary_freight, lr.primary_freight_currency, lr.primary_freight_status,
lr.secondary_freight, lr.secondary_freight_currency, lr.secondary_freight_status,
(COALESCE(lr.primary_freight, 0) + COALESCE(lr.secondary_freight, 0)) as total_freight,
po.code as order_code, p.name as project_name
FROM logistics_records lr
LEFT JOIN purchase_orders po ON lr.purchase_order_id = po.id
LEFT JOIN projects p ON po.project_id = p.id
WHERE lr.logistics_company_id = $1
ORDER BY lr.created_at DESC
`, [id]);
const summary = summaryResult.rows[0] || {
order_count: 0,
total_primary_freight: 0,
total_secondary_freight: 0,
total_freight: 0,
paid_primary_freight: 0,
paid_secondary_freight: 0
};
const totalPaid = (summary.paid_primary_freight || 0) + (summary.paid_secondary_freight || 0);
const totalUnpaid = (summary.total_freight || 0) - totalPaid;
res.json({
success: true,
data: {
summary: {
order_count: summary.order_count || 0,
total_primary_freight: summary.total_primary_freight || 0,
total_secondary_freight: summary.total_secondary_freight || 0,
total_freight: summary.total_freight || 0,
paid_amount: totalPaid,
unpaid_amount: totalUnpaid
},
orders: ordersResult.rows
}
});
} catch (error) {
console.error('获取物流公司台账失败:', error);
res.status(500).json({
success: false,
message: '获取物流公司台账失败',
error: error.message
});
}
});
module.exports = router;
+203
View File
@@ -0,0 +1,203 @@
/**
* 物流管理路由 - PostgreSQL版本
*/
const express = require('express');
const db = require('../db');
const router = express.Router();
router.get('/', async (req, res) => {
try {
const { purchase_order_id, status, ship_from } = req.query;
let 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
`;
const params = [];
const conditions = [];
let paramIndex = 1;
if (purchase_order_id) {
conditions.push('lr.purchase_order_id = $' + paramIndex++);
params.push(purchase_order_id);
}
if (status) {
conditions.push('lr.status = $' + paramIndex++);
params.push(status);
}
if (ship_from) {
conditions.push('lr.ship_from = $' + paramIndex++);
params.push(ship_from);
}
if (conditions.length > 0) {
query += ' WHERE ' + conditions.join(' AND ');
}
query += ' ORDER BY lr.created_at DESC';
const result = await db.query(query, params);
res.json({
success: true,
data: result.rows,
count: result.rows.length
});
} catch (error) {
console.error('获取物流单列表失败:', error);
res.status(500).json({
success: false,
message: '获取物流单列表失败',
error: error.message
});
}
});
router.get('/:id', async (req, res) => {
try {
const { id } = req.params;
const 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 = $1
`, [id]);
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.post('/', async (req, res) => {
try {
const {
purchase_order_id, ship_from, logistics_company_id, logistics_company,
tracking_number, ship_date, ship_location, estimated_arrival_date,
use_hub, primary_freight, primary_freight_currency,
secondary_freight, secondary_freight_currency, driver_phone,
cargo_weight, transport_distance, remark, created_by
} = req.body;
const code = 'LR' + new Date().toISOString().slice(0, 10).replace(/-/g, '') +
String(Math.floor(Math.random() * 10000)).padStart(4, '0');
const result = await db.query(`
INSERT INTO logistics_records
(code, purchase_order_id, ship_from, logistics_company_id, logistics_company,
tracking_number, ship_date, ship_location, estimated_arrival_date,
use_hub, primary_freight, primary_freight_currency, primary_freight_status,
secondary_freight, secondary_freight_currency, secondary_freight_status,
driver_phone, cargo_weight, transport_distance, status, remark, created_by, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, 'pending', $13, $14, 'pending', $15, $16, $17, 'pending', $18, $19, NOW(), NOW())
RETURNING id
`, [code, purchase_order_id, ship_from || 'Laos', logistics_company_id, logistics_company,
tracking_number, ship_date, ship_location, estimated_arrival_date,
use_hub ? 1 : 0, primary_freight || 0, primary_freight_currency || 'CNY',
secondary_freight || 0, secondary_freight_currency || 'LAK',
driver_phone, cargo_weight, transport_distance, remark, created_by]);
res.json({
success: true,
message: '物流单创建成功',
data: { id: result.rows[0].id, code }
});
} catch (error) {
console.error('创建物流单失败:', error);
res.status(500).json({
success: false,
message: '创建物流单失败',
error: error.message
});
}
});
router.put('/:id', async (req, res) => {
try {
const { id } = req.params;
const updateFields = req.body;
const fields = [];
const values = [];
let paramIndex = 1;
const allowedFields = [
'ship_from', 'logistics_company_id', 'logistics_company', 'tracking_number',
'ship_date', 'ship_location', 'estimated_arrival_date',
'customs_arrival_date', 'customs_clearance_date',
'use_hub', 'hub_arrival_date', 'hub_receiver', 'hub_verified_quantity', 'second_ship_date',
'primary_freight', 'primary_freight_currency', 'primary_freight_status', 'primary_freight_document',
'secondary_freight', 'secondary_freight_currency', 'secondary_freight_status',
'driver_phone', 'cargo_weight', 'transport_distance',
'final_arrival_date', 'final_location', 'status', 'remark'
];
for (const field of allowedFields) {
if (updateFields[field] !== undefined) {
fields.push(field + ' = $' + paramIndex++);
values.push(updateFields[field]);
}
}
if (fields.length === 0) {
return res.status(400).json({ success: false, message: '没有要更新的字段' });
}
fields.push('updated_at = NOW()');
values.push(id);
const result = await db.query(
'UPDATE logistics_records SET ' + fields.join(', ') + ' WHERE id = $' + paramIndex,
values
);
if (result.rowCount === 0) {
return res.status(404).json({ success: false, message: '物流单不存在' });
}
res.json({ success: true, message: '物流单更新成功' });
} catch (error) {
console.error('更新物流单失败:', error);
res.status(500).json({
success: false,
message: '更新物流单失败',
error: error.message
});
}
});
router.delete('/:id', async (req, res) => {
try {
const { id } = req.params;
const result = await db.query('DELETE FROM logistics_records WHERE id = $1', [id]);
if (result.rowCount === 0) {
return res.status(404).json({ success: false, message: '物流单不存在' });
}
res.json({ success: true, message: '物流单删除成功' });
} catch (error) {
console.error('删除物流单失败:', error);
res.status(500).json({ success: false, message: '删除物流单失败', error: error.message });
}
});
module.exports = router;
+499
View File
@@ -0,0 +1,499 @@
/**
* 付款执行统一路由
* 遵循设计方案:采购-付款-物流-退库一体化流程设计方案.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;
+387
View File
@@ -0,0 +1,387 @@
/**
* 付款计划路由
* 遵循设计方案:采购-付款-物流-退库一体化流程设计方案.md
* 章节:五、付款计划功能
*
* 功能:
* - 订单确认后自动生成付款计划(在purchase-orders.js中实现)
* - 支持手动调整付款计划
* - 付款计划与付款申请关联
* - 付款计划状态机:pending → requested → approved → paid
*/
const express = require('express');
const db = require('../db');
const router = express.Router();
/**
* 获取付款计划列表
* 支持按订单ID、状态筛选
*/
router.get('/', async (req, res) => {
try {
const { purchase_order_id, status } = req.query;
let query = `
SELECT pp.*,
po.order_code,
po.supplier_id,
s.name as supplier_name,
pr.status as request_status
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
LEFT JOIN payment_requests pr ON pp.payment_request_id = pr.id
`;
const params = [];
const conditions = [];
if (purchase_order_id) {
conditions.push('pp.purchase_order_id = $1');
params.push(purchase_order_id);
}
if (status) {
conditions.push('pp.status = $1');
params.push(status);
}
if (conditions.length > 0) {
query += ' WHERE ' + conditions.join(' AND ');
}
query += ' ORDER BY pp.planned_date ASC, pp.id ASC';
const result = await db.query(query, params);
res.json({
success: true,
data: result.rows,
count: result.rows.length
});
} catch (error) {
console.error('获取付款计划列表失败:', error);
res.status(500).json({
success: false,
message: '获取付款计划列表失败',
error: error.message
});
}
});
/**
* 获取付款计划详情
*/
router.get('/:id', async (req, res) => {
try {
const { id } = req.params;
const result = await db.query(`
SELECT pp.*,
po.order_code,
po.supplier_id,
s.name as supplier_name,
pr.status as request_status,
pr.amount as request_amount
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
LEFT JOIN payment_requests pr ON pp.payment_request_id = pr.id
WHERE pp.id = ?
`, [id]);
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.post('/', async (req, res) => {
try {
const { purchase_order_id, stage, planned_date, planned_amount, planned_percentage, remark } = req.body;
const result = await db.query(`
INSERT INTO payment_plans
(purchase_order_id, stage, planned_date, planned_amount, planned_percentage, remark, status, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, 'pending', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
`, [purchase_order_id, stage, planned_date, planned_amount, planned_percentage, remark]);
res.json({
success: true,
message: '付款计划创建成功',
data: { id: (result.rows[0]?.id || result.rows?.[0]?.id) }
});
} catch (error) {
console.error('创建付款计划失败:', error);
res.status(500).json({
success: false,
message: '创建付款计划失败',
error: error.message
});
}
});
/**
* 更新付款计划
*/
router.put('/:id', async (req, res) => {
try {
const { id } = req.params;
const { stage, planned_date, planned_amount, planned_percentage, remark } = req.body;
const planResult = await db.query('SELECT * FROM payment_plans WHERE id = $1', [id]);
if (planResult.rows.length === 0) {
return res.status(404).json({ success: false, message: '付款计划不存在' });
}
const plan = planResult.rows[0];
if (plan.status !== 'pending') {
return res.status(400).json({ success: false, message: '只能修改待付款状态的计划' });
}
const result = await db.query(`
UPDATE payment_plans
SET stage = ?, planned_date = ?, planned_amount = ?, planned_percentage = ?, remark = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`, [stage, planned_date, planned_amount, planned_percentage, remark, id]);
res.json({
success: true,
message: '付款计划更新成功'
});
} catch (error) {
console.error('更新付款计划失败:', error);
res.status(500).json({
success: false,
message: '更新付款计划失败',
error: error.message
});
}
});
/**
* 删除付款计划
*/
router.delete('/:id', async (req, res) => {
try {
const { id } = req.params;
const planResult = await db.query('SELECT * FROM payment_plans WHERE id = $1', [id]);
if (planResult.rows.length === 0) {
return res.status(404).json({ success: false, message: '付款计划不存在' });
}
const plan = planResult.rows[0];
if (plan.status !== 'pending') {
return res.status(400).json({ success: false, message: '只能删除待付款状态的计划' });
}
await db.query('DELETE FROM payment_plans WHERE id = $1', [id]);
res.json({ success: true, message: '付款计划删除成功' });
} catch (error) {
console.error('删除付款计划失败:', error);
res.status(500).json({
success: false,
message: '删除付款计划失败',
error: error.message
});
}
});
/**
* 创建付款申请
* 遵循设计方案:付款计划与付款申请关联
* 状态从 pending 变为 requested
*/
router.post('/:id/create-request', async (req, res) => {
try {
const { id } = req.params;
const planResult = await db.query(`
SELECT pp.*, po.supplier_id, po.currency, po.project_id
FROM payment_plans pp
LEFT JOIN purchase_orders po ON pp.purchase_order_id = po.id
WHERE pp.id = ?
`, [id]);
if (planResult.rows.length === 0) {
return res.status(404).json({ success: false, message: '付款计划不存在' });
}
const plan = planResult.rows[0];
if (plan.status !== 'pending') {
return res.status(400).json({ success: false, message: '只能对待付款状态的计划创建付款申请' });
}
await db.query('BEGIN TRANSACTION');
try {
const requestCode = 'PAY' + new Date().toISOString().slice(0, 10).replace(/-/g, '') +
String(Math.floor(Math.random() * 10000)).padStart(4, '0');
const requestResult = await db.query(`
INSERT INTO payment_requests
(code, payment_type, purchase_order_id, amount, currency, applicant, request_date, status, created_at)
VALUES (?, 'material', ?, ?, ?, '系统管理员', CURRENT_DATE, 'pending', CURRENT_TIMESTAMP)
`, [requestCode, plan.purchase_order_id, plan.planned_amount, plan.currency || 'CNY']);
await db.query(`
UPDATE payment_plans
SET status = 'requested', payment_request_id = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`, [requestResult.lastID, id]);
await db.query('COMMIT');
res.json({
success: true,
message: '付款申请创建成功',
data: { request_id: requestResult.lastID, request_code: requestCode }
});
} catch (innerError) {
await db.query('ROLLBACK');
throw innerError;
}
} catch (error) {
console.error('创建付款申请失败:', error);
res.status(500).json({
success: false,
message: '创建付款申请失败',
error: error.message
});
}
});
/**
* 标记为已支付
* 遵循设计方案:付款计划状态机
* 状态从 requested/approved 变为 paid
*/
router.post('/:id/mark-paid', async (req, res) => {
try {
const { id } = req.params;
const { actual_amount, actual_date, voucher_url } = req.body;
const planResult = await db.query('SELECT * FROM payment_plans WHERE id = $1', [id]);
if (planResult.rows.length === 0) {
return res.status(404).json({ success: false, message: '付款计划不存在' });
}
const plan = planResult.rows[0];
if (!['requested', 'approved'].includes(plan.status)) {
return res.status(400).json({ success: false, message: '只能对已申请或已批准的计划标记为已支付' });
}
await db.query('BEGIN TRANSACTION');
try {
await db.query(`
UPDATE payment_plans
SET status = 'paid', actual_amount = ?, actual_date = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`, [actual_amount || plan.planned_amount, actual_date || new Date().toISOString().slice(0, 10), id]);
if (plan.payment_request_id) {
await db.query(`
UPDATE payment_requests
SET status = 'paid', updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`, [plan.payment_request_id]);
}
const orderResult = await db.query(`
SELECT SUM(CASE WHEN status = 'paid' THEN actual_amount ELSE 0 END) as paid_amount,
SUM(planned_amount) as total_amount
FROM payment_plans
WHERE purchase_order_id = ?
`, [plan.purchase_order_id]);
const { paid_amount, total_amount } = orderResult.rows[0];
let newOrderStatus = 'confirmed';
if (paid_amount >= total_amount) {
newOrderStatus = 'paid';
} else if (paid_amount > 0) {
newOrderStatus = 'partial_paid';
}
await db.query(`
UPDATE purchase_orders
SET paid_amount = ?, status = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`, [paid_amount, newOrderStatus, plan.purchase_order_id]);
await db.query('COMMIT');
res.json({
success: true,
message: '付款标记成功',
data: { order_status: newOrderStatus, paid_amount }
});
} catch (innerError) {
await db.query('ROLLBACK');
throw innerError;
}
} catch (error) {
console.error('标记付款失败:', error);
res.status(500).json({
success: false,
message: '标记付款失败',
error: error.message
});
}
});
/**
* 获取待提醒的付款计划
* 遵循设计方案:提前N天提醒
*/
router.get('/reminders/upcoming', async (req, res) => {
try {
const days = parseInt(req.query.days) || 3;
const result = await db.query(`
SELECT pp.*,
po.order_code,
s.name as supplier_name
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 = 'pending'
AND pp.planned_date <= date('now', '+' || ? || ' days')
AND pp.planned_date >= CURRENT_DATE
ORDER BY pp.planned_date ASC
`, [days]);
res.json({
success: true,
data: result.rows,
count: result.rows.length
});
} catch (error) {
console.error('获取待提醒付款计划失败:', error);
res.status(500).json({
success: false,
message: '获取待提醒付款计划失败',
error: error.message
});
}
});
module.exports = router;
+36
View File
@@ -0,0 +1,36 @@
const express = require('express');
const db = require('../db');
const { authenticate, requireAdmin } = require('../middleware/auth');
const router = express.Router();
router.get('/', async (req, res) => {
try {
const result = await db.query(`
SELECT pn.*, p.name as project_name, p.code as project_code
FROM payment_nodes pn
LEFT JOIN projects p ON pn.project_id = p.id
ORDER BY pn.due_date ASC
LIMIT 50
`);
res.json({ success: true, data: result.rows, count: result.rows.length });
} catch (error) {
console.error('获取付款节点失败:', error);
res.status(500).json({ success: false, message: '获取付款节点失败', error: error.message });
}
});
router.post('/', authenticate, async (req, res) => {
try {
const { project_id, name, amount, due_date, status } = req.body;
const result = await db.query(
'INSERT INTO payment_nodes (project_id, name, amount, due_date, status) VALUES ($1, $2, $3, $4, $5)',
[project_id, name, amount || 0, due_date, status || 'pending']
);
res.json({ success: true, data: { id: (result.rows[0]?.id || result.rows?.[0]?.id) }, message: '付款节点创建成功' });
} catch (error) {
res.status(500).json({ success: false, message: '创建付款节点失败', error: error.message });
}
});
module.exports = router;
+37
View File
@@ -0,0 +1,37 @@
const express = require('express');
const db = require('../db');
const { authenticate, requireAdmin } = require('../middleware/auth');
const router = express.Router();
router.get('/', async (req, res) => {
try {
const result = await db.query(`
SELECT pr.*, pn.name as node_name, p.name as project_name
FROM payment_records pr
LEFT JOIN payment_nodes pn ON pr.node_id = pn.id
LEFT JOIN projects p ON pn.project_id = p.id
ORDER BY pr.payment_date DESC
LIMIT 50
`);
res.json({ success: true, data: result.rows, count: result.rows.length });
} catch (error) {
console.error('获取付款记录失败:', error);
res.status(500).json({ success: false, message: '获取付款记录失败', error: error.message });
}
});
router.post('/', authenticate, async (req, res) => {
try {
const { node_id, amount, payment_date, method, status } = req.body;
const result = await db.query(
'INSERT INTO payment_records (node_id, amount, payment_date, method, status) VALUES ($1, $2, $3, $4, $5)',
[node_id, amount || 0, payment_date, method, status || 'completed']
);
res.json({ success: true, data: { id: (result.rows[0]?.id || result.rows?.[0]?.id) }, message: '付款记录创建成功' });
} catch (error) {
res.status(500).json({ success: false, message: '创建付款记录失败', error: error.message });
}
});
module.exports = router;
+259
View File
@@ -0,0 +1,259 @@
const express = require('express');
const db = require('../db');
const { authenticate, requireAdmin } = require('../middleware/auth');
const router = express.Router();
router.get('/', async (req, res) => {
try {
const result = await db.query(`
SELECT * FROM payment_requests
ORDER BY created_at DESC
`);
const data = result.rows.map(item => {
if (item.attachments) {
try {
item.attachments = JSON.parse(item.attachments);
} catch (error) {
item.attachments = [];
}
} else {
item.attachments = [];
}
if (item.detail_items) {
try {
item.detail_items = JSON.parse(item.detail_items);
} catch (error) {
item.detail_items = [];
}
} else {
item.detail_items = [];
}
return item;
});
res.json({ success: true, data, count: data.length });
} catch (error) {
console.error('获取付款申请失败:', error);
res.status(500).json({ success: false, message: '获取付款申请失败', error: error.message });
}
});
router.post('/', async (req, res) => {
try {
const {
payment_date, payee, bank_account, bank_name, currency, reason,
detail_items, attachments, applicant,
payee_type, payee_id, expense_type, expense_category, project_id, amount
} = req.body;
// 生成付款申请编号
const requestCode = `PAY-${Date.now()}`;
// 使用默认值处理可选字段
const finalBankAccount = bank_account || '';
const finalBankName = bank_name || '';
const finalAmount = amount || 0;
const result = await db.query(
`INSERT INTO payment_requests (
payee, bank_account, bank_name, amount, currency, reason, payment_date,
request_code, status, applicant, detail_items, attachments,
payee_type, payee_id, expense_type, expense_category, project_id
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
payee, finalBankAccount, finalBankName, finalAmount, currency || 'CNY',
reason, payment_date, requestCode, 'pending', applicant,
JSON.stringify(detail_items || []), JSON.stringify(attachments || []),
payee_type || 'other', payee_id || null, expense_type || 'company',
expense_category || '', project_id || null
]
);
// SQLite不支持RETURNING,所以需要查询刚插入的数据
const lastInsert = await db.query('SELECT * FROM payment_requests ORDER BY id DESC LIMIT 1');
res.json({ success: true, data: lastInsert.rows[0] });
} catch (error) {
console.error('创建付款申请失败:', error);
res.status(500).json({ success: false, message: '创建付款申请失败', error: error.message });
}
});
router.get('/:id', async (req, res) => {
try {
const { id } = req.params;
const result = await db.query('SELECT * FROM payment_requests WHERE id = $1', [id]);
if (result.rows.length > 0) {
const data = result.rows[0];
if (data.attachments) {
try {
data.attachments = JSON.parse(data.attachments);
} catch (error) {
data.attachments = [];
}
} else {
data.attachments = [];
}
if (data.detail_items) {
try {
data.detail_items = JSON.parse(data.detail_items);
} catch (error) {
data.detail_items = [];
}
} else {
data.detail_items = [];
}
res.json({ success: true, data });
} else {
res.status(404).json({ success: false, message: '付款申请不存在' });
}
} catch (error) {
console.error('获取付款申请失败:', error);
res.status(500).json({ success: false, message: '获取付款申请失败', error: error.message });
}
});
router.put('/:id', async (req, res) => {
try {
const { id } = req.params;
const {
payment_date, payee, bank_account, bank_name, currency, reason,
detail_items, attachments, applicant, status,
payee_type, payee_id, expense_type, expense_category, project_id, amount
} = req.body;
// 构建动态更新SQL,只更新提供的字段
const updates = [];
const params = [];
if (payment_date !== undefined) { updates.push('payment_date = $1'); params.push(payment_date); }
if (payee !== undefined) { updates.push('payee = $1'); params.push(payee); }
if (bank_account !== undefined) { updates.push('bank_account = $1'); params.push(bank_account); }
if (bank_name !== undefined) { updates.push('bank_name = $1'); params.push(bank_name); }
if (amount !== undefined) { updates.push('amount = $1'); params.push(amount); }
if (currency !== undefined) { updates.push('currency = $1'); params.push(currency); }
if (reason !== undefined) { updates.push('reason = $1'); params.push(reason); }
if (detail_items !== undefined) { updates.push('detail_items = $1'); params.push(JSON.stringify(detail_items || [])); }
if (attachments !== undefined) { updates.push('attachments = $1'); params.push(JSON.stringify(attachments || [])); }
if (applicant !== undefined) { updates.push('applicant = $1'); params.push(applicant); }
if (status !== undefined) { updates.push('status = $1'); params.push(status); }
if (payee_type !== undefined) { updates.push('payee_type = $1'); params.push(payee_type); }
if (payee_id !== undefined) { updates.push('payee_id = $1'); params.push(payee_id); }
if (expense_type !== undefined) { updates.push('expense_type = $1'); params.push(expense_type); }
if (expense_category !== undefined) { updates.push('expense_category = $1'); params.push(expense_category); }
if (project_id !== undefined) { updates.push('project_id = $1'); params.push(project_id); }
if (updates.length === 0) {
return res.status(400).json({ success: false, message: '没有要更新的字段' });
}
params.push(id);
const result = await db.query(
`UPDATE payment_requests SET ${updates.join(', ')} WHERE id = $1`,
params
);
if (result.changes > 0) {
res.json({ success: true, message: '更新成功' });
} else {
res.status(404).json({ success: false, message: '付款申请不存在' });
}
} catch (error) {
console.error('更新付款申请失败:', error);
res.status(500).json({ success: false, message: '更新付款申请失败', error: error.message });
}
});
router.delete('/:id', async (req, res) => {
try {
const { id } = req.params;
const result = await db.query('DELETE FROM payment_requests WHERE id = $1', [id]);
if (result.changes > 0) {
res.json({ success: true, message: '删除成功' });
} else {
res.status(404).json({ success: false, message: '付款申请不存在' });
}
} catch (error) {
console.error('删除付款申请失败:', error);
res.status(500).json({ success: false, message: '删除付款申请失败', error: error.message });
}
});
router.post('/:id/submit', async (req, res) => {
try {
const { id } = req.params;
const result = await db.query('UPDATE payment_requests SET status = $1 WHERE id = $2', ['pending', id]);
if (result.changes > 0) {
res.json({ success: true, message: '提交成功' });
} else {
res.status(404).json({ success: false, message: '付款申请不存在' });
}
} catch (error) {
console.error('提交付款申请失败:', error);
res.status(500).json({ success: false, message: '提交付款申请失败', error: error.message });
}
});
router.post('/:id/withdraw', async (req, res) => {
try {
const { id } = req.params;
const result = await db.query('UPDATE payment_requests SET status = $1 WHERE id = $2', ['withdrawn', id]);
if (result.changes > 0) {
res.json({ success: true, message: '撤回成功' });
} else {
res.status(404).json({ success: false, message: '付款申请不存在' });
}
} catch (error) {
console.error('撤回报销申请失败:', error);
res.status(500).json({ success: false, message: '撤回报销申请失败', error: error.message });
}
});
router.post('/:id/approve', async (req, res) => {
try {
const { id } = req.params;
const { remark } = req.body;
const result = await db.query('UPDATE payment_requests SET status = $1, approval_remark = $2 WHERE id = $3', ['approved', remark, id]);
if (result.changes > 0) {
res.json({ success: true, message: '审批通过成功' });
} else {
res.status(404).json({ success: false, message: '付款申请不存在' });
}
} catch (error) {
console.error('审批付款申请失败:', error);
res.status(500).json({ success: false, message: '审批付款申请失败', error: error.message });
}
});
router.post('/:id/reject', async (req, res) => {
try {
const { id } = req.params;
const { rejectReason } = req.body;
const result = await db.query('UPDATE payment_requests SET status = $1 WHERE id = $2', ['pending_edit', id]);
if (result.changes > 0) {
res.json({ success: true, message: '退回成功' });
} else {
res.status(404).json({ success: false, message: '付款申请不存在' });
}
} catch (error) {
console.error('退回报销申请失败:', error);
res.status(500).json({ success: false, message: '退回报销申请失败', error: error.message });
}
});
module.exports = router;
+253
View File
@@ -0,0 +1,253 @@
const express = require('express');
const router = express.Router();
const multer = require('multer');
const db = require('../db');
router.get('/', async (req, res) => {
try {
const { category_id, status, keyword } = req.query;
let query = `
SELECT p.*, pc.name as category_name, pc2.name as category_level1_name
FROM products p
LEFT JOIN product_categories pc ON p.category_id = pc.id
LEFT JOIN product_categories pc2 ON pc.parent_id = pc2.id
WHERE 1=1
`;
const params = [];
let paramIndex = 1;
if (category_id) {
const catResult = await db.query('SELECT parent_id FROM product_categories WHERE id = $1', [category_id]);
if (catResult.rows.length > 0 && !catResult.rows[0].parent_id) {
const childCategories = await db.query('SELECT id FROM product_categories WHERE parent_id = $1', [category_id]);
if (childCategories.rows.length > 0) {
const childIds = childCategories.rows.map(row => row.id);
const placeholders = childIds.map(() => '$' + paramIndex++).join(',');
query += ` AND p.category_id IN (${placeholders})`;
params.push(...childIds);
} else {
query += ' AND 1=0';
}
} else {
query += ` AND p.category_id = $${paramIndex++}`;
params.push(category_id);
}
}
if (status) {
query += ` AND p.status = $${paramIndex++}`;
params.push(status);
}
if (keyword) {
const searchTerm = `%${keyword}%`;
query += ` AND (p.name LIKE $${paramIndex} OR p.model LIKE $${paramIndex + 1} OR p.brand LIKE $${paramIndex + 2})`;
params.push(searchTerm, searchTerm, searchTerm);
paramIndex += 3;
}
query += ' ORDER BY p.created_at DESC';
const result = await db.query(query, params);
res.json({ success: true, data: result.rows, count: result.rows.length });
} catch (error) {
console.error('获取商品失败:', error);
res.status(500).json({ success: false, message: '获取商品失败', error: error.message });
}
});
router.get('/template', (req, res) => {
try {
const XLSX = require('xlsx');
const templateData = [
{ '商品名称': 'JKLYJ-35-22kV', '型号': 'Model-001', '一级分类': '电缆电线', '二级分类': '高压电缆', '单位': '米', '成本单价': 12.50, '销售单价': 15.50, '品牌': '云南线缆', '规格参数': '35mm², 22kV', '来源': '中国', '备注': '示例商品' },
{ '商品名称': 'XP-70', '型号': 'XP-70', '一级分类': '电杆横担', '二级分类': '横担', '单位': '个', '成本单价': 20.00, '销售单价': 25.00, '品牌': '江西电瓷', '规格参数': '70kN', '来源': '老挝', '备注': '' }
];
const worksheet = XLSX.utils.json_to_sheet(templateData);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, '商品导入模板');
const buffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' });
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
res.setHeader('Content-Disposition', 'attachment; filename="product_template.xlsx"');
res.send(buffer);
} catch (error) {
console.error('生成模板失败:', error);
res.status(500).json({ success: false, message: '生成模板失败', error: error.message });
}
});
router.post('/batch-import', multer().single('file'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ success: false, message: '请上传文件' });
}
const XLSX = require('xlsx');
const workbook = XLSX.read(req.file.buffer, { type: 'buffer' });
const worksheet = workbook.Sheets[workbook.SheetNames[0]];
const data = XLSX.utils.sheet_to_json(worksheet);
const imported = [];
const errors = [];
for (let i = 0; i < data.length; i++) {
const row = data[i];
try {
let level1Category = null;
if (row['一级分类']) {
const catResult = await db.query('SELECT id FROM product_categories WHERE name = $1 AND parent_id IS NULL', [row['一级分类']]);
if (catResult.rows.length > 0) {
level1Category = catResult.rows[0].id;
} else {
const newCatResult = await db.query('INSERT INTO product_categories (name) VALUES ($1) RETURNING id', [row['一级分类']]);
level1Category = newCatResult.rows[0].id;
}
}
let categoryId = null;
if (row['二级分类']) {
const catResult = await db.query('SELECT id FROM product_categories WHERE name = $1 AND parent_id = $2', [row['二级分类'], level1Category]);
if (catResult.rows.length > 0) {
categoryId = catResult.rows[0].id;
} else {
const newCatResult = await db.query('INSERT INTO product_categories (name, parent_id) VALUES ($1, $2) RETURNING id', [row['二级分类'], level1Category]);
categoryId = newCatResult.rows[0].id;
}
}
const result = await db.query(
`INSERT INTO products (name, model, category_id, unit, cost_price, price, brand, specification, source, remark, stock_quantity) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING id`,
[row['商品名称'] || '', row['型号'] || '', categoryId, row['单位'] || '件', row['成本单价'] || 0, row['销售单价'] || 0, row['品牌'] || '', row['规格参数'] || '', row['来源'] || '老挝', row['备注'] || '', row['库存数量'] || 0]
);
imported.push({ row: i + 2, name: row['商品名称'], id: result.rows[0].id });
} catch (error) {
errors.push({ row: i + 2, name: row['商品名称'], error: error.message });
}
}
res.json({ success: true, message: `导入完成,成功 ${imported.length} 条,失败 ${errors.length}`, imported, errors });
} catch (error) {
console.error('批量导入商品失败:', error);
res.status(500).json({ success: false, message: '批量导入商品失败', error: error.message });
}
});
router.post('/import', multer().single('file'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ success: false, message: '请上传文件' });
}
const XLSX = require('xlsx');
const workbook = XLSX.read(req.file.buffer, { type: 'buffer' });
const worksheet = workbook.Sheets[workbook.SheetNames[0]];
const data = XLSX.utils.sheet_to_json(worksheet);
const imported = [];
const errors = [];
for (let i = 0; i < data.length; i++) {
const row = data[i];
try {
let level1Category = null;
if (row['一级分类']) {
const catResult = await db.query('SELECT id FROM product_categories WHERE name = $1 AND parent_id IS NULL', [row['一级分类']]);
if (catResult.rows.length > 0) { level1Category = catResult.rows[0].id; }
else { const r = await db.query('INSERT INTO product_categories (name) VALUES ($1) RETURNING id', [row['一级分类']]); level1Category = r.rows[0].id; }
}
let categoryId = null;
if (row['二级分类']) {
const catResult = await db.query('SELECT id FROM product_categories WHERE name = $1 AND parent_id = $2', [row['二级分类'], level1Category]);
if (catResult.rows.length > 0) { categoryId = catResult.rows[0].id; }
else { const r = await db.query('INSERT INTO product_categories (name, parent_id) VALUES ($1, $2) RETURNING id', [row['二级分类'], level1Category]); categoryId = r.rows[0].id; }
}
const result = await db.query(
`INSERT INTO products (name, model, category_id, unit, cost_price, price, brand, specification, source, remark, stock_quantity) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING id`,
[row['商品名称'] || '', row['型号'] || '', categoryId, row['单位'] || '件', row['成本单价'] || 0, row['销售单价'] || 0, row['品牌'] || '', row['规格参数'] || '', row['来源'] || '老挝', row['备注'] || '', row['库存数量'] || 0]
);
imported.push({ row: i + 2, name: row['商品名称'], id: result.rows[0].id });
} catch (error) {
errors.push({ row: i + 2, name: row['商品名称'], error: error.message });
}
}
res.json({ success: true, message: `导入完成,成功 ${imported.length} 条,失败 ${errors.length}`, imported, errors });
} catch (error) {
console.error('导入商品失败:', error);
res.status(500).json({ success: false, message: '导入商品失败', error: error.message });
}
});
router.get('/:id', async (req, res) => {
try {
const { id } = req.params;
const result = await db.query(`
SELECT p.*, pc.name as category_name, pc2.name as category_level1_name
FROM products p
LEFT JOIN product_categories pc ON p.category_id = pc.id
LEFT JOIN product_categories pc2 ON pc.parent_id = pc2.id
WHERE p.id = $1
`, [id]);
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.post('/', async (req, res) => {
try {
const { name, model, category_id, unit, cost_price, price, brand, specification, source, remark, stock_quantity, status } = req.body;
if (!name) { return res.status(400).json({ success: false, message: '商品名称不能为空' }); }
let categoryName = null;
if (category_id) {
const catResult = await db.query('SELECT name FROM product_categories WHERE id = $1', [category_id]);
if (catResult.rows.length > 0) { categoryName = catResult.rows[0].name; }
}
const result = await db.query(
`INSERT INTO products (name, model, category_id, category_name, unit, cost_price, price, brand, specification, source, remark, stock_quantity, status) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING *`,
[name, model || '', category_id || null, categoryName, unit || '件', cost_price || null, price || 0, brand || '', specification || '', source || '老挝', remark || '', stock_quantity || 0, status || 'active']
);
res.json({ success: true, data: result.rows[0], message: '创建成功' });
} catch (error) {
console.error('创建商品失败:', error);
res.status(500).json({ success: false, message: '创建商品失败', error: error.message });
}
});
router.put('/:id', async (req, res) => {
try {
const { id } = req.params;
const { name, model, category_id, unit, cost_price, price, brand, specification, source, remark, stock_quantity, stock_warning, status } = req.body;
let categoryName = null;
if (category_id !== undefined && category_id) {
const catResult = await db.query('SELECT name FROM product_categories WHERE id = $1', [category_id]);
if (catResult.rows.length > 0) { categoryName = catResult.rows[0].name; }
}
const updates = [];
const params = [];
let i = 1;
if (name !== undefined) { updates.push(`name = $${i++}`); params.push(name); }
if (model !== undefined) { updates.push(`model = $${i++}`); params.push(model || ''); }
if (category_id !== undefined) { updates.push(`category_id = $${i++}`); params.push(category_id || null); updates.push(`category_name = $${i++}`); params.push(categoryName); }
if (unit !== undefined) { updates.push(`unit = $${i++}`); params.push(unit || '件'); }
if (cost_price !== undefined) { updates.push(`cost_price = $${i++}`); params.push(cost_price || null); }
if (price !== undefined) { updates.push(`price = $${i++}`); params.push(price || 0); }
if (brand !== undefined) { updates.push(`brand = $${i++}`); params.push(brand || ''); }
if (specification !== undefined) { updates.push(`specification = $${i++}`); params.push(specification || ''); }
if (source !== undefined) { updates.push(`source = $${i++}`); params.push(source || '老挝'); }
if (remark !== undefined) { updates.push(`remark = $${i++}`); params.push(remark || ''); }
if (stock_quantity !== undefined) { updates.push(`stock_quantity = $${i++}`); params.push(stock_quantity || 0); }
if (stock_warning !== undefined) { updates.push(`stock_warning = $${i++}`); params.push(stock_warning || 10); }
if (status !== undefined) { updates.push(`status = $${i++}`); params.push(status || 'active'); }
if (updates.length === 0) { return res.status(400).json({ success: false, message: '没有提供更新数据' }); }
updates.push(`updated_at = CURRENT_TIMESTAMP`);
params.push(id);
const result = await db.query(`UPDATE products SET ${updates.join(', ')} WHERE id = $${i} RETURNING *`, params);
if (result.rows.length === 0) { return res.status(404).json({ success: false, message: '商品不存在' }); }
res.json({ success: true, data: result.rows[0], message: '更新成功' });
} catch (error) {
console.error('更新商品失败:', error);
res.status(500).json({ success: false, message: '更新商品失败', error: error.message });
}
});
router.delete('/:id', async (req, res) => {
try {
const { id } = req.params;
const result = await db.query('DELETE FROM products WHERE id = $1 RETURNING id', [id]);
if (result.rows.length === 0) { return res.status(404).json({ success: false, message: '商品不存在' }); }
res.json({ success: true, message: '删除成功' });
} catch (error) {
console.error('删除商品失败:', error);
res.status(500).json({ success: false, message: '删除商品失败', error: error.message });
}
});
module.exports = router;
+364
View File
@@ -0,0 +1,364 @@
/**
* 项目材料管理路由
* 遵循设计方案:采购-付款-物流-退库一体化流程设计方案.md
* 章节:九、项目材料管理
*
* 功能:
* - 材料库存:显示项目当前材料库存
* - 采购记录:显示项目关联的所有采购订单
* - 退库记录:显示项目材料退库记录
* - 材料价格历史:查询材料的历史采购价格
*/
const express = require('express');
const db = require('../db');
const router = express.Router();
/**
* 获取项目材料库存列表
*/
router.get('/inventory/:projectId', async (req, res) => {
try {
const { projectId } = req.params;
const result = await db.query(`
SELECT pmi.*, p.name as product_name, p.specification
FROM project_material_inventory pmi
LEFT JOIN products p ON pmi.product_id = p.id
WHERE pmi.project_id = ?
ORDER BY p.name
`, [projectId]);
res.json({
success: true,
data: result.rows,
count: result.rows.length
});
} catch (error) {
console.error('获取项目材料库存失败:', error);
res.status(500).json({
success: false,
message: '获取项目材料库存失败',
error: error.message
});
}
});
/**
* 获取项目材料库存汇总
*/
router.get('/inventory/:projectId/summary', async (req, res) => {
try {
const { projectId } = req.params;
const result = await db.query(`
SELECT
COUNT(*) as item_count,
SUM(purchased_quantity) as total_purchased,
SUM(received_quantity) as total_received,
SUM(used_quantity) as total_used,
SUM(returned_quantity) as total_returned,
SUM(current_quantity) as total_current,
SUM(total_amount) as total_value
FROM project_material_inventory
WHERE project_id = ?
`, [projectId]);
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('/purchases/:projectId', async (req, res) => {
try {
const { projectId } = req.params;
const { status } = req.query;
let query = `
SELECT po.*, s.name as supplier_name,
(SELECT SUM(total_price) FROM purchase_order_items WHERE order_id = po.id) as total_amount,
(SELECT SUM(CASE WHEN pp.status = 'paid' THEN COALESCE(pp.actual_amount, 0) ELSE 0 END)
FROM payment_plans pp WHERE pp.purchase_order_id = po.id) as paid_amount
FROM purchase_orders po
LEFT JOIN suppliers s ON po.supplier_id = s.id
WHERE po.project_id = ?
`;
const params = [projectId];
if (status) {
query += ' AND po.status = $1';
params.push(status);
}
query += ' ORDER BY po.created_at DESC';
const result = await db.query(query, params);
res.json({
success: true,
data: result.rows,
count: result.rows.length
});
} catch (error) {
console.error('获取项目采购记录失败:', error);
res.status(500).json({
success: false,
message: '获取项目采购记录失败',
error: error.message
});
}
});
/**
* 获取项目退库记录
*/
router.get('/returns/:projectId', async (req, res) => {
try {
const { projectId } = req.params;
const { status } = req.query;
let query = `
SELECT * FROM return_records
WHERE project_id = ?
`;
const params = [projectId];
if (status) {
query += ' AND status = $1';
params.push(status);
}
query += ' ORDER BY created_at DESC';
const result = await db.query(query, params);
const processedResults = result.rows.map(row => {
if (row.items) {
try {
row.items = JSON.parse(row.items);
} catch (e) {
row.items = [];
}
}
return row;
});
res.json({
success: true,
data: processedResults,
count: processedResults.length
});
} catch (error) {
console.error('获取项目退库记录失败:', error);
res.status(500).json({
success: false,
message: '获取项目退库记录失败',
error: error.message
});
}
});
/**
* 获取材料价格历史
* 遵循设计方案:每次订单确认时自动写入material_price_history表
*/
router.get('/price-history/:productId', async (req, res) => {
try {
const { productId } = req.params;
const { supplier_id, limit } = req.query;
let query = `
SELECT mph.*, s.name as supplier_name, s.country as supplier_country,
po.code as order_code
FROM material_price_history mph
LEFT JOIN suppliers s ON mph.supplier_id = s.id
LEFT JOIN purchase_orders po ON mph.purchase_order_id = po.id
WHERE mph.product_id = ?
`;
const params = [productId];
if (supplier_id) {
query += ' AND mph.supplier_id = $1';
params.push(supplier_id);
}
query += ' ORDER BY mph.purchase_date DESC';
if (limit) {
query += ' LIMIT $1';
params.push(parseInt(limit));
}
const result = await db.query(query, params);
res.json({
success: true,
data: result.rows,
count: result.rows.length
});
} catch (error) {
console.error('获取材料价格历史失败:', error);
res.status(500).json({
success: false,
message: '获取材料价格历史失败',
error: error.message
});
}
});
/**
* 获取材料平均采购价格
*/
router.get('/average-price/:productId', async (req, res) => {
try {
const { productId } = req.params;
const result = await db.query(`
SELECT
AVG(unit_price) as avg_price,
MIN(unit_price) as min_price,
MAX(unit_price) as max_price,
COUNT(*) as purchase_count,
SUM(quantity) as total_quantity
FROM material_price_history
WHERE product_id = ?
`, [productId]);
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('/price-trend/:productId', async (req, res) => {
try {
const { productId } = req.params;
const { months } = req.query;
const monthLimit = parseInt(months) || 6;
const result = await db.query(`
SELECT
strftime('%Y-%m', purchase_date) as month,
AVG(unit_price) as avg_price,
SUM(quantity) as total_quantity,
COUNT(*) as purchase_count
FROM material_price_history
WHERE product_id = ?
AND purchase_date >= date('now', '-' || ? || ' months')
GROUP BY strftime('%Y-%m', purchase_date)
ORDER BY month DESC
`, [productId, monthLimit]);
res.json({
success: true,
data: result.rows
});
} catch (error) {
console.error('获取材料价格趋势失败:', error);
res.status(500).json({
success: false,
message: '获取材料价格趋势失败',
error: error.message
});
}
});
/**
* 更新项目材料库存(手动调整)
*/
router.put('/inventory/:projectId/:productId', async (req, res) => {
try {
const { projectId, productId } = req.params;
const { used_quantity, remark } = req.body;
const existingResult = await db.query(`
SELECT * FROM project_material_inventory
WHERE project_id = ? AND product_id = ?
`, [projectId, productId]);
if (existingResult.rows.length === 0) {
return res.status(404).json({ success: false, message: '材料库存记录不存在' });
}
const existing = existingResult.rows[0];
const newUsedQty = (existing.used_quantity || 0) + (used_quantity || 0);
const newCurrentQty = Math.max(0, (existing.current_quantity || 0) - (used_quantity || 0));
await db.query(`
UPDATE project_material_inventory
SET used_quantity = ?, current_quantity = ?, updated_at = CURRENT_TIMESTAMP
WHERE project_id = ? AND product_id = ?
`, [newUsedQty, newCurrentQty, projectId, productId]);
res.json({
success: true,
message: '库存更新成功',
data: { used_quantity: newUsedQty, current_quantity: newCurrentQty }
});
} catch (error) {
console.error('更新材料库存失败:', error);
res.status(500).json({
success: false,
message: '更新材料库存失败',
error: error.message
});
}
});
/**
* 获取所有项目的材料库存汇总
*/
router.get('/all-projects-summary', async (req, res) => {
try {
const result = await db.query(`
SELECT
p.id as project_id,
p.name as project_name,
COUNT(pmi.id) as item_count,
SUM(pmi.current_quantity) as total_quantity,
SUM(pmi.total_amount) as total_value
FROM projects p
LEFT JOIN project_material_inventory pmi ON p.id = pmi.project_id
WHERE p.status = 'active'
GROUP BY p.id
ORDER BY p.name
`);
res.json({
success: true,
data: result.rows
});
} catch (error) {
console.error('获取所有项目材料汇总失败:', error);
res.status(500).json({
success: false,
message: '获取所有项目材料汇总失败',
error: error.message
});
}
});
module.exports = router;
+586
View File
@@ -0,0 +1,586 @@
const express = require('express');
const db = require('../db');
const { authenticate, requireAdmin } = require('../middleware/auth');
const router = express.Router();
router.get('/', async (req, res) => {
try {
const result = await db.query(`
SELECT
p.id,
p.name,
p.customer_id,
p.project_manager_id,
p.contract_amount,
p.start_date,
p.end_date,
p.description,
p.status,
p.location,
c.name as customer_name,
u.name as manager_name
FROM projects p
LEFT JOIN customers c ON p.customer_id = c.id
LEFT JOIN users u ON p.project_manager_id = u.id
ORDER BY p.created_at DESC
LIMIT 50
`);
res.json({
success: true,
data: result.rows,
count: result.rows.length
});
} catch (error) {
console.error('获取项目失败:', error);
res.status(500).json({
success: false,
message: '获取项目失败',
error: error.message
});
}
});
router.get('/:id', async (req, res) => {
try {
const { id } = req.params;
// 获取项目基本信息
const projectResult = await db.query(`
SELECT
p.*,
c.name as customer_name,
u.name as manager_name
FROM projects p
LEFT JOIN customers c ON p.customer_id = c.id
LEFT JOIN users u ON p.project_manager_id = u.id
WHERE p.id = $1
`, [id]);
if (projectResult.rows.length > 0) {
const project = projectResult.rows[0];
// 获取项目合同信息
const contractResult = await db.query(`
SELECT * FROM project_contracts
WHERE project_id = $1
ORDER BY created_at DESC
LIMIT 1
`, [id]);
const contract = contractResult.rows[0];
// 从合同表读取质保金数据,如果没有则使用默认值
const warrantyPercent = contract?.warranty_deposit_percentage || 5;
const warrantyMonths = contract?.warranty_period || 12;
const contractAmount = parseFloat(project.contract_amount || 0);
// 计算质保金金额:合同金额 * 质保比例 / 100
const warrantyAmount = Math.round(contractAmount * warrantyPercent / 100);
// 计算质保期结束日期
const warrantyStartDate = project.end_date;
const warrantyEndDate = warrantyStartDate
? new Date(new Date(warrantyStartDate).getTime() + warrantyMonths * 30 * 24 * 60 * 60 * 1000).toISOString()
: null;
res.json({
success: true,
data: {
id: project.id,
project_code: project.code || `PROJ-${String(project.id).padStart(4, '0')}`,
name: project.name,
customer_id: project.customer_id,
customer_name: project.customer_name || '未知客户',
status: project.status || 'planning',
budget: '0',
spent: '0',
start_date: project.start_date,
end_date: project.end_date,
description: project.description,
contract_type: 'lump_sum',
contract_amount: project.contract_amount?.toString() || '0',
currency: 'CNY',
contract_days: contract?.contract_period || 180,
project_manager_id: project.project_manager_id,
manager_id: project.project_manager_id,
manager_name: project.manager_name || '未知经理',
location: project.location || '',
work_quantity: '',
project_situation: project.description || '',
settlement_type: contract?.settlement_method || 'lump_sum',
has_warranty: true,
warranty_amount: warrantyAmount.toString(),
warranty_percent: warrantyPercent.toString(),
warranty_months: warrantyMonths,
warranty_start_date: warrantyStartDate,
warranty_end_date: warrantyEndDate,
warranty_status: 'pending'
}
});
} else {
res.status(404).json({
success: false,
message: '项目不存在'
});
}
} catch (error) {
console.error('获取项目详情失败:', error);
res.status(500).json({
success: false,
message: '获取项目详情失败',
error: error.message
});
}
});
router.get('/:id/contracts', async (req, res) => {
try {
const { id } = req.params;
const result = await db.query(`
SELECT * FROM project_contracts
WHERE project_id = $1
ORDER BY created_at DESC
`, [id]);
res.json({
success: true,
data: result.rows
});
} catch (error) {
console.error('获取项目合同失败:', error);
res.status(500).json({
success: false,
message: '获取项目合同失败',
error: error.message
});
}
});
router.get('/:id/subcontracts', async (req, res) => {
try {
const { id } = req.params;
const result = await db.query(`
SELECT * FROM subcontracts
WHERE project_id = $1
ORDER BY created_at DESC
`, [id]);
// 解析unit_price_items字段
const subcontracts = result.rows.map(subcontract => {
if (subcontract.unit_price_items) {
try {
subcontract.unit_price_items = JSON.parse(subcontract.unit_price_items);
} catch (error) {
subcontract.unit_price_items = [];
}
} else {
subcontract.unit_price_items = [];
}
return subcontract;
});
res.json({
success: true,
data: subcontracts
});
} catch (error) {
console.error('获取项目分包失败:', error);
res.status(500).json({
success: false,
message: '获取项目分包失败',
error: error.message
});
}
});
router.post('/:id/subcontracts', async (req, res) => {
try {
const { id } = req.params;
const { subcontractor_id, subcontractor_name, contract_amount, currency, settlement_type, other_terms, payment_description, unit_price_items, start_date, end_date, work_days, status } = req.body;
const unitPriceItemsJson = unit_price_items ? JSON.stringify(unit_price_items) : null;
const result = await db.query(
`INSERT INTO subcontracts (project_id, subcontractor_id, subcontractor_name, contract_amount, currency, settlement_type, other_terms, payment_description, unit_price_items, start_date, end_date, work_days, paid_amount, status, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
[id, subcontractor_id, subcontractor_name, contract_amount, currency || 'CNY', settlement_type || 'lump_sum', other_terms, payment_description, unitPriceItemsJson, start_date, end_date, work_days, 0, status || 'active']
);
const subcontractId = (result.rows[0]?.id || result.rows?.[0]?.id);
res.json({
success: true,
message: '新增分包成功',
data: {
id: subcontractId,
project_id: id,
subcontractor_id,
subcontractor_name,
contract_amount,
currency: currency || 'CNY',
settlement_type: settlement_type || 'lump_sum',
other_terms,
payment_description,
unit_price_items,
start_date,
end_date,
work_days,
paid_amount: 0,
status: status || 'active',
created_at: new Date().toISOString()
}
});
} catch (error) {
console.error('新增项目分包失败:', error);
res.status(500).json({
success: false,
message: '新增项目分包失败',
error: error.message
});
}
});
router.get('/:id/materials', async (req, res) => {
try {
const { id } = req.params;
const result = await db.query(`
SELECT * FROM project_materials
WHERE project_id = $1
ORDER BY created_at DESC
`, [id]);
res.json({
success: true,
data: result.rows
});
} catch (error) {
console.error('获取项目材料失败:', error);
res.status(500).json({
success: false,
message: '获取项目材料失败',
error: error.message
});
}
});
router.get('/:id/milestones', async (req, res) => {
try {
const { id } = req.params;
const result = await db.query(`
SELECT * FROM project_milestones
WHERE project_id = $1
ORDER BY expected_date ASC
`, [id]);
res.json({
success: true,
data: result.rows
});
} catch (error) {
console.error('获取项目施工节点失败:', error);
res.status(500).json({
success: false,
message: '获取项目施工节点失败',
error: error.message
});
}
});
router.get('/:id/finances', async (req, res) => {
try {
const { id } = req.params;
const result = await db.query(`
SELECT * FROM project_finances
WHERE project_id = $1
ORDER BY payment_date DESC
`, [id]);
res.json({
success: true,
data: result.rows
});
} catch (error) {
console.error('获取项目财务失败:', error);
res.status(500).json({
success: false,
message: '获取项目财务失败',
error: error.message
});
}
});
router.get('/:id/warranty-deposits', async (req, res) => {
try {
const { id } = req.params;
const result = await db.query(`
SELECT * FROM warranty_deposits
WHERE project_id = $1
ORDER BY created_at DESC
`, [id]);
res.json({
success: true,
data: result.rows
});
} catch (error) {
console.error('获取项目质保金失败:', error);
res.status(500).json({
success: false,
message: '获取项目质保金失败',
error: error.message
});
}
});
router.get('/:id/construction-logs', async (req, res) => {
try {
const { id } = req.params;
// 由于施工日志表可能不存在,返回空数组
res.json({
success: true,
data: []
});
} catch (error) {
console.error('获取项目施工日志失败:', error);
res.status(500).json({
success: false,
message: '获取项目施工日志失败',
error: error.message
});
}
});
router.post('/', async (req, res) => {
try {
const { name, code, customer_id, manager_id, contract_amount, start_date, end_date, description, status, location } = req.body;
const projectCode = code || 'PROJ' + new Date().toISOString().slice(0, 10).replace(/-/g, '') +
String(Math.floor(Math.random() * 10000)).padStart(4, '0');
const result = await db.query(
`INSERT INTO projects (name, code, customer_id, manager_id, contract_amount, start_date, end_date, description, status, location, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
[name, projectCode, customer_id || null, manager_id || null, contract_amount || 0, start_date || '', end_date || '', description || '', status || 'planning', location || '']
);
res.json({
success: true,
message: '项目创建成功',
data: { id: (result.rows[0]?.id || result.rows?.[0]?.id), code: projectCode }
});
} catch (error) {
console.error('创建项目失败:', error);
res.status(500).json({
success: false,
message: '创建项目失败',
error: error.message
});
}
});
router.delete('/:id', authenticate, requireAdmin, async (req, res) => {
try {
const { id } = req.params;
await db.query('DELETE FROM projects WHERE id = $1', [id]);
res.json({ success: true, message: '项目已删除' });
} catch (error) {
console.error('删除项目失败:', error);
res.status(500).json({ success: false, message: error.message });
}
});
router.put('/:id', async (req, res) => {
try {
const { id } = req.params;
const { name, manager_id, location, start_date, end_date, description, status, contract_amount } = req.body;
console.log('收到项目更新请求:', { id, name, manager_id, location, start_date, end_date, description });
// 更新项目信息
await db.query(
'UPDATE projects SET name = CASE WHEN $1 IS NOT NULL THEN $2 ELSE name END, manager_id = CASE WHEN $3 IS NOT NULL THEN $4 ELSE manager_id END, location = CASE WHEN $5 IS NOT NULL THEN $6 ELSE location END, start_date = CASE WHEN $7 IS NOT NULL THEN $8 ELSE start_date END, end_date = CASE WHEN $9 IS NOT NULL THEN $10 ELSE end_date END, description = CASE WHEN $11 IS NOT NULL THEN $12 ELSE description END, status = CASE WHEN $13 IS NOT NULL THEN $14 ELSE status END, contract_amount = CASE WHEN $15 IS NOT NULL THEN $16 ELSE contract_amount END, updated_at = CURRENT_TIMESTAMP WHERE id = $17',
[name, name, manager_id, manager_id, location, location, start_date, start_date, end_date, end_date, description, description, status, status, contract_amount, contract_amount, id]
);
// 如果提供了开始和结束日期,更新合同的工期信息
if (start_date && end_date) {
const start = new Date(start_date);
const end = new Date(end_date);
const contractPeriod = Math.floor((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)) + 1;
// 更新合同信息
await db.query(
'UPDATE project_contracts SET start_date = $1, end_date = $2, contract_period = $3 WHERE project_id = $4',
[start_date, end_date, contractPeriod, id]
);
}
// 查询更新后的数据
const updatedResult = await db.query('SELECT * FROM projects WHERE id = $1', [id]);
res.json({ success: true, data: updatedResult.rows[0] });
} catch (error) {
console.error('更新项目失败:', error);
res.status(500).json({ success: false, message: error.message });
}
});
router.put('/:id/contract', async (req, res) => {
try {
const { id } = req.params;
const {
project_overview,
settlement_type,
contract_total,
tax_included,
unit_price_items,
payment_nodes,
other_info,
contract_file
} = req.body;
console.log('收到合同细节保存请求:', { id, project_overview, settlement_type, contract_total, tax_included, contract_file });
// 1. 更新项目基本信息
await db.query(
`UPDATE projects
SET description = $1, contract_amount = $2
WHERE id = $3`,
[project_overview, contract_total, id]
);
// 2. 更新或创建项目合同
const contractResult = await db.query(
`SELECT * FROM project_contracts WHERE project_id = $1`,
[id]
);
if (contractResult.rows.length > 0) {
// 更新现有合同
await db.query(
`UPDATE project_contracts
SET settlement_method = $1, contract_amount = $2, contract_file = $3, other_info = $4, tax_included = $5
WHERE project_id = $6`,
[settlement_type, contract_total, contract_file, other_info, tax_included, id]
);
} else {
// 创建新合同
const contractCode = `CONTRACT-${Date.now()}`;
await db.query(
`INSERT INTO project_contracts (project_id, contract_code, contract_amount, settlement_method, contract_file, other_info, tax_included, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
[id, contractCode, contract_total, settlement_type, contract_file, other_info, tax_included]
);
}
// 3. 处理付款节点
if (payment_nodes && Array.isArray(payment_nodes)) {
// 删除旧的付款节点
await db.query(`DELETE FROM project_milestones WHERE project_id = $1`, [id]);
// 创建新的付款节点
for (const node of payment_nodes) {
await db.query(
`INSERT INTO project_milestones (project_id, milestone_name, condition, percentage, amount, status, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
[id, node.name, node.condition || '', node.percentage, node.amount, 'pending']
);
}
}
// 4. 处理单价项
if (unit_price_items && Array.isArray(unit_price_items) && settlement_type === 'unit_price') {
// 删除旧的材料项
await db.query(`DELETE FROM project_materials WHERE project_id = $1`, [id]);
// 创建新的材料项
for (const item of unit_price_items) {
await db.query(
`INSERT INTO project_materials (project_id, product_name, unit, budget_quantity, average_price, total_amount, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
[id, item.name, item.unit, item.quantity, item.price, item.total]
);
}
}
res.json({
success: true,
message: '合同细节保存成功'
});
} catch (error) {
console.error('保存合同细节失败:', error);
res.status(500).json({ success: false, message: error.message });
}
});
router.get('/:id/cost-summary', async (req, res) => {
try {
const { id } = req.params;
const purchaseResult = await db.query(`
SELECT
expense_category,
SUM(total_amount) as total_amount
FROM purchase_requests
WHERE project_id = $1 AND status IN ('approved', 'executed')
GROUP BY expense_category
`, [id]);
const paymentResult = await db.query(`
SELECT
SUM(amount) as total_payment
FROM payment_requests
WHERE project_id = $1 AND status = 'approved' AND payment_type = 'company'
`, [id]);
const projectResult = await db.query('SELECT * FROM projects WHERE id = $1', [id]);
if (projectResult.rows.length === 0) {
return res.status(404).json({ success: false, message: '项目不存在' });
}
const project = projectResult.rows[0];
const purchaseByCategory = {};
let totalPurchase = 0;
purchaseResult.rows.forEach(row => {
purchaseByCategory[row.expense_category] = row.total_amount;
totalPurchase += row.total_amount;
});
const totalPayment = paymentResult.rows[0]?.total_payment || 0;
res.json({
success: true,
data: {
project_name: project.name,
contract_amount: project.contract_amount || 0,
purchase_cost: {
total: totalPurchase,
by_category: purchaseByCategory
},
payment_cost: totalPayment,
total_cost: totalPurchase + totalPayment,
profit: (project.contract_amount || 0) - (totalPurchase + totalPayment)
}
});
} catch (error) {
console.error('获取项目成本统计失败:', error);
res.status(500).json({
success: false,
message: '获取项目成本统计失败',
error: error.message
});
}
});
module.exports = router;
+635
View File
@@ -0,0 +1,635 @@
/**
* 采购订单路由
* 遵循设计方案:采购-付款-物流-退库一体化流程设计方案.md
* 章节:三、采购订单页面设计
*
* 多TAB设计:
* - 基本信息:订单基本信息、供应商选择
* - 商品明细:订单商品列表
* - 付款信息:付款计划列表
* - 物流信息:物流单信息
* - 验收记录:验收单列表
*/
const express = require('express');
const db = require('../db');
const router = express.Router();
/**
* 获取采购订单列表
* 遵循设计方案:支持按项目、供应商、状态筛选
*/
router.get('/', async (req, res) => {
try {
const { project_id, supplier_id, status } = req.query;
let query = `
SELECT po.*,
p.name as project_name,
s.name as supplier_name,
(SELECT SUM(planned_amount) FROM payment_plans WHERE purchase_order_id = po.id AND status = 'paid') as paid_amount
FROM purchase_orders po
LEFT JOIN projects p ON po.project_id = p.id
LEFT JOIN suppliers s ON po.supplier_id = s.id
`;
const params = [];
const conditions = [];
if (project_id) {
conditions.push('po.project_id = $1');
params.push(project_id);
}
if (supplier_id) {
conditions.push('po.supplier_id = $1');
params.push(supplier_id);
}
if (status) {
conditions.push('po.status = $1');
params.push(status);
}
if (conditions.length > 0) {
query += ' WHERE ' + conditions.join(' AND ');
}
query += ' ORDER BY po.created_at DESC';
const result = await db.query(query, params);
res.json({
success: true,
data: result.rows,
count: result.rows.length
});
} catch (error) {
console.error('获取采购订单列表失败:', error);
res.status(500).json({
success: false,
message: '获取采购订单列表失败',
error: error.message
});
}
});
/**
* 获取采购订单详情(包含所有TAB数据)
* 遵循设计方案:三、采购订单页面设计 - 多TAB
*/
router.get('/:id', async (req, res) => {
try {
const { id } = req.params;
const orderResult = await db.query(`
SELECT po.*,
p.name as project_name,
s.name as supplier_name,
s.country as supplier_country
FROM purchase_orders po
LEFT JOIN projects p ON po.project_id = p.id
LEFT JOIN suppliers s ON po.supplier_id = s.id
WHERE po.id = ?
`, [id]);
if (orderResult.rows.length === 0) {
return res.status(404).json({ success: false, message: '采购订单不存在' });
}
const order = orderResult.rows[0];
const itemsResult = await db.query(`
SELECT poi.*, p.name as product_name, p.specification, p.unit
FROM purchase_order_items poi
LEFT JOIN products p ON poi.product_id = p.id
WHERE poi.purchase_order_id = ?
ORDER BY poi.id
`, [id]);
order.items = itemsResult.rows;
const paymentPlansResult = await db.query(`
SELECT pp.*, pr.status as request_status
FROM payment_plans pp
LEFT JOIN payment_requests pr ON pp.payment_request_id = pr.id
WHERE pp.purchase_order_id = ?
ORDER BY pp.stage, pp.id
`, [id]);
order.payment_plans = paymentPlansResult.rows;
const logisticsResult = await db.query(`
SELECT lr.*, lc.name as logistics_company_name
FROM logistics_records lr
LEFT JOIN logistics_companies lc ON lr.logistics_company_id = lc.id
WHERE lr.purchase_order_id = ?
ORDER BY lr.id DESC
`, [id]);
order.logistics = logisticsResult.rows;
const verificationResult = await db.query(`
SELECT vr.*, p.name as project_name
FROM verification_records vr
LEFT JOIN projects p ON vr.project_id = p.id
WHERE vr.purchase_order_id = ?
ORDER BY vr.id DESC
`, [id]);
order.verifications = verificationResult.rows;
const totalAmountResult = await db.query(
'SELECT SUM(total_price) as total FROM purchase_order_items WHERE purchase_order_id = $1',
[id]
);
order.total_amount = totalAmountResult.rows[0]?.total || 0;
const paidAmountResult = await db.query(
"SELECT SUM(actual_amount) as paid FROM payment_plans WHERE purchase_order_id = $1 AND status = 'paid'",
[id]
);
order.paid_amount = paidAmountResult.rows[0]?.paid || 0;
res.json({
success: true,
data: order
});
} catch (error) {
console.error('获取采购订单详情失败:', error);
res.status(500).json({
success: false,
message: '获取采购订单详情失败',
error: error.message
});
}
});
/**
* 创建采购订单
*/
router.post('/', async (req, res) => {
try {
const { code, purchase_request_id, project_id, supplier_id, currency, remark, created_by } = req.body;
const orderCode = code || 'PO' + new Date().toISOString().slice(0, 10).replace(/-/g, '') +
String(Math.floor(Math.random() * 10000)).padStart(4, '0');
const result = await db.query(`
INSERT INTO purchase_orders
(code, purchase_request_id, project_id, supplier_id, currency, status, remark, created_by, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, 'draft', ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
`, [orderCode, purchase_request_id, project_id, supplier_id, currency || 'CNY', remark, created_by]);
res.json({
success: true,
message: '采购订单创建成功',
data: { id: (result.rows[0]?.id || result.rows?.[0]?.id), code: orderCode }
});
} catch (error) {
console.error('创建采购订单失败:', error);
res.status(500).json({
success: false,
message: '创建采购订单失败',
error: error.message
});
}
});
/**
* 更新采购订单基本信息
*/
router.put('/:id', async (req, res) => {
try {
const { id } = req.params;
const { supplier_id, supplier_country, contract_url, quotation_url, remark } = req.body;
const result = await db.query(`
UPDATE purchase_orders
SET supplier_id = ?, supplier_country = ?, contract_url = ?, quotation_url = ?, remark = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`, [supplier_id, supplier_country, contract_url, quotation_url, remark, id]);
if (result.changes === 0) {
return res.status(404).json({ success: false, message: '采购订单不存在' });
}
res.json({
success: true,
message: '采购订单更新成功'
});
} catch (error) {
console.error('更新采购订单失败:', error);
res.status(500).json({
success: false,
message: '更新采购订单失败',
error: error.message
});
}
});
/**
* 确认订单
* 遵循设计方案:订单确认后自动创建付款计划,记录材料价格历史
*/
router.post('/:id/confirm', async (req, res) => {
try {
const { id } = req.params;
const { payment_stages } = req.body;
const orderResult = await db.query('SELECT * FROM purchase_orders WHERE id = $1', [id]);
if (orderResult.rows.length === 0) {
return res.status(404).json({ success: false, message: '采购订单不存在' });
}
const order = orderResult.rows[0];
const itemsResult = await db.query('SELECT * FROM purchase_order_items WHERE purchase_order_id = $1', [id]);
const items = itemsResult.rows;
if (items.length === 0) {
return res.status(400).json({ success: false, message: '订单没有商品明细,无法确认' });
}
await db.query('BEGIN TRANSACTION');
try {
await db.query("UPDATE purchase_orders SET status = 'confirmed', updated_at = CURRENT_TIMESTAMP WHERE id = ?", [id]);
const totalAmount = items.reduce((sum, item) => sum + (item.total_price || 0), 0);
await db.query('UPDATE purchase_orders SET total_amount = $1 WHERE id = $2', [totalAmount, id]);
for (const item of items) {
await db.query(`
INSERT INTO material_price_history
(product_id, purchase_order_id, supplier_id, supplier_country, unit_price, currency, quantity, purchase_date, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_DATE, CURRENT_TIMESTAMP)
`, [item.product_id, id, order.supplier_id, order.supplier_country, item.unit_price, order.currency, item.quantity]);
}
if (payment_stages && payment_stages.length > 0) {
for (const stage of payment_stages) {
await db.query(`
INSERT INTO payment_plans
(purchase_order_id, stage, planned_date, planned_amount, planned_percentage, status, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, 'pending', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
`, [id, stage.name, stage.date, stage.amount, stage.percentage]);
}
}
await db.query('COMMIT');
res.json({
success: true,
message: '订单确认成功,已记录材料价格历史'
});
} catch (innerError) {
await db.query('ROLLBACK');
throw innerError;
}
} catch (error) {
console.error('确认订单失败:', error);
res.status(500).json({
success: false,
message: '确认订单失败',
error: error.message
});
}
});
/**
* 取消订单
*/
router.post('/:id/cancel', async (req, res) => {
try {
const { id } = req.params;
const result = await db.query(
"UPDATE purchase_orders SET status = 'cancelled', updated_at = CURRENT_TIMESTAMP WHERE id = ?",
[id]
);
if (result.changes === 0) {
return res.status(404).json({ success: false, message: '采购订单不存在' });
}
res.json({ success: true, message: '订单已取消' });
} catch (error) {
console.error('取消订单失败:', error);
res.status(500).json({ success: false, message: '取消订单失败', error: error.message });
}
});
/**
* 获取订单商品明细
*/
router.get('/:id/items', async (req, res) => {
try {
const { id } = req.params;
const result = await db.query(`
SELECT poi.*, p.name as product_name, p.specification, p.unit
FROM purchase_order_items poi
LEFT JOIN products p ON poi.product_id = p.id
WHERE poi.purchase_order_id = ?
ORDER BY poi.id
`, [id]);
res.json({
success: true,
data: result.rows
});
} catch (error) {
console.error('获取订单商品明细失败:', error);
res.status(500).json({
success: false,
message: '获取订单商品明细失败',
error: error.message
});
}
});
/**
* 添加订单商品明细
*/
router.post('/:id/items', async (req, res) => {
try {
const { id } = req.params;
const { product_id, product_name, specification, unit, quantity, unit_price } = req.body;
const total_price = (quantity || 0) * (unit_price || 0);
const result = await db.query(`
INSERT INTO purchase_order_items
(purchase_order_id, product_id, product_name, specification, unit, quantity, unit_price, total_price, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
`, [id, product_id, product_name, specification, unit, quantity, unit_price, total_price]);
res.json({
success: true,
message: '商品明细添加成功',
data: { id: (result.rows[0]?.id || result.rows?.[0]?.id) }
});
} catch (error) {
console.error('添加商品明细失败:', error);
res.status(500).json({
success: false,
message: '添加商品明细失败',
error: error.message
});
}
});
/**
* 更新订单商品明细
*/
router.put('/:id/items/:itemId', async (req, res) => {
try {
const { id, itemId } = req.params;
const { product_id, product_name, specification, unit, quantity, unit_price } = req.body;
const total_price = (quantity || 0) * (unit_price || 0);
const result = await db.query(`
UPDATE purchase_order_items
SET product_id = ?, product_name = ?, specification = ?, unit = ?, quantity = ?, unit_price = ?, total_price = ?
WHERE id = ? AND purchase_order_id = ?
`, [product_id, product_name, specification, unit, quantity, unit_price, total_price, itemId, id]);
if (result.changes === 0) {
return res.status(404).json({ success: false, message: '商品明细不存在' });
}
res.json({ success: true, message: '商品明细更新成功' });
} catch (error) {
console.error('更新商品明细失败:', error);
res.status(500).json({
success: false,
message: '更新商品明细失败',
error: error.message
});
}
});
/**
* 删除订单商品明细
*/
router.delete('/:id/items/:itemId', async (req, res) => {
try {
const { id, itemId } = req.params;
const result = await db.query(
'DELETE FROM purchase_order_items WHERE id = $1 AND purchase_order_id = $2',
[itemId, id]
);
if (result.changes === 0) {
return res.status(404).json({ success: false, message: '商品明细不存在' });
}
res.json({ success: true, message: '商品明细删除成功' });
} catch (error) {
console.error('删除商品明细失败:', error);
res.status(500).json({
success: false,
message: '删除商品明细失败',
error: error.message
});
}
});
/**
* 获取订单付款计划
*/
router.get('/:id/payment-plans', async (req, res) => {
try {
const { id } = req.params;
const result = await db.query(`
SELECT pp.*, pr.status as request_status
FROM payment_plans pp
LEFT JOIN payment_requests pr ON pp.payment_request_id = pr.id
WHERE pp.purchase_order_id = ?
ORDER BY pp.stage, pp.id
`, [id]);
res.json({
success: true,
data: result.rows
});
} catch (error) {
console.error('获取付款计划失败:', error);
res.status(500).json({
success: false,
message: '获取付款计划失败',
error: error.message
});
}
});
/**
* 添加付款计划
*/
router.post('/:id/payment-plans', async (req, res) => {
try {
const { id } = req.params;
const { stage, planned_date, planned_amount, planned_percentage, remark } = req.body;
const result = await db.query(`
INSERT INTO payment_plans
(purchase_order_id, stage, planned_date, planned_amount, planned_percentage, remark, status, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, 'pending', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
`, [id, stage, planned_date, planned_amount, planned_percentage, remark]);
res.json({
success: true,
message: '付款计划添加成功',
data: { id: (result.rows[0]?.id || result.rows?.[0]?.id) }
});
} catch (error) {
console.error('添加付款计划失败:', error);
res.status(500).json({
success: false,
message: '添加付款计划失败',
error: error.message
});
}
});
/**
* 更新付款计划
*/
router.put('/:id/payment-plans/:planId', async (req, res) => {
try {
const { id, planId } = req.params;
const { stage, planned_date, planned_amount, planned_percentage, remark } = req.body;
const result = await db.query(`
UPDATE payment_plans
SET stage = $1, planned_date = $2, planned_amount = $3, planned_percentage = $4, remark = $5, updated_at = CURRENT_TIMESTAMP
WHERE id = ? AND purchase_order_id = ?
`, [stage, planned_date, planned_amount, planned_percentage, remark, planId, id]);
if (result.changes === 0) {
return res.status(404).json({ success: false, message: '付款计划不存在' });
}
res.json({ success: true, message: '付款计划更新成功' });
} catch (error) {
console.error('更新付款计划失败:', error);
res.status(500).json({
success: false,
message: '更新付款计划失败',
error: error.message
});
}
});
/**
* 删除付款计划
*/
router.delete('/:id/payment-plans/:planId', async (req, res) => {
try {
const { id, planId } = req.params;
const result = await db.query(
'DELETE FROM payment_plans WHERE id = $1 AND purchase_order_id = $2',
[planId, id]
);
if (result.changes === 0) {
return res.status(404).json({ success: false, message: '付款计划不存在' });
}
res.json({ success: true, message: '付款计划删除成功' });
} catch (error) {
console.error('删除付款计划失败:', error);
res.status(500).json({
success: false,
message: '删除付款计划失败',
error: error.message
});
}
});
/**
* 获取订单物流信息
*/
router.get('/:id/logistics', async (req, res) => {
try {
const { id } = req.params;
const result = await db.query(`
SELECT lr.*, lc.name as logistics_company_name
FROM logistics_records lr
LEFT JOIN logistics_companies lc ON lr.logistics_company_id = lc.id
WHERE lr.purchase_order_id = ?
ORDER BY lr.id DESC
`, [id]);
res.json({
success: true,
data: result.rows
});
} catch (error) {
console.error('获取物流信息失败:', error);
res.status(500).json({
success: false,
message: '获取物流信息失败',
error: error.message
});
}
});
/**
* 获取订单验收记录
*/
router.get('/:id/verifications', async (req, res) => {
try {
const { id } = req.params;
const result = await db.query(`
SELECT vr.*, p.name as project_name
FROM verification_records vr
LEFT JOIN projects p ON vr.project_id = p.id
WHERE vr.purchase_order_id = ?
ORDER BY vr.id DESC
`, [id]);
res.json({
success: true,
data: result.rows
});
} catch (error) {
console.error('获取验收记录失败:', error);
res.status(500).json({
success: false,
message: '获取验收记录失败',
error: error.message
});
}
});
/**
* 删除采购订单
*/
router.delete('/:id', async (req, res) => {
try {
const { id } = req.params;
await db.query('BEGIN TRANSACTION');
try {
await db.query('DELETE FROM purchase_order_items WHERE purchase_order_id = $1', [id]);
await db.query('DELETE FROM payment_plans WHERE purchase_order_id = $1', [id]);
await db.query('DELETE FROM purchase_orders WHERE id = $1', [id]);
await db.query('COMMIT');
res.json({ success: true, message: '采购订单删除成功' });
} catch (innerError) {
await db.query('ROLLBACK');
throw innerError;
}
} catch (error) {
console.error('删除采购订单失败:', error);
res.status(500).json({
success: false,
message: '删除采购订单失败',
error: error.message
});
}
});
module.exports = router;
+374
View File
@@ -0,0 +1,374 @@
/**
* 采购申请路由
* 遵循设计方案:采购-付款-物流-退库一体化流程设计方案.md
* 章节:二、采购申请页面改造
*
* 简化后的采购申请:
* - 不再录入供应商(询价前未知)
* - 不再录入商品明细(询价后确定)
* - 仅填写需求描述和预计金额
* - 审批通过后自动生成订单草稿
*/
const express = require('express');
const db = require('../db');
const { authenticate, requireAdmin } = require('../middleware/auth');
const router = express.Router();
/**
* 获取采购申请列表
* 遵循设计方案:支持按项目、状态筛选
*/
router.get('/', async (req, res) => {
try {
const { project_id, status } = req.query;
let query = `
SELECT pr.*, p.name as project_name
FROM purchase_requests pr
LEFT JOIN projects p ON pr.project_id = p.id
`;
const params = [];
if (project_id) {
query += ' WHERE pr.project_id = $1';
params.push(project_id);
}
if (status) {
query += project_id ? ' AND pr.status = $1' : ' WHERE pr.status = $2';
params.push(status);
}
query += ' ORDER BY pr.created_at DESC';
const result = await db.query(query, params);
const data = result.rows.map(row => ({
...row,
request_code: row.code
}));
res.json({
success: true,
data: data,
count: data.length
});
} catch (error) {
console.error('获取采购申请列表失败:', error);
res.status(500).json({
success: false,
message: '获取采购申请列表失败',
error: error.message
});
}
});
/**
* 获取采购申请详情
* 遵循设计方案:简化后不再返回商品明细
*/
router.get('/:id', async (req, res) => {
try {
const { id } = req.params;
const requestResult = await db.query(`
SELECT pr.*, p.name as project_name
FROM purchase_requests pr
LEFT JOIN projects p ON pr.project_id = p.id
WHERE pr.id = ?
`, [id]);
if (requestResult.rows.length === 0) {
return res.status(404).json({ success: false, message: '采购申请不存在' });
}
const purchaseRequest = requestResult.rows[0];
purchaseRequest.request_code = purchaseRequest.code;
if (purchaseRequest.attachments) {
if (typeof purchaseRequest.attachments === 'string') {
purchaseRequest.attachments = purchaseRequest.attachments.split(',').map((url) => ({
url: url,
name: url.split('/').pop() || '',
uid: url,
status: 'done'
}));
}
} else {
purchaseRequest.attachments = [];
}
res.json({
success: true,
data: purchaseRequest
});
} catch (error) {
console.error('获取采购申请详情失败:', error);
res.status(500).json({
success: false,
message: '获取采购申请详情失败',
error: error.message
});
}
});
/**
* 创建采购申请
* 遵循设计方案:二、采购申请页面改造
* 简化后的字段:
* - purchase_type: 采购类型(项目采购/库存采购)
* - project_id: 关联项目(项目采购必填)
* - brief_description: 事由描述
* - total_amount: 预计金额
* - currency: 币种
* - expected_date: 需求日期
* - remark: 备注
* - attachments: 附件
*/
router.post('/', async (req, res) => {
try {
const {
project_id, applicant, request_date,
expense_category, total_amount, currency, remark, attachments,
purchase_type, brief_description, expected_date
} = req.body;
const date = new Date();
const requestCode = `PUR-${date.getFullYear()}${String(date.getMonth() + 1).padStart(2, '0')}${String(date.getDate()).padStart(2, '0')}-${String(Math.floor(Math.random() * 1000)).padStart(3, '0')}`;
const result = await db.query(`
INSERT INTO purchase_requests
(code, title, project_id, applicant, request_date, expense_category, total_amount, currency,
status, purchase_type, brief_description, expected_date, remark, attachments, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
`, [requestCode, brief_description || '采购申请', project_id, applicant || '系统管理员',
request_date, expense_category || 'material', total_amount || 0, currency || 'CNY',
'pending_edit', purchase_type || 'inventory', brief_description, expected_date, remark, attachments || '']);
res.json({
success: true,
message: '采购申请创建成功',
data: { id: (result.rows[0]?.id || result.rows?.[0]?.id), request_code: requestCode }
});
} catch (error) {
console.error('创建采购申请失败:', error);
res.status(500).json({
success: false,
message: '创建采购申请失败',
error: error.message
});
}
});
/**
* 更新采购申请
* 遵循设计方案:简化后不再处理商品明细
*/
router.put('/:id', async (req, res) => {
try {
const { id } = req.params;
const {
project_id, applicant, request_date,
expense_category, total_amount, currency, remark, attachments,
purchase_type, brief_description, expected_date
} = req.body;
const result = await db.query(`
UPDATE purchase_requests
SET project_id = ?, applicant = ?, request_date = ?, expense_category = ?,
total_amount = ?, currency = ?, purchase_type = ?, brief_description = ?,
expected_date = ?, remark = ?, attachments = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`, [project_id, applicant, request_date, expense_category, total_amount, currency || 'CNY',
purchase_type || 'inventory', brief_description, expected_date, remark, attachments || '', id]);
if (result.changes === 0) {
return res.status(404).json({ success: false, message: '采购申请不存在' });
}
res.json({
success: true,
message: '采购申请更新成功'
});
} catch (error) {
console.error('更新采购申请失败:', error);
res.status(500).json({
success: false,
message: '更新采购申请失败',
error: error.message
});
}
});
/**
* 删除采购申请
*/
router.delete('/:id', async (req, res) => {
try {
const { id } = req.params;
const result = await db.query('DELETE FROM purchase_requests WHERE id = $1', [id]);
if (result.changes === 0) {
return res.status(404).json({ success: false, message: '采购申请不存在' });
}
res.json({
success: true,
message: '采购申请删除成功'
});
} catch (error) {
console.error('删除采购申请失败:', error);
res.status(500).json({
success: false,
message: '删除采购申请失败',
error: error.message
});
}
});
/**
* 提交审批
* 遵循设计方案:状态从 pending_edit 变为 pending
*/
router.post('/:id/submit', async (req, res) => {
try {
const { id } = req.params;
const result = await db.query(
'UPDATE purchase_requests SET status = $1, updated_at = datetime(\'now\') WHERE id = $2',
['pending', id]
);
if (result.changes === 0) {
return res.status(404).json({ success: false, message: '采购申请不存在' });
}
res.json({ success: true, message: '提交成功' });
} catch (error) {
console.error('提交采购申请失败:', error);
res.status(500).json({ success: false, message: '提交采购申请失败', error: error.message });
}
});
/**
* 审批通过
* 遵循设计方案:一、流程设计 - 审批通过后自动生成订单草稿
* 章节:1.2 流程节点说明
* - 状态变为 approved
* - 自动创建采购订单草稿(purchase_orders表)
*/
router.post('/:id/approve', async (req, res) => {
try {
const { id } = req.params;
const requestResult = await db.query('SELECT * FROM purchase_requests WHERE id = $1', [id]);
if (requestResult.rows.length === 0) {
return res.status(404).json({ success: false, message: '采购申请不存在' });
}
const purchaseRequest = requestResult.rows[0];
await db.query('BEGIN TRANSACTION');
try {
await db.query(
'UPDATE purchase_requests SET status = $1, updated_at = datetime(\'now\') WHERE id = $2',
['approved', id]
);
const orderCode = 'PO' + new Date().toISOString().slice(0, 10).replace(/-/g, '') +
String(Math.floor(Math.random() * 10000)).padStart(4, '0');
const orderResult = await db.query(`
INSERT INTO purchase_orders
(code, purchase_request_id, project_id, estimated_amount, currency, status, created_by, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
`, [orderCode, id, purchaseRequest.project_id, purchaseRequest.total_amount,
purchaseRequest.currency || 'CNY', 'draft', purchaseRequest.applicant || '系统']);
await db.query('COMMIT');
res.json({
success: true,
message: '审批通过成功,已自动生成采购订单草稿',
data: { order_id: orderResult.lastID, order_code: orderCode }
});
} catch (innerError) {
await db.query('ROLLBACK');
throw innerError;
}
} catch (error) {
console.error('审批采购申请失败:', error);
res.status(500).json({ success: false, message: '审批采购申请失败', error: error.message });
}
});
/**
* 驳回
* 遵循设计方案:状态从 pending 变为 pending_edit
*/
router.post('/:id/reject', async (req, res) => {
try {
const { id } = req.params;
const result = await db.query(
'UPDATE purchase_requests SET status = $1, updated_at = datetime(\'now\') WHERE id = $2',
['pending_edit', id]
);
if (result.changes === 0) {
return res.status(404).json({ success: false, message: '采购申请不存在' });
}
res.json({ success: true, message: '驳回成功' });
} catch (error) {
console.error('驳回采购申请失败:', error);
res.status(500).json({ success: false, message: '驳回采购申请失败', error: error.message });
}
});
/**
* 撤回
* 遵循设计方案:状态从 pending 变为 withdrawn
*/
router.post('/:id/withdraw', async (req, res) => {
try {
const { id } = req.params;
const result = await db.query(
'UPDATE purchase_requests SET status = $1, updated_at = datetime(\'now\') WHERE id = $2',
['withdrawn', id]
);
if (result.changes === 0) {
return res.status(404).json({ success: false, message: '采购申请不存在' });
}
res.json({ success: true, message: '撤回成功' });
} catch (error) {
console.error('撤回采购申请失败:', error);
res.status(500).json({ success: false, message: '撤回采购申请失败', error: error.message });
}
});
/**
* 执行(已废弃,保留向后兼容)
* 遵循设计方案:简化后不再使用此接口
*/
router.post('/:id/execute', async (req, res) => {
try {
const { id } = req.params;
await db.query(
'UPDATE purchase_requests SET status = $1, updated_at = datetime(\'now\') WHERE id = $2',
['executed', id]
);
res.json({ success: true, message: '执行成功' });
} catch (error) {
console.error('执行采购申请失败:', error);
res.status(500).json({ success: false, message: '执行采购申请失败', error: error.message });
}
});
module.exports = router;
+239
View File
@@ -0,0 +1,239 @@
const express = require('express');
const db = require('../db');
const { authenticate, requireAdmin } = require('../middleware/auth');
const { body, validationResult } = require('express-validator');
const router = express.Router();
// 验证错误处理中间件
const validate = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
errors: errors.array()
});
}
next();
};
router.get('/', async (req, res) => {
try {
const result = await db.query(`
SELECT r.*, u.name as user_name, p.name as project_name
FROM reimbursements r
LEFT JOIN users u ON r.applicant_id = u.id
LEFT JOIN projects p ON r.project_id = p.id
ORDER BY r.created_at DESC
`);
// 解析每个报销申请的 attachments 和 detail_items 字段为数组
const data = result.rows.map(item => {
// 解析 attachments 字段
if (item.attachments) {
try {
item.attachments = JSON.parse(item.attachments);
} catch (error) {
item.attachments = [];
}
} else {
item.attachments = [];
}
// 解析 detail_items 字段
if (item.detail_items) {
try {
item.detail_items = JSON.parse(item.detail_items);
} catch (error) {
item.detail_items = [];
}
} else {
item.detail_items = [];
}
return item;
});
res.json({ success: true, data, count: data.length });
} catch (error) {
console.error('获取报销记录失败:', error);
res.status(500).json({
success: false,
message: '获取报销记录失败',
error: error.message
});
}
});
router.post('/', [
body('amount').isFloat({ min: 0.01 }),
body('reason').notEmpty(),
body('expense_type').notEmpty()
], validate, async (req, res) => {
try {
const { amount, reason, project_id, currency, reimbursement_date, attachments, amount_cny, applicant, expense_type, detail_items } = req.body;
const user_id = 1; // 临时使用admin用户
// 生成报销编号
const reimbursementCode = `REIMB-${Date.now()}`;
const result = await db.query(
'INSERT INTO reimbursements (user_id, project_id, amount, currency, amount_cny, reason, reimbursement_date, reimbursement_code, status, applicant, expense_type, detail_items, attachments) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)',
[user_id, project_id, amount, currency || 'CNY', amount_cny || 0, reason, reimbursement_date, reimbursementCode, 'pending', applicant, expense_type, JSON.stringify(detail_items || []), JSON.stringify(attachments || [])]
);
// SQLite不支持RETURNING,所以需要查询刚插入的数据
const lastInsert = await db.query('SELECT * FROM reimbursements ORDER BY id DESC LIMIT 1');
res.json({ success: true, data: lastInsert.rows[0] });
} catch (error) {
console.error('创建报销申请失败:', error);
res.status(500).json({ success: false, message: '创建报销申请失败', error: error.message });
}
});
router.get('/:id', async (req, res) => {
try {
const { id } = req.params;
const result = await db.query('SELECT * FROM reimbursements WHERE id = $1', [id]);
if (result.rows.length > 0) {
const data = result.rows[0];
// 解析 attachments 字段为数组
if (data.attachments) {
try {
data.attachments = JSON.parse(data.attachments);
} catch (error) {
data.attachments = [];
}
} else {
data.attachments = [];
}
// 解析 detail_items 字段为数组
if (data.detail_items) {
try {
data.detail_items = JSON.parse(data.detail_items);
} catch (error) {
data.detail_items = [];
}
} else {
data.detail_items = [];
}
res.json({ success: true, data });
} else {
res.status(404).json({ success: false, message: '报销申请不存在' });
}
} catch (error) {
console.error('获取报销申请失败:', error);
res.status(500).json({ success: false, message: '获取报销申请失败', error: error.message });
}
});
router.put('/:id', async (req, res) => {
try {
const { id } = req.params;
const { amount, reason, project_id, currency, reimbursement_date, attachments, amount_cny, applicant, expense_type, detail_items, status } = req.body;
const result = await db.query(
'UPDATE reimbursements SET amount = $1, reason = $2, project_id = $3, currency = $4, reimbursement_date = $5, attachments = $6, amount_cny = $7, applicant = $8, expense_type = $9, detail_items = $10, status = $11 WHERE id = $12',
[amount, reason, project_id, currency, reimbursement_date, JSON.stringify(attachments || []), amount_cny, applicant, expense_type, JSON.stringify(detail_items || []), status, id]
);
if (result.changes > 0) {
res.json({ success: true, message: '更新成功' });
} else {
res.status(404).json({ success: false, message: '报销申请不存在' });
}
} catch (error) {
console.error('更新报销申请失败:', error);
res.status(500).json({ success: false, message: '更新报销申请失败', error: error.message });
}
});
router.delete('/:id', async (req, res) => {
try {
const { id } = req.params;
const result = await db.query('DELETE FROM reimbursements WHERE id = $1', [id]);
if (result.changes > 0) {
res.json({ success: true, message: '删除成功' });
} else {
res.status(404).json({ success: false, message: '报销申请不存在' });
}
} catch (error) {
console.error('删除报销申请失败:', error);
res.status(500).json({ success: false, message: '删除报销申请失败', error: error.message });
}
});
router.post('/:id/submit', async (req, res) => {
try {
const { id } = req.params;
const result = await db.query('UPDATE reimbursements SET status = $1 WHERE id = $2', ['pending', id]);
if (result.changes > 0) {
res.json({ success: true, message: '提交成功' });
} else {
res.status(404).json({ success: false, message: '报销申请不存在' });
}
} catch (error) {
console.error('提交报销申请失败:', error);
res.status(500).json({ success: false, message: '提交报销申请失败', error: error.message });
}
});
router.post('/:id/withdraw', async (req, res) => {
try {
const { id } = req.params;
const result = await db.query('UPDATE reimbursements SET status = $1 WHERE id = $2', ['withdrawn', id]);
if (result.changes > 0) {
res.json({ success: true, message: '撤回成功' });
} else {
res.status(404).json({ success: false, message: '报销申请不存在' });
}
} catch (error) {
console.error('撤回报销申请失败:', error);
res.status(500).json({ success: false, message: '撤回报销申请失败', error: error.message });
}
});
router.post('/:id/approve', async (req, res) => {
try {
const { id } = req.params;
const { remark } = req.body;
const result = await db.query('UPDATE reimbursements SET status = $1, approval_remark = $2 WHERE id = $3', ['approved', remark, id]);
if (result.changes > 0) {
res.json({ success: true, message: '审批通过成功' });
} else {
res.status(404).json({ success: false, message: '报销申请不存在' });
}
} catch (error) {
console.error('审批报销申请失败:', error);
res.status(500).json({ success: false, message: '审批报销申请失败', error: error.message });
}
});
router.post('/:id/reject', async (req, res) => {
try {
const { id } = req.params;
const { rejectReason } = req.body;
const result = await db.query('UPDATE reimbursements SET status = $1 WHERE id = $2', ['pending_edit', id]);
if (result.changes > 0) {
res.json({ success: true, message: '退回成功' });
} else {
res.status(404).json({ success: false, message: '报销申请不存在' });
}
} catch (error) {
console.error('退回报销申请失败:', error);
res.status(500).json({ success: false, message: '退回报销申请失败', error: error.message });
}
});
module.exports = router;
+358
View File
@@ -0,0 +1,358 @@
/**
* 退库管理路由
* 遵循设计方案:采购-付款-物流-退库一体化流程设计方案.md
* 章节:九、项目材料管理 - 退库记录
*
* 功能:
* - 支持退回供应商和退回仓库两种类型
* - 退库后自动更新项目材料库存
* - 支持成本调整和退款处理
*/
const express = require('express');
const db = require('../db');
const router = express.Router();
/**
* 获取退库单列表
*/
router.get('/', async (req, res) => {
try {
const { project_id, status, return_type } = req.query;
let query = `
SELECT rr.*, p.name as project_name
FROM return_records rr
LEFT JOIN projects p ON rr.project_id = p.id
`;
const params = [];
const conditions = [];
if (project_id) {
conditions.push('rr.project_id = $1');
params.push(project_id);
}
if (status) {
conditions.push('rr.status = $1');
params.push(status);
}
if (return_type) {
conditions.push('rr.return_type = $1');
params.push(return_type);
}
if (conditions.length > 0) {
query += ' WHERE ' + conditions.join(' AND ');
}
query += ' ORDER BY rr.created_at DESC';
const result = await db.query(query, params);
res.json({
success: true,
data: result.rows,
count: result.rows.length
});
} catch (error) {
console.error('获取退库单列表失败:', error);
res.status(500).json({
success: false,
message: '获取退库单列表失败',
error: error.message
});
}
});
/**
* 获取退库单详情
*/
router.get('/:id', async (req, res) => {
try {
const { id } = req.params;
const result = await db.query(`
SELECT rr.*, p.name as project_name
FROM return_records rr
LEFT JOIN projects p ON rr.project_id = p.id
WHERE rr.id = ?
`, [id]);
if (result.rows.length === 0) {
return res.status(404).json({ success: false, message: '退库单不存在' });
}
const returnRecord = result.rows[0];
if (returnRecord.items) {
try {
returnRecord.items = JSON.parse(returnRecord.items);
} catch (e) {
returnRecord.items = [];
}
} else {
returnRecord.items = [];
}
res.json({
success: true,
data: returnRecord
});
} catch (error) {
console.error('获取退库单详情失败:', error);
res.status(500).json({
success: false,
message: '获取退库单详情失败',
error: error.message
});
}
});
/**
* 创建退库单
*/
router.post('/', async (req, res) => {
try {
const {
project_id, return_type, return_date, applicant,
items, total_quantity, total_amount, cost_adjustment,
refund_amount, remark, attachments
} = req.body;
const code = 'RT' + new Date().toISOString().slice(0, 10).replace(/-/g, '') +
String(Math.floor(Math.random() * 10000)).padStart(4, '0');
const itemsJson = items ? JSON.stringify(items) : null;
let calcTotalQty = 0;
let calcTotalAmt = 0;
if (items && Array.isArray(items)) {
for (const item of items) {
calcTotalQty += item.quantity || 0;
calcTotalAmt += item.amount || 0;
}
}
const result = await db.query(`
INSERT INTO return_records
(code, project_id, return_type, return_date, applicant, items,
total_quantity, total_amount, cost_adjustment, refund_amount, status, remark, attachments, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?, CURRENT_TIMESTAMP)
`, [code, project_id, return_type || 'warehouse', return_date, applicant, itemsJson,
total_quantity || calcTotalQty, total_amount || calcTotalAmt, cost_adjustment || 0,
refund_amount || 0, remark, attachments]);
res.json({
success: true,
message: '退库单创建成功',
data: { id: (result.rows[0]?.id || result.rows?.[0]?.id), code }
});
} catch (error) {
console.error('创建退库单失败:', error);
res.status(500).json({
success: false,
message: '创建退库单失败',
error: error.message
});
}
});
/**
* 确认退库
* 遵循设计方案:退库后自动减少项目材料库存
*/
router.post('/:id/confirm', async (req, res) => {
try {
const { id } = req.params;
const returnResult = await db.query('SELECT * FROM return_records WHERE id = $1', [id]);
if (returnResult.rows.length === 0) {
return res.status(404).json({ success: false, message: '退库单不存在' });
}
const returnRecord = returnResult.rows[0];
if (returnRecord.status !== 'pending') {
return res.status(400).json({ success: false, message: '只能确认待审核状态的退库单' });
}
await db.query('BEGIN TRANSACTION');
try {
await db.query("UPDATE return_records SET status = 'confirmed' WHERE id = ?", [id]);
if (returnRecord.items) {
let items;
try {
items = JSON.parse(returnRecord.items);
} catch (e) {
items = [];
}
for (const item of items) {
if (item.quantity > 0 && item.product_id) {
const existingInventory = await db.query(`
SELECT * FROM project_material_inventory
WHERE project_id = ? AND product_id = ?
`, [returnRecord.project_id, item.product_id]);
if (existingInventory.rows.length > 0) {
const existing = existingInventory.rows[0];
const newReturnedQty = (existing.returned_quantity || 0) + item.quantity;
const newCurrentQty = Math.max(0, (existing.current_quantity || 0) - item.quantity);
const newTotalAmount = Math.max(0, (existing.total_amount || 0) - (item.quantity * item.unit_price || 0));
const newAvgPrice = newCurrentQty > 0 ? newTotalAmount / newCurrentQty : 0;
await db.query(`
UPDATE project_material_inventory
SET returned_quantity = ?, current_quantity = ?, total_amount = ?, average_price = ?, updated_at = CURRENT_TIMESTAMP
WHERE project_id = ? AND product_id = ?
`, [newReturnedQty, newCurrentQty, newTotalAmount, newAvgPrice, returnRecord.project_id, item.product_id]);
}
}
}
}
await db.query('COMMIT');
res.json({
success: true,
message: '退库确认成功,已更新项目材料库存'
});
} catch (innerError) {
await db.query('ROLLBACK');
throw innerError;
}
} catch (error) {
console.error('确认退库失败:', error);
res.status(500).json({
success: false,
message: '确认退库失败',
error: error.message
});
}
});
/**
* 驳回退库
*/
router.post('/:id/reject', async (req, res) => {
try {
const { id } = req.params;
const { reason } = req.body;
const result = await db.query(`
UPDATE return_records
SET status = 'rejected', remark = COALESCE(remark || ' | ', '') || '驳回原因: ' || ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`, [reason || '无', id]);
if (result.changes === 0) {
return res.status(404).json({ success: false, message: '退库单不存在' });
}
res.json({ success: true, message: '退库已驳回' });
} catch (error) {
console.error('驳回退库失败:', error);
res.status(500).json({ success: false, message: '驳回退库失败', error: error.message });
}
});
/**
* 更新退库单
*/
router.put('/:id', async (req, res) => {
try {
const { id } = req.params;
const { return_type, return_date, items, total_quantity, total_amount, cost_adjustment, refund_amount, remark, attachments } = req.body;
const returnResult = await db.query('SELECT * FROM return_records WHERE id = $1', [id]);
if (returnResult.rows.length === 0) {
return res.status(404).json({ success: false, message: '退库单不存在' });
}
const returnRecord = returnResult.rows[0];
if (returnRecord.status !== 'pending') {
return res.status(400).json({ success: false, message: '只能修改待审核状态的退库单' });
}
const itemsJson = items ? JSON.stringify(items) : null;
await db.query(`
UPDATE return_records
SET return_type = ?, return_date = ?, items = ?, total_quantity = ?, total_amount = ?,
cost_adjustment = ?, refund_amount = ?, remark = ?, attachments = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`, [return_type, return_date, itemsJson, total_quantity, total_amount, cost_adjustment, refund_amount, remark, attachments, id]);
res.json({ success: true, message: '退库单更新成功' });
} catch (error) {
console.error('更新退库单失败:', error);
res.status(500).json({
success: false,
message: '更新退库单失败',
error: error.message
});
}
});
/**
* 删除退库单
*/
router.delete('/:id', async (req, res) => {
try {
const { id } = req.params;
const returnResult = await db.query('SELECT * FROM return_records WHERE id = $1', [id]);
if (returnResult.rows.length === 0) {
return res.status(404).json({ success: false, message: '退库单不存在' });
}
const returnRecord = returnResult.rows[0];
if (returnRecord.status !== 'pending') {
return res.status(400).json({ success: false, message: '只能删除待审核状态的退库单' });
}
await db.query('DELETE FROM return_records WHERE id = $1', [id]);
res.json({ success: true, message: '退库单删除成功' });
} catch (error) {
console.error('删除退库单失败:', error);
res.status(500).json({
success: false,
message: '删除退库单失败',
error: error.message
});
}
});
/**
* 获取项目可退库的材料列表
*/
router.get('/project-materials/:projectId', async (req, res) => {
try {
const { projectId } = req.params;
const result = await db.query(`
SELECT pmi.*, p.name as product_name, p.specification, p.unit
FROM project_material_inventory pmi
LEFT JOIN products p ON pmi.product_id = p.id
WHERE pmi.project_id = ? AND pmi.current_quantity > 0
ORDER BY p.name
`, [projectId]);
res.json({
success: true,
data: result.rows
});
} catch (error) {
console.error('获取项目材料列表失败:', error);
res.status(500).json({
success: false,
message: '获取项目材料列表失败',
error: error.message
});
}
});
module.exports = router;
+509
View File
@@ -0,0 +1,509 @@
const express = require('express');
const db = require('../db');
const { authenticate, requireAdmin } = require('../middleware/auth');
const LedgerService = require('../services/ledgerService');
const router = express.Router();
router.get('/', async (req, res) => {
try {
const result = await db.query(`
SELECT * FROM subcontractors
ORDER BY created_at DESC
LIMIT 50
`);
// 为每个分包商获取联系人和收款信息
const subcontractorsWithDetails = await Promise.all(
result.rows.map(async (subcontractor) => {
// 获取联系人信息
const contactsResult = await db.query(
`SELECT * FROM contacts WHERE entity_id = $1 AND entity_type = 'subcontractor' ORDER BY is_primary DESC`,
[subcontractor.id]
);
const contacts = contactsResult.rows.map(contact => ({
name: contact.name || '未命名',
position: contact.position || '',
phone: contact.phone || '',
is_primary: contact.is_primary === 1
}));
// 获取收款信息
const paymentInfosResult = await db.query(
`SELECT * FROM subcontractor_payment_infos WHERE subcontractor_id = $1 ORDER BY is_primary DESC`,
[subcontractor.id]
);
const paymentInfos = paymentInfosResult.rows.map(payment => ({
id: payment.id,
account_name: payment.account_name,
bank_account: payment.bank_account,
bank_name: payment.bank_name,
qr_code: payment.qr_code,
is_primary: payment.is_primary === 1
}));
return {
...subcontractor,
contacts: contacts.length > 0 ? contacts : [],
payment_infos: paymentInfos.length > 0 ? paymentInfos : []
};
})
);
res.json({
success: true,
data: subcontractorsWithDetails,
count: subcontractorsWithDetails.length
});
} catch (error) {
console.error('获取分包商失败:', error);
res.status(500).json({
success: false,
message: '获取分包商失败',
error: error.message
});
}
});
router.get('/:id', async (req, res) => {
try {
const { id } = req.params;
// 获取分包商基本信息
const subcontractorResult = await db.query(`
SELECT * FROM subcontractors
WHERE id = ?
`, [id]);
if (subcontractorResult.rows.length > 0) {
const subcontractor = subcontractorResult.rows[0];
// 获取分包商的所有联系人
const contactsResult = await db.query(`
SELECT * FROM contacts
WHERE entity_id = ? AND entity_type = 'subcontractor'
ORDER BY is_primary DESC
`, [id]);
// 转换联系人数据结构
const contacts = contactsResult.rows.map(contact => ({
name: contact.name || '未命名',
position: contact.position || '',
phone: contact.phone || '',
is_primary: contact.is_primary === 1
}));
// 获取分包商的所有收款信息
const paymentInfosResult = await db.query(`
SELECT * FROM subcontractor_payment_infos
WHERE subcontractor_id = ?
ORDER BY is_primary DESC
`, [id]);
// 转换收款信息数据结构
const paymentInfos = paymentInfosResult.rows.map(payment => ({
id: payment.id,
account_name: payment.account_name,
bank_account: payment.bank_account,
bank_name: payment.bank_name,
qr_code: payment.qr_code,
is_primary: payment.is_primary === 1
}));
// 获取业务台账
const ledger = await LedgerService.getSubcontractorLedger(id);
// 转换数据结构以匹配前端期望
const formattedSubcontractor = {
id: subcontractor.id,
code: `SC${String(subcontractor.id).padStart(4, '0')}`,
name: subcontractor.name,
scope: subcontractor.scope || '',
features: subcontractor.features || '',
country: subcontractor.country || '',
contacts: contacts.length > 0 ? contacts : [],
payment_infos: paymentInfos.length > 0 ? paymentInfos : [],
remark: subcontractor.remark || '',
total_contract_amount: ledger.summary.total_contract_amount,
total_paid: ledger.summary.total_paid_amount,
total_payable: ledger.summary.total_unpaid_amount,
ledger: ledger,
created_at: subcontractor.created_at
};
res.json({
success: true,
data: formattedSubcontractor
});
} else {
res.status(404).json({
success: false,
message: '分包商不存在'
});
}
} catch (error) {
console.error('获取分包商详情失败:', error);
res.status(500).json({
success: false,
message: '获取分包商详情失败',
error: error.message
});
}
});
router.post('/', async (req, res) => {
try {
const { name, scope, features, country, remark, contacts, payment_infos } = req.body;
// 从contacts中获取主联系人信息
const primaryContact = contacts?.find(c => c.is_primary) || contacts?.[0];
const contact = primaryContact?.name || '';
const position = primaryContact?.position || '';
const phone = primaryContact?.phone || '';
const email = ''; // 前端没有email字段
const address = ''; // 前端没有address字段
const result = await db.query(
`INSERT INTO subcontractors (name, address, contact, position, phone, email, scope, features, country, remark, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
[name, address, contact, position, phone, email, scope, features, country, remark]
);
const subcontractorId = (result.rows[0]?.id || result.rows?.[0]?.id);
// 插入联系人数据
if (contacts && contacts.length > 0) {
for (const contactItem of contacts) {
await db.query(
`INSERT INTO contacts (entity_id, entity_type, name, position, phone, is_primary, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
[subcontractorId, 'subcontractor', contactItem.name, contactItem.position, contactItem.phone, contactItem.is_primary ? 1 : 0]
);
}
}
// 插入收款信息数据
if (payment_infos && payment_infos.length > 0) {
for (const paymentItem of payment_infos) {
await db.query(
`INSERT INTO subcontractor_payment_infos (subcontractor_id, account_name, bank_name, bank_account, qr_code, is_primary, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
[subcontractorId, paymentItem.account_name, paymentItem.bank_name, paymentItem.bank_account, paymentItem.qr_code, paymentItem.is_primary ? 1 : 0]
);
}
}
res.json({
success: true,
message: '分包商创建成功',
data: {
id: subcontractorId,
code: `SC${String(subcontractorId).padStart(4, '0')}`,
name,
scope,
features,
country,
contacts: contacts || [],
payment_infos: payment_infos || [],
remark,
total_contract_amount: 0,
total_paid: 0,
total_payable: 0,
created_at: new Date().toISOString()
}
});
} catch (error) {
console.error('创建分包商失败:', error);
res.status(500).json({
success: false,
message: '创建分包商失败',
error: error.message
});
}
});
router.put('/:id', async (req, res) => {
try {
const { id } = req.params;
const { name, scope, features, country, remark, contacts, payment_infos } = req.body;
// 从contacts中获取主联系人信息
const primaryContact = contacts?.find(c => c.is_primary) || contacts?.[0];
const contact = primaryContact?.name || '';
const position = primaryContact?.position || '';
const phone = primaryContact?.phone || '';
const email = ''; // 前端没有email字段
const address = ''; // 前端没有address字段
await db.query(
`UPDATE subcontractors
SET name = ?, address = ?, contact = ?, position = ?, phone = ?, email = ?, scope = ?, features = ?, country = ?, remark = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?`,
[name, address, contact, position, phone, email, scope, features, country, remark, id]
);
// 删除旧的联系人数据
await db.query(`DELETE FROM contacts WHERE entity_id = $1 AND entity_type = 'subcontractor'`, [id]);
// 插入新的联系人数据
if (contacts && contacts.length > 0) {
for (const contactItem of contacts) {
await db.query(
`INSERT INTO contacts (entity_id, entity_type, name, position, phone, is_primary, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
[id, 'subcontractor', contactItem.name, contactItem.position, contactItem.phone, contactItem.is_primary ? 1 : 0]
);
}
}
// 删除旧的收款信息数据
await db.query(`DELETE FROM subcontractor_payment_infos WHERE subcontractor_id = $1`, [id]);
// 插入新的收款信息数据
if (payment_infos && payment_infos.length > 0) {
for (const paymentItem of payment_infos) {
await db.query(
`INSERT INTO subcontractor_payment_infos (subcontractor_id, account_name, bank_name, bank_account, qr_code, is_primary, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
[id, paymentItem.account_name, paymentItem.bank_name, paymentItem.bank_account, paymentItem.qr_code, paymentItem.is_primary ? 1 : 0]
);
}
}
res.json({
success: true,
message: '分包商更新成功',
data: {
id,
code: `SC${String(id).padStart(4, '0')}`,
name,
scope,
features,
country,
contacts: contacts || [],
payment_infos: payment_infos || [],
remark,
total_contract_amount: 0,
total_paid: 0,
total_payable: 0,
created_at: new Date().toISOString()
}
});
} catch (error) {
console.error('更新分包商失败:', error);
res.status(500).json({
success: false,
message: '更新分包商失败',
error: error.message
});
}
});
router.delete('/:id', async (req, res) => {
try {
const { id } = req.params;
// 先删除关联的联系人数据
await db.query(`DELETE FROM contacts WHERE entity_id = $1 AND entity_type = 'subcontractor'`, [id]);
// 再删除分包商数据
const result = await db.query(`DELETE FROM subcontractors WHERE id = $1`, [id]);
if (result.changes > 0) {
res.json({
success: true,
message: '分包商删除成功'
});
} else {
res.status(404).json({
success: false,
message: '分包商不存在'
});
}
} catch (error) {
console.error('删除分包商失败:', error);
res.status(500).json({
success: false,
message: '删除分包商失败',
error: error.message
});
}
});
// ==================== 分包商收款信息管理接口 ====================
// 获取分包商的所有收款信息
router.get('/:id/payment-infos', async (req, res) => {
try {
const { id } = req.params;
const result = await db.query(`
SELECT * FROM subcontractor_payment_infos
WHERE subcontractor_id = ?
ORDER BY is_primary DESC, created_at DESC
`, [id]);
const paymentInfos = result.rows.map(payment => ({
id: payment.id,
account_name: payment.account_name,
bank_account: payment.bank_account,
bank_name: payment.bank_name,
qr_code: payment.qr_code,
is_primary: payment.is_primary === 1,
created_at: payment.created_at,
updated_at: payment.updated_at
}));
res.json({
success: true,
data: paymentInfos,
count: paymentInfos.length
});
} catch (error) {
console.error('获取分包商收款信息失败:', error);
res.status(500).json({
success: false,
message: '获取分包商收款信息失败',
error: error.message
});
}
});
// 添加分包商收款信息
router.post('/:id/payment-infos', async (req, res) => {
try {
const { id } = req.params;
const { account_name, bank_account, bank_name, qr_code, is_primary } = req.body;
// 如果设置为默认账户,先取消其他账户的默认状态
if (is_primary) {
await db.query(
'UPDATE subcontractor_payment_infos SET is_primary = 0 WHERE subcontractor_id = $1',
[id]
);
}
const result = await db.query(
`INSERT INTO subcontractor_payment_infos (subcontractor_id, account_name, bank_account, bank_name, qr_code, is_primary, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
[id, account_name, bank_account, bank_name, qr_code, is_primary ? 1 : 0]
);
const paymentInfoId = (result.rows[0]?.id || result.rows?.[0]?.id);
res.json({
success: true,
message: '收款信息添加成功',
data: {
id: paymentInfoId,
subcontractor_id: id,
account_name,
bank_account,
bank_name,
qr_code,
is_primary: !!is_primary,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
}
});
} catch (error) {
console.error('添加分包商收款信息失败:', error);
res.status(500).json({
success: false,
message: '添加分包商收款信息失败',
error: error.message
});
}
});
// 更新分包商收款信息
router.put('/payment-infos/:infoId', async (req, res) => {
try {
const { infoId } = req.params;
const { account_name, bank_account, bank_name, qr_code, is_primary } = req.body;
// 先获取当前收款信息以获取分包商ID
const currentInfoResult = await db.query(
'SELECT subcontractor_id FROM subcontractor_payment_infos WHERE id = $1',
[infoId]
);
if (currentInfoResult.rows.length === 0) {
return res.status(404).json({
success: false,
message: '收款信息不存在'
});
}
const subcontractorId = currentInfoResult.rows[0].subcontractor_id;
// 如果设置为默认账户,先取消其他账户的默认状态
if (is_primary) {
await db.query(
'UPDATE subcontractor_payment_infos SET is_primary = 0 WHERE subcontractor_id = $1 AND id != $2',
[subcontractorId, infoId]
);
}
await db.query(
`UPDATE subcontractor_payment_infos
SET account_name = ?, bank_account = ?, bank_name = ?, qr_code = ?, is_primary = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?`,
[account_name, bank_account, bank_name, qr_code, is_primary ? 1 : 0, infoId]
);
res.json({
success: true,
message: '收款信息更新成功',
data: {
id: infoId,
subcontractor_id: subcontractorId,
account_name,
bank_account,
bank_name,
qr_code,
is_primary: !!is_primary,
updated_at: new Date().toISOString()
}
});
} catch (error) {
console.error('更新分包商收款信息失败:', error);
res.status(500).json({
success: false,
message: '更新分包商收款信息失败',
error: error.message
});
}
});
// 删除分包商收款信息
router.delete('/payment-infos/:infoId', async (req, res) => {
try {
const { infoId } = req.params;
const result = await db.query('DELETE FROM subcontractor_payment_infos WHERE id = $1', [infoId]);
if (result.changes > 0) {
res.json({
success: true,
message: '收款信息删除成功'
});
} else {
res.status(404).json({
success: false,
message: '收款信息不存在'
});
}
} catch (error) {
console.error('删除分包商收款信息失败:', error);
res.status(500).json({
success: false,
message: '删除分包商收款信息失败',
error: error.message
});
}
});
module.exports = router;
+431
View File
@@ -0,0 +1,431 @@
const express = require('express');
const db = require('../db');
const { authenticate, requireAdmin } = require('../middleware/auth');
const LedgerService = require('../services/ledgerService');
const router = express.Router();
router.get('/', async (req, res) => {
try {
const result = await db.query(`
SELECT * FROM suppliers
ORDER BY created_at DESC
LIMIT 50
`);
// 为每个供应商获取联系人和收款信息
const suppliersWithDetails = await Promise.all(
result.rows.map(async (supplier) => {
// 获取联系人信息
const contactsResult = await db.query(
`SELECT * FROM contacts WHERE entity_id = $1 AND entity_type = 'supplier' ORDER BY is_primary DESC`,
[supplier.id]
);
const contacts = contactsResult.rows.map(contact => ({
name: contact.name || '未命名',
position: contact.position || '',
phone: contact.phone || '',
is_primary: contact.is_primary === 1
}));
// 获取收款信息
const paymentInfosResult = await db.query(
`SELECT * FROM supplier_payment_infos WHERE supplier_id = $1 ORDER BY is_default DESC`,
[supplier.id]
);
const paymentInfos = paymentInfosResult.rows.map(payment => ({
id: payment.id,
account_name: payment.account_name,
bank_account: payment.account_number,
bank_name: payment.bank_name,
qr_code: payment.qr_code,
is_primary: payment.is_default === 1
}));
return {
...supplier,
contacts: contacts.length > 0 ? contacts : [],
payment_infos: paymentInfos.length > 0 ? paymentInfos : []
};
})
);
res.json({
success: true,
data: suppliersWithDetails,
count: suppliersWithDetails.length
});
} catch (error) {
console.error('获取供应商失败:', error);
res.status(500).json({
success: false,
message: '获取供应商失败',
error: error.message
});
}
});
router.get('/:id', async (req, res) => {
try {
const { id } = req.params;
// 获取供应商基本信息
const supplierResult = await db.query(`
SELECT * FROM suppliers
WHERE id = ?
`, [id]);
if (supplierResult.rows.length > 0) {
const supplier = supplierResult.rows[0];
// 获取供应商的所有联系人
const contactsResult = await db.query(`
SELECT * FROM contacts
WHERE entity_id = ? AND entity_type = 'supplier'
ORDER BY is_primary DESC
`, [id]);
// 转换联系人数据结构
const contacts = contactsResult.rows.map(contact => ({
name: contact.name || '未命名',
position: contact.position || '',
phone: contact.phone || '',
is_primary: contact.is_primary === 1
}));
// 获取供应商的所有收款信息
const paymentInfosResult = await db.query(`
SELECT * FROM supplier_payment_infos
WHERE supplier_id = ?
ORDER BY is_default DESC
`, [id]);
// 转换收款信息数据结构
const paymentInfos = paymentInfosResult.rows.map(payment => ({
id: payment.id,
account_name: payment.account_name,
bank_account: payment.account_number,
bank_name: payment.bank_name,
qr_code: payment.qr_code,
is_primary: payment.is_default === 1
}));
const ledger = await LedgerService.getSupplierLedger(id);
const formattedSupplier = {
id: supplier.id,
code: `S${String(supplier.id).padStart(4, '0')}`,
name: supplier.name || '未命名',
supply_category: supplier.supply_category || '电力设备',
country: supplier.country || 'Laos',
contacts: contacts.length > 0 ? contacts : [],
payment_infos: paymentInfos.length > 0 ? paymentInfos : [],
remark: supplier.remark || '',
total_purchase_amount: ledger.summary.total_order_amount,
total_paid: ledger.summary.total_paid_amount,
total_payable: ledger.summary.total_unpaid_amount,
ledger: ledger,
created_at: supplier.created_at
};
// 设置响应头确保UTF-8编码
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.json({
success: true,
data: formattedSupplier
});
} else {
res.status(404).json({
success: false,
message: '供应商不存在'
});
}
} catch (error) {
console.error('获取供应商详情失败:', error);
res.status(500).json({
success: false,
message: '获取供应商详情失败',
error: error.message
});
}
});
router.post('/', async (req, res) => {
try {
const { name, supply_category, country, remark, contacts, payment_infos } = req.body;
// 从contacts中获取主联系人信息
const primaryContact = contacts?.find(c => c.is_primary) || contacts?.[0];
const contact = primaryContact?.name || '';
const position = primaryContact?.position || '';
const phone = primaryContact?.phone || '';
const email = ''; // 前端没有email字段
const address = ''; // 前端没有address字段
const result = await db.query(
`INSERT INTO suppliers (name, address, contact, position, phone, email, supply_category, country, remark, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
[name, address, contact, position, phone, email, supply_category, country, remark]
);
const supplierId = (result.rows[0]?.id || result.rows?.[0]?.id);
// 插入联系人数据
if (contacts && contacts.length > 0) {
for (const contactItem of contacts) {
await db.query(
`INSERT INTO contacts (entity_id, entity_type, name, position, phone, is_primary, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
[supplierId, 'supplier', contactItem.name, contactItem.position, contactItem.phone, contactItem.is_primary ? 1 : 0]
);
}
}
// 插入收款信息数据
if (payment_infos && payment_infos.length > 0) {
for (const paymentInfo of payment_infos) {
await db.query(
`INSERT INTO supplier_payment_infos (supplier_id, account_name, bank_account, bank_name, qr_code, is_primary, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
[supplierId, paymentInfo.account_name, paymentInfo.bank_account, paymentInfo.bank_name, paymentInfo.qr_code, paymentInfo.is_primary ? 1 : 0]
);
}
}
res.json({
success: true,
message: '供应商创建成功',
data: {
id: supplierId,
code: `S${String(supplierId).padStart(4, '0')}`,
name,
supply_category,
country,
contacts: contacts || [],
payment_infos: payment_infos || [],
remark,
total_purchase_amount: 0,
total_paid: 0,
total_payable: 0,
created_at: new Date().toISOString()
}
});
} catch (error) {
console.error('创建供应商失败:', error);
res.status(500).json({
success: false,
message: '创建供应商失败',
error: error.message
});
}
});
router.put('/:id', async (req, res) => {
try {
const { id } = req.params;
const { name, supply_category, country, remark, contacts, payment_infos } = req.body;
// 从contacts中获取主联系人信息
const primaryContact = contacts?.find(c => c.is_primary) || contacts?.[0];
const contact = primaryContact?.name || '';
const position = primaryContact?.position || '';
const phone = primaryContact?.phone || '';
const email = ''; // 前端没有email字段
const address = ''; // 前端没有address字段
await db.query(
`UPDATE suppliers
SET name = ?, address = ?, contact = ?, position = ?, phone = ?, email = ?, supply_category = ?, country = ?, remark = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?`,
[name, address, contact, position, phone, email, supply_category, country, remark, id]
);
// 删除旧的联系人数据
await db.query(`DELETE FROM contacts WHERE entity_id = $1 AND entity_type = 'supplier'`, [id]);
// 插入新的联系人数据
if (contacts && contacts.length > 0) {
for (const contactItem of contacts) {
await db.query(
`INSERT INTO contacts (entity_id, entity_type, name, position, phone, is_primary, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
[id, 'supplier', contactItem.name, contactItem.position, contactItem.phone, contactItem.is_primary ? 1 : 0]
);
}
}
// 删除旧的收款信息数据
await db.query(`DELETE FROM supplier_payment_infos WHERE supplier_id = $1`, [id]);
// 插入新的收款信息数据
if (payment_infos && payment_infos.length > 0) {
for (const paymentInfo of payment_infos) {
await db.query(
`INSERT INTO supplier_payment_infos (supplier_id, account_name, bank_account, bank_name, qr_code, is_primary, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
[id, paymentInfo.account_name, paymentInfo.bank_account, paymentInfo.bank_name, paymentInfo.qr_code, paymentInfo.is_primary ? 1 : 0]
);
}
}
res.json({
success: true,
message: '供应商更新成功',
data: {
id,
code: `S${String(id).padStart(4, '0')}`,
name,
supply_category,
country,
contacts: contacts || [],
payment_infos: payment_infos || [],
remark,
total_purchase_amount: 0,
total_paid: 0,
total_payable: 0,
created_at: new Date().toISOString()
}
});
} catch (error) {
console.error('更新供应商失败:', error);
res.status(500).json({
success: false,
message: '更新供应商失败',
error: error.message
});
}
});
router.delete('/:id', async (req, res) => {
try {
const { id } = req.params;
// 先删除关联的联系人数据
await db.query(`DELETE FROM contacts WHERE entity_id = $1 AND entity_type = 'supplier'`, [id]);
// 再删除供应商数据
const result = await db.query(`DELETE FROM suppliers WHERE id = $1`, [id]);
if (result.changes > 0) {
res.json({
success: true,
message: '供应商删除成功'
});
} else {
res.status(404).json({
success: false,
message: '供应商不存在'
});
}
} catch (error) {
console.error('删除供应商失败:', error);
res.status(500).json({
success: false,
message: '删除供应商失败',
error: error.message
});
}
});
router.get('/:id/orders', async (req, res) => {
try {
const { id } = req.params;
const ordersResult = await db.query(`
SELECT
po.id,
po.code,
po.project_id,
po.total_amount,
po.paid_amount,
(po.total_amount - po.paid_amount) as unpaid_amount,
po.order_date,
po.status,
po.currency,
p.name as project_name
FROM purchase_orders po
LEFT JOIN projects p ON po.project_id = p.id
WHERE po.supplier_id = ?
ORDER BY po.order_date DESC
`, [id]);
res.json({
success: true,
data: ordersResult.rows
});
} catch (error) {
console.error('获取供应商订单列表失败:', error);
res.status(500).json({
success: false,
message: '获取供应商订单列表失败',
error: error.message
});
}
});
router.get('/:id/ledger', async (req, res) => {
try {
const { id } = req.params;
const summaryResult = await db.query(`
SELECT
COUNT(*) as order_count,
COALESCE(SUM(total_amount), 0) as total_amount,
COALESCE(SUM(paid_amount), 0) as paid_amount,
COALESCE(SUM(total_amount - paid_amount), 0) as unpaid_amount
FROM purchase_orders
WHERE supplier_id = ?
`, [id]);
const ordersResult = await db.query(`
SELECT
po.id,
po.code,
po.project_id,
po.total_amount,
po.paid_amount,
(po.total_amount - po.paid_amount) as unpaid_amount,
po.order_date,
po.status,
po.currency,
p.name as project_name
FROM purchase_orders po
LEFT JOIN projects p ON po.project_id = p.id
WHERE po.supplier_id = ?
ORDER BY po.order_date DESC
`, [id]);
const summary = summaryResult.rows[0] || {
order_count: 0,
total_amount: 0,
paid_amount: 0,
unpaid_amount: 0
};
res.json({
success: true,
data: {
summary: {
order_count: summary.order_count || 0,
total_amount: summary.total_amount || 0,
paid_amount: summary.paid_amount || 0,
unpaid_amount: summary.unpaid_amount || 0
},
orders: ordersResult.rows
}
});
} catch (error) {
console.error('获取供应商台账失败:', error);
res.status(500).json({
success: false,
message: '获取供应商台账失败',
error: error.message
});
}
});
module.exports = router;
+122
View File
@@ -0,0 +1,122 @@
const express = require('express');
const multer = require('multer');
const path = require('path');
const COS = require('cos-nodejs-sdk-v5');
const router = express.Router();
const cos = new COS({
SecretId: 'AKID8PXTCi2A4vdB6oMitsfM3b1FNQL5kEVZ',
SecretKey: 'vY0OgmhRyKUSClBRXSIySmqkTUZZYdwo'
});
const cosConfig = {
Bucket: 'qingyuan-erp-files-1310040146',
Region: 'ap-hongkong'
};
const imageFormats = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'];
const upload = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 10 * 1024 * 1024 }
});
router.post('/upload/single', upload.single('file'), (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ success: false, error: '没有上传文件' });
}
const ext = req.file.originalname.split('.').pop().toLowerCase();
const timestamp = Date.now();
const randomStr = Math.random().toString(36).substring(2, 8);
const filename = 'uploads/' + timestamp + '_' + randomStr + '.' + ext;
console.log('开始上传文件到 COS:', filename);
cos.putObject({
Bucket: cosConfig.Bucket,
Region: cosConfig.Region,
Key: filename,
Body: req.file.buffer,
ContentType: req.file.mimetype,
ACL: 'public-read'
}, (err, data) => {
if (err) {
console.error('COS 上传失败:', err);
return res.status(500).json({ success: false, error: '上传失败: ' + err.message });
}
console.log('COS 上传成功:', data);
const permanentUrl = 'https://' + cosConfig.Bucket + '.cos.' + cosConfig.Region + '.myqcloud.com/' + filename;
console.log('返回永久 URL:', permanentUrl);
res.json({
success: true,
data: {
url: permanentUrl,
key: filename,
name: req.file.originalname,
size: req.file.size,
type: req.file.mimetype,
isImage: imageFormats.includes(ext)
},
message: '文件上传成功'
});
});
} catch (error) {
console.error('上传异常:', error);
res.status(500).json({ success: false, error: '上传失败' });
}
});
router.post('/upload/multiple', upload.array('files', 10), (req, res) => {
try {
if (!req.files || req.files.length === 0) {
return res.status(400).json({ success: false, error: '没有上传文件' });
}
const uploadPromises = req.files.map(file => {
return new Promise((resolve, reject) => {
const ext = file.originalname.split('.').pop().toLowerCase();
const filename = 'uploads/' + Date.now() + '_' + Math.random().toString(36).substring(2, 8) + '.' + ext;
cos.putObject({
Bucket: cosConfig.Bucket,
Region: cosConfig.Region,
Key: filename,
Body: file.buffer,
ContentType: file.mimetype,
ACL: 'public-read'
}, (err, data) => {
if (err) reject(err);
else {
const permanentUrl = 'https://' + cosConfig.Bucket + '.cos.' + cosConfig.Region + '.myqcloud.com/' + filename;
resolve({
url: permanentUrl,
key: filename,
name: file.originalname,
size: file.size,
isImage: imageFormats.includes(ext)
});
}
});
});
});
Promise.all(uploadPromises)
.then(results => res.json({ success: true, data: results }))
.catch(error => {
console.error('批量上传失败:', error);
res.status(500).json({ success: false, error: '上传失败' });
});
} catch (error) {
console.error('批量上传异常:', error);
res.status(500).json({ success: false, error: '上传失败' });
}
});
module.exports = router;
+153
View File
@@ -0,0 +1,153 @@
const express = require('express');
const router = express.Router();
const db = require('../db');
const { hashPassword, verifyPassword } = require('../utils/auth');
const { authenticate, requireAdmin } = require('../middleware/auth');
router.get('/', authenticate, requireAdmin, async (req, res) => {
try {
const usersResult = await db.query('SELECT id, username, name, email, phone, role, is_active, created_at, updated_at FROM users ORDER BY id');
const users = usersResult.rows;
res.json({ success: true, data: users, count: users.length });
} catch (error) {
console.error('获取用户列表失败:', error);
res.status(500).json({ success: false, message: '获取用户列表失败' });
}
});
router.post('/', authenticate, requireAdmin, async (req, res) => {
try {
const { username, name, email, phone, role, password } = req.body;
if (!username || !name || !password) {
return res.status(400).json({ success: false, message: '用户名、姓名和密码为必填项' });
}
const existingUser = await db.query('SELECT id FROM users WHERE username = $1', [username]);
if (existingUser.rows.length > 0) {
return res.status(400).json({ success: false, message: '用户名已存在' });
}
const passwordHash = hashPassword(password);
const result = await db.query(
'INSERT INTO users (username, password_hash, name, email, phone, role, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW()) RETURNING id',
[username, passwordHash, name, email || null, phone || null, role || 'employee']
);
const newUserResult = await db.query('SELECT id, username, name, email, phone, role, is_active, created_at, updated_at FROM users WHERE id = $1', [result.rows[0].id]);
const newUser = newUserResult.rows[0];
console.log('用户 ' + username + ' 创建成功,操作者: ' + req.user.username);
res.json({ success: true, data: newUser });
} catch (error) {
console.error('创建用户失败:', error);
res.status(500).json({ success: false, message: '创建用户失败' });
}
});
router.put('/:id', authenticate, requireAdmin, async (req, res) => {
try {
const { id } = req.params;
const { name, email, phone, role, password } = req.body;
if (password) {
const passwordHash = hashPassword(password);
await db.query(
'UPDATE users SET name = $1, email = $2, phone = $3, role = $4, password_hash = $5, updated_at = NOW() WHERE id = $6',
[name, email || null, phone || null, role, passwordHash, id]
);
} else {
await db.query(
'UPDATE users SET name = $1, email = $2, phone = $3, role = $4, updated_at = NOW() WHERE id = $5',
[name, email || null, phone || null, role, id]
);
}
const updatedUserResult = await db.query('SELECT id, username, name, email, phone, role, is_active, created_at, updated_at FROM users WHERE id = $1', [id]);
const updatedUser = updatedUserResult.rows[0];
if (!updatedUser) {
return res.status(404).json({ success: false, message: '用户不存在' });
}
console.log('用户 ID ' + id + ' 已更新,操作者: ' + req.user.username);
res.json({ success: true, data: updatedUser });
} catch (error) {
console.error('更新用户信息失败:', error);
res.status(500).json({ success: false, message: '更新用户信息失败' });
}
});
router.put('/:id/password', authenticate, async (req, res) => {
try {
const { id } = req.params;
const { currentPassword, newPassword } = req.body;
if (!newPassword) {
return res.status(400).json({ success: false, message: '新密码不能为空' });
}
const userResult = await db.query('SELECT id, password_hash FROM users WHERE id = $1', [id]);
if (userResult.rows.length === 0) {
return res.status(404).json({ success: false, message: '用户不存在' });
}
const user = userResult.rows[0];
const isAdmin = req.user.role === 'admin';
const isSelf = req.user.id === parseInt(id);
if (isSelf && currentPassword) {
if (!verifyPassword(currentPassword, user.password_hash)) {
return res.status(400).json({ success: false, message: '当前密码错误' });
}
} else if (!isAdmin) {
return res.status(403).json({ success: false, message: '只能修改自己的密码' });
}
const newPasswordHash = hashPassword(newPassword);
await db.query(
'UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2',
[newPasswordHash, id]
);
console.log('用户 ID ' + id + ' 密码已更新,操作者: ' + req.user.username);
res.json({ success: true, message: '密码更新成功' });
} catch (error) {
console.error('更新密码失败:', error);
res.status(500).json({ success: false, message: '更新密码失败' });
}
});
router.delete('/:id', authenticate, requireAdmin, async (req, res) => {
try {
const { id } = req.params;
if (parseInt(id) === req.user.id) {
return res.status(400).json({ success: false, message: '不能删除自己' });
}
const existingUser = await db.query('SELECT id, role FROM users WHERE id = $1', [id]);
if (existingUser.rows.length === 0) {
return res.status(404).json({ success: false, message: '用户不存在' });
}
if (existingUser.rows[0].role === 'admin') {
const adminCount = await db.query("SELECT COUNT(*) as cnt FROM users WHERE role = 'admin'");
if (parseInt(adminCount.rows[0].cnt) <= 1) {
return res.status(400).json({ success: false, message: '至少需要保留一个管理员账号' });
}
}
await db.query('DELETE FROM users WHERE id = $1', [id]);
console.log('用户 ID ' + id + ' 已删除,操作者: ' + req.user.username);
res.json({ success: true, message: '用户删除成功' });
} catch (error) {
console.error('删除用户失败:', error);
res.status(500).json({ success: false, message: '删除用户失败' });
}
});
module.exports = router;
+432
View File
@@ -0,0 +1,432 @@
/**
* 验收管理路由
* 遵循设计方案:采购-付款-物流-退库一体化流程设计方案.md
* 章节:八、验收管理功能
*
* 功能:
* - 支持一次验收(直接验收)和二次验收(经集散地后验收)
* - 支持部分签收
* - 验收后自动更新项目材料库存
*/
const express = require('express');
const db = require('../db');
const router = express.Router();
/**
* 获取验收单列表
*/
router.get('/', async (req, res) => {
try {
const { purchase_order_id, project_id, status } = req.query;
let query = `
SELECT vr.*,
po.code as order_code,
p.name as project_name
FROM verification_records vr
LEFT JOIN purchase_orders po ON vr.purchase_order_id = po.id
LEFT JOIN projects p ON vr.project_id = p.id
`;
const params = [];
const conditions = [];
if (purchase_order_id) {
conditions.push('vr.purchase_order_id = $1');
params.push(purchase_order_id);
}
if (project_id) {
conditions.push('vr.project_id = $1');
params.push(project_id);
}
if (status) {
conditions.push('vr.status = $1');
params.push(status);
}
if (conditions.length > 0) {
query += ' WHERE ' + conditions.join(' AND ');
}
query += ' ORDER BY vr.created_at DESC';
const result = await db.query(query, params);
res.json({
success: true,
data: result.rows,
count: result.rows.length
});
} catch (error) {
console.error('获取验收单列表失败:', error);
res.status(500).json({
success: false,
message: '获取验收单列表失败',
error: error.message
});
}
});
/**
* 获取验收单详情
*/
router.get('/:id', async (req, res) => {
try {
const { id } = req.params;
const result = await db.query(`
SELECT vr.*,
po.code as order_code,
p.name as project_name
FROM verification_records vr
LEFT JOIN purchase_orders po ON vr.purchase_order_id = po.id
LEFT JOIN projects p ON vr.project_id = p.id
WHERE vr.id = ?
`, [id]);
if (result.rows.length === 0) {
return res.status(404).json({ success: false, message: '验收单不存在' });
}
const verification = result.rows[0];
if (verification.items) {
try {
verification.items = JSON.parse(verification.items);
} catch (e) {
verification.items = [];
}
} else {
verification.items = [];
}
res.json({
success: true,
data: verification
});
} catch (error) {
console.error('获取验收单详情失败:', error);
res.status(500).json({
success: false,
message: '获取验收单详情失败',
error: error.message
});
}
});
/**
* 创建验收单
* 遵循设计方案:支持一次验收/二次验收,支持部分签收
*/
router.post('/', async (req, res) => {
try {
const {
purchase_order_id, logistics_record_id, verification_type,
verification_date, verifier, items, project_id, storage_type,
remark, attachments
} = req.body;
const code = 'VR' + new Date().toISOString().slice(0, 10).replace(/-/g, '') +
String(Math.floor(Math.random() * 10000)).padStart(4, '0');
const itemsJson = items ? JSON.stringify(items) : null;
let totalOrdered = 0;
let totalReceived = 0;
let totalVerified = 0;
let totalRejected = 0;
if (items && Array.isArray(items)) {
for (const item of items) {
totalOrdered += item.ordered_quantity || 0;
totalReceived += item.received_quantity || 0;
totalVerified += item.verified_quantity || 0;
totalRejected += item.rejected_quantity || 0;
}
}
await db.query('BEGIN TRANSACTION');
try {
const result = await db.query(`
INSERT INTO verification_records
(code, purchase_order_id, logistics_record_id, verification_type,
verification_date, verifier, items, total_ordered, total_received,
total_verified, total_rejected, project_id, storage_type, status, remark, attachments, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?, CURRENT_TIMESTAMP)
`, [code, purchase_order_id, logistics_record_id, verification_type || 'direct',
verification_date, verifier, itemsJson, totalOrdered, totalReceived,
totalVerified, totalRejected, project_id, storage_type, remark, attachments]);
await db.query('COMMIT');
res.json({
success: true,
message: '验收单创建成功',
data: { id: (result.rows[0]?.id || result.rows?.[0]?.id), code }
});
} catch (innerError) {
await db.query('ROLLBACK');
throw innerError;
}
} catch (error) {
console.error('创建验收单失败:', error);
res.status(500).json({
success: false,
message: '创建验收单失败',
error: error.message
});
}
});
/**
* 确认验收
* 遵循设计方案:验收通过后自动更新项目材料库存
*/
router.post('/:id/confirm', async (req, res) => {
try {
const { id } = req.params;
const verificationResult = await db.query('SELECT * FROM verification_records WHERE id = $1', [id]);
if (verificationResult.rows.length === 0) {
return res.status(404).json({ success: false, message: '验收单不存在' });
}
const verification = verificationResult.rows[0];
if (verification.status !== 'pending') {
return res.status(400).json({ success: false, message: '只能确认待审核状态的验收单' });
}
await db.query('BEGIN TRANSACTION');
try {
await db.query("UPDATE verification_records SET status = 'confirmed' WHERE id = ?", [id]);
if (verification.items) {
let items;
try {
items = JSON.parse(verification.items);
} catch (e) {
items = [];
}
for (const item of items) {
if (item.verified_quantity > 0 && item.product_id) {
const existingInventory = await db.query(`
SELECT * FROM project_material_inventory
WHERE project_id = ? AND product_id = ?
`, [verification.project_id, item.product_id]);
if (existingInventory.rows.length > 0) {
const existing = existingInventory.rows[0];
const newReceivedQty = (existing.received_quantity || 0) + item.verified_quantity;
const newCurrentQty = (existing.current_quantity || 0) + item.verified_quantity;
const newTotalAmount = (existing.total_amount || 0) + (item.verified_quantity * item.unit_price || 0);
const newAvgPrice = newCurrentQty > 0 ? newTotalAmount / newCurrentQty : 0;
await db.query(`
UPDATE project_material_inventory
SET received_quantity = ?, current_quantity = ?, total_amount = ?, average_price = ?, updated_at = CURRENT_TIMESTAMP
WHERE project_id = ? AND product_id = ?
`, [newReceivedQty, newCurrentQty, newTotalAmount, newAvgPrice, verification.project_id, item.product_id]);
} else {
await db.query(`
INSERT INTO project_material_inventory
(project_id, product_id, product_name, unit, received_quantity, current_quantity, total_amount, average_price, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
`, [verification.project_id, item.product_id, item.product_name, item.unit,
item.verified_quantity, item.verified_quantity,
item.verified_quantity * item.unit_price || 0, item.unit_price || 0]);
}
}
}
}
await db.query(`
UPDATE purchase_orders
SET status = 'verified', updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`, [verification.purchase_order_id]);
await db.query('COMMIT');
res.json({
success: true,
message: '验收确认成功,已更新项目材料库存'
});
} catch (innerError) {
await db.query('ROLLBACK');
throw innerError;
}
} catch (error) {
console.error('确认验收失败:', error);
res.status(500).json({
success: false,
message: '确认验收失败',
error: error.message
});
}
});
/**
* 驳回验收
*/
router.post('/:id/reject', async (req, res) => {
try {
const { id } = req.params;
const { reason } = req.body;
const result = await db.query(`
UPDATE verification_records
SET status = 'rejected', remark = COALESCE(remark || ' | ', '') || '驳回原因: ' || ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`, [reason || '无', id]);
if (result.changes === 0) {
return res.status(404).json({ success: false, message: '验收单不存在' });
}
res.json({ success: true, message: '验收已驳回' });
} catch (error) {
console.error('驳回验收失败:', error);
res.status(500).json({ success: false, message: '驳回验收失败', error: error.message });
}
});
/**
* 更新验收单
*/
router.put('/:id', async (req, res) => {
try {
const { id } = req.params;
const { items, verification_date, verifier, remark, attachments } = req.body;
const verificationResult = await db.query('SELECT * FROM verification_records WHERE id = $1', [id]);
if (verificationResult.rows.length === 0) {
return res.status(404).json({ success: false, message: '验收单不存在' });
}
const verification = verificationResult.rows[0];
if (verification.status !== 'pending') {
return res.status(400).json({ success: false, message: '只能修改待审核状态的验收单' });
}
const itemsJson = items ? JSON.stringify(items) : null;
let totalOrdered = 0;
let totalReceived = 0;
let totalVerified = 0;
let totalRejected = 0;
if (items && Array.isArray(items)) {
for (const item of items) {
totalOrdered += item.ordered_quantity || 0;
totalReceived += item.received_quantity || 0;
totalVerified += item.verified_quantity || 0;
totalRejected += item.rejected_quantity || 0;
}
}
await db.query(`
UPDATE verification_records
SET items = ?, verification_date = ?, verifier = ?,
total_ordered = ?, total_received = ?, total_verified = ?, total_rejected = ?,
remark = ?, attachments = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`, [itemsJson, verification_date, verifier, totalOrdered, totalReceived, totalVerified, totalRejected, remark, attachments, id]);
res.json({ success: true, message: '验收单更新成功' });
} catch (error) {
console.error('更新验收单失败:', error);
res.status(500).json({
success: false,
message: '更新验收单失败',
error: error.message
});
}
});
/**
* 删除验收单
*/
router.delete('/:id', async (req, res) => {
try {
const { id } = req.params;
const verificationResult = await db.query('SELECT * FROM verification_records WHERE id = $1', [id]);
if (verificationResult.rows.length === 0) {
return res.status(404).json({ success: false, message: '验收单不存在' });
}
const verification = verificationResult.rows[0];
if (verification.status !== 'pending') {
return res.status(400).json({ success: false, message: '只能删除待审核状态的验收单' });
}
await db.query('DELETE FROM verification_records WHERE id = $1', [id]);
res.json({ success: true, message: '验收单删除成功' });
} catch (error) {
console.error('删除验收单失败:', error);
res.status(500).json({
success: false,
message: '删除验收单失败',
error: error.message
});
}
});
/**
* 获取订单可验收的商品明细
*/
router.get('/order-items/:orderId', async (req, res) => {
try {
const { orderId } = req.params;
const itemsResult = await db.query(`
SELECT poi.*, p.name as product_name
FROM purchase_order_items poi
LEFT JOIN products p ON poi.product_id = p.id
WHERE poi.order_id = ?
`, [orderId]);
const verifiedResult = await db.query(`
SELECT items
FROM verification_records
WHERE purchase_order_id = ? AND status = 'confirmed'
`, [orderId]);
const verifiedQty = {};
for (const row of verifiedResult.rows) {
if (row.items) {
try {
const items = JSON.parse(row.items);
for (const item of items) {
const key = item.product_id || item.product_name;
verifiedQty[key] = (verifiedQty[key] || 0) + (item.verified_quantity || 0);
}
} catch (e) {}
}
}
const items = itemsResult.rows.map(item => ({
...item,
already_verified: verifiedQty[item.product_id || item.product_name] || 0,
pending_verify: (item.quantity || 0) - (verifiedQty[item.product_id || item.product_name] || 0)
}));
res.json({
success: true,
data: items
});
} catch (error) {
console.error('获取订单商品明细失败:', error);
res.status(500).json({
success: false,
message: '获取订单商品明细失败',
error: error.message
});
}
});
module.exports = router;
+365
View File
@@ -0,0 +1,365 @@
const express = require('express');
const db = require('../db');
const { authenticate, requireAdmin } = require('../middleware/auth');
const router = express.Router();
router.get('/', async (req, res) => {
try {
const { advance_id } = req.query;
let query = `
SELECT v.*, a.advance_code, a.applicant_id as advance_applicant_id
FROM verifications v
LEFT JOIN advances a ON v.advance_id = a.id
`;
const params = [];
if (advance_id) {
query += ` WHERE v.advance_id = $1`;
params.push(advance_id);
}
query += ` ORDER BY v.created_at DESC`;
const result = await db.query(query, params);
const data = result.rows.map(item => {
if (item.attachments) {
try {
item.attachments = JSON.parse(item.attachments);
} catch (error) {
item.attachments = [];
}
} else {
item.attachments = [];
}
if (item.detail_items) {
try {
item.detail_items = JSON.parse(item.detail_items);
} catch (error) {
item.detail_items = [];
}
} else {
item.detail_items = [];
}
return item;
});
res.json({ success: true, data, count: data.length });
} catch (error) {
console.error('获取核销记录失败:', error);
res.status(500).json({ success: false, message: '获取核销记录失败', error: error.message });
}
});
router.post('/', async (req, res) => {
try {
const { verification_date, advance_id, advance_code, advance_amount, currency, reason, detail_items, attachments, applicant, expense_type, project_id, settlement, settlement_amount } = req.body;
// 生成核销编号
const verificationCode = `VER-${Date.now()}`;
const amount = detail_items?.reduce((sum, item) => sum + (item.amount || 0), 0) || 0;
// 验证关联预支单
if (!advance_id && !advance_code) {
return res.status(400).json({ success: false, message: '关联预支单是必填项' });
}
let finalAdvanceCode = advance_code;
let finalAdvanceId = advance_id;
// 如果advance_code为空,根据advance_id查询预支单的advance_code
if (!finalAdvanceCode && finalAdvanceId) {
const advanceResult = await db.query('SELECT advance_code FROM advances WHERE id = $1', [finalAdvanceId]);
if (advanceResult.rows.length > 0) {
finalAdvanceCode = advanceResult.rows[0].advance_code;
} else {
return res.status(400).json({ success: false, message: '关联的预支单不存在' });
}
}
// 如果advance_id为空,根据advance_code查询预支单的id
if (!finalAdvanceId && finalAdvanceCode) {
const advanceResult = await db.query('SELECT id FROM advances WHERE advance_code = $1', [finalAdvanceCode]);
if (advanceResult.rows.length > 0) {
finalAdvanceId = advanceResult.rows[0].id;
} else {
return res.status(400).json({ success: false, message: '关联的预支单不存在' });
}
}
// 如果仍然为空,返回错误
if (!finalAdvanceCode || !finalAdvanceId) {
return res.status(400).json({ success: false, message: '关联预支单不存在' });
}
// 开始事务
await db.query('BEGIN TRANSACTION');
try {
// 插入核销申请
const result = await db.query(
'INSERT INTO verifications (advance_id, amount, currency, reason, verification_date, verification_code, status, applicant, advance_code, advance_amount, detail_items, attachments, expense_type, project_id, settlement, settlement_amount) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)',
[finalAdvanceId, amount, currency || 'CNY', reason, verification_date, verificationCode, 'pending', applicant, finalAdvanceCode, advance_amount || 0, JSON.stringify(detail_items || []), JSON.stringify(attachments || []), expense_type || 'company', project_id, settlement ? 1 : 0, settlement_amount || 0]
);
// 提交事务
await db.query('COMMIT');
// SQLite不支持RETURNING,所以需要查询刚插入的数据
const lastInsert = await db.query('SELECT * FROM verifications ORDER BY id DESC LIMIT 1');
res.json({ success: true, data: lastInsert.rows[0] });
} catch (error) {
// 回滚事务
await db.query('ROLLBACK');
throw error;
}
} catch (error) {
console.error('创建核销申请失败:', error);
res.status(500).json({ success: false, message: '创建核销申请失败', error: error.message });
}
});
router.get('/:id', async (req, res) => {
try {
const { id } = req.params;
const result = await db.query('SELECT * FROM verifications WHERE id = $1', [id]);
if (result.rows.length > 0) {
const data = result.rows[0];
if (data.attachments) {
try {
data.attachments = JSON.parse(data.attachments);
} catch (error) {
data.attachments = [];
}
} else {
data.attachments = [];
}
if (data.detail_items) {
try {
data.detail_items = JSON.parse(data.detail_items);
} catch (error) {
data.detail_items = [];
}
} else {
data.detail_items = [];
}
res.json({ success: true, data });
} else {
res.status(404).json({ success: false, message: '核销申请不存在' });
}
} catch (error) {
console.error('获取核销申请失败:', error);
res.status(500).json({ success: false, message: '获取核销申请失败', error: error.message });
}
});
router.put('/:id', async (req, res) => {
try {
const { id } = req.params;
const { verification_date, advance_id, advance_code, advance_amount, currency, reason, detail_items, attachments, applicant, status, expense_type, project_id, settlement, settlement_amount } = req.body;
const amount = detail_items?.reduce((sum, item) => sum + (item.amount || 0), 0) || 0;
// 开始事务
await db.query('BEGIN TRANSACTION');
try {
// 获取原核销金额
const oldVerification = await db.query('SELECT amount, advance_id FROM verifications WHERE id = $1', [id]);
const oldAmount = oldVerification.rows[0]?.amount || 0;
const oldAdvanceId = oldVerification.rows[0]?.advance_id;
let finalAdvanceCode = advance_code;
// 如果advance_code为空,根据advance_id查询预支单的advance_code
if (!finalAdvanceCode && advance_id) {
const advanceResult = await db.query('SELECT advance_code FROM advances WHERE id = $1', [advance_id]);
if (advanceResult.rows.length > 0) {
finalAdvanceCode = advanceResult.rows[0].advance_code;
}
}
// 如果仍然为空,使用默认值
if (!finalAdvanceCode) {
finalAdvanceCode = 'UNKNOWN';
}
// 更新核销申请
const result = await db.query(
'UPDATE verifications SET verification_date = $1, advance_id = $2, amount = $3, currency = $4, reason = $5, advance_code = $6, advance_amount = $7, detail_items = $8, attachments = $9, applicant = $10, status = $11, expense_type = $12, project_id = $13, settlement = $14, settlement_amount = $15 WHERE id = $16',
[verification_date, advance_id, amount, currency, reason, finalAdvanceCode, advance_amount || 0, JSON.stringify(detail_items || []), JSON.stringify(attachments || []), applicant, status, expense_type || 'company', project_id, settlement ? 1 : 0, settlement_amount || 0, id]
);
// 不在这里更新预支单已核销金额,而是在执行核销时更新
// if (oldAdvanceId) {
// const amountDiff = amount - oldAmount;
// if (amountDiff !== 0) {
// await db.query(
// 'UPDATE advances SET total_reimbursed = total_reimbursed + $1 WHERE id = $2',
// [amountDiff, oldAdvanceId]
// );
// }
// }
// 提交事务
await db.query('COMMIT');
if (result.changes > 0) {
res.json({ success: true, message: '更新成功' });
} else {
res.status(404).json({ success: false, message: '核销申请不存在' });
}
} catch (error) {
// 回滚事务
await db.query('ROLLBACK');
throw error;
}
} catch (error) {
console.error('更新核销申请失败:', error);
res.status(500).json({ success: false, message: '更新核销申请失败', error: error.message });
}
});
router.delete('/:id', async (req, res) => {
try {
const { id } = req.params;
// 开始事务
await db.query('BEGIN TRANSACTION');
try {
// 获取核销金额和预支单ID
const verification = await db.query('SELECT amount, advance_id FROM verifications WHERE id = $1', [id]);
const amount = verification.rows[0]?.amount || 0;
const advanceId = verification.rows[0]?.advance_id;
// 删除核销申请
const result = await db.query('DELETE FROM verifications WHERE id = $1', [id]);
// 不在这里恢复预支单已核销金额,因为只有执行的核销才会增加已核销金额
// if (advanceId && amount > 0) {
// await db.query(
// 'UPDATE advances SET total_reimbursed = total_reimbursed - $1 WHERE id = $2',
// [amount, advanceId]
// );
// }
// 提交事务
await db.query('COMMIT');
if (result.changes > 0) {
res.json({ success: true, message: '删除成功' });
} else {
res.status(404).json({ success: false, message: '核销申请不存在' });
}
} catch (error) {
// 回滚事务
await db.query('ROLLBACK');
throw error;
}
} catch (error) {
console.error('删除核销申请失败:', error);
res.status(500).json({ success: false, message: '删除核销申请失败', error: error.message });
}
});
router.post('/:id/submit', async (req, res) => {
try {
const { id } = req.params;
const result = await db.query('UPDATE verifications SET status = $1 WHERE id = $2', ['pending', id]);
if (result.changes > 0) {
res.json({ success: true, message: '提交成功' });
} else {
res.status(404).json({ success: false, message: '核销申请不存在' });
}
} catch (error) {
console.error('提交核销申请失败:', error);
res.status(500).json({ success: false, message: '提交核销申请失败', error: error.message });
}
});
router.post('/:id/withdraw', async (req, res) => {
try {
const { id } = req.params;
const result = await db.query('UPDATE verifications SET status = $1 WHERE id = $2', ['withdrawn', id]);
if (result.changes > 0) {
res.json({ success: true, message: '撤回成功' });
} else {
res.status(404).json({ success: false, message: '核销申请不存在' });
}
} catch (error) {
console.error('撤回核销申请失败:', error);
res.status(500).json({ success: false, message: '撤回核销申请失败', error: error.message });
}
});
router.post('/:id/approve', async (req, res) => {
try {
const { id } = req.params;
const { remark } = req.body;
const result = await db.query('UPDATE verifications SET status = $1, approval_remark = $2 WHERE id = $3', ['approved', remark, id]);
if (result.changes > 0) {
res.json({ success: true, message: '审批通过成功' });
} else {
res.status(404).json({ success: false, message: '核销申请不存在' });
}
} catch (error) {
console.error('审批核销申请失败:', error);
res.status(500).json({ success: false, message: '审批核销申请失败', error: error.message });
}
});
router.post('/:id/reject', async (req, res) => {
try {
const { id } = req.params;
const { rejectReason } = req.body;
// 开始事务
await db.query('BEGIN TRANSACTION');
try {
// 获取核销金额和预支单ID
const verification = await db.query('SELECT amount, advance_id FROM verifications WHERE id = $1', [id]);
const amount = verification.rows[0]?.amount || 0;
const advanceId = verification.rows[0]?.advance_id;
// 退回核销申请
const result = await db.query('UPDATE verifications SET status = $1 WHERE id = $2', ['pending_edit', id]);
// 不在这里恢复预支单已核销金额,因为只有执行的核销才会增加已核销金额
// if (advanceId && amount > 0) {
// await db.query(
// 'UPDATE advances SET total_reimbursed = total_reimbursed - $1 WHERE id = $2',
// [amount, advanceId]
// );
// }
// 提交事务
await db.query('COMMIT');
if (result.changes > 0) {
res.json({ success: true, message: '退回成功' });
} else {
res.status(404).json({ success: false, message: '核销申请不存在' });
}
} catch (error) {
// 回滚事务
await db.query('ROLLBACK');
throw error;
}
} catch (error) {
console.error('退回核销申请失败:', error);
res.status(500).json({ success: false, message: '退回核销申请失败', error: error.message });
}
});
module.exports = router;