备份:修复前完整项目快照 2026-04-19
This commit is contained in:
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user