财务分类体系+统一账本+Excel导入+报表改造
This commit is contained in:
@@ -67,6 +67,8 @@ app.use('/api/returns', require('./routes/returns'));
|
||||
app.use('/api/project-materials', require('./routes/project-materials'));
|
||||
app.use('/api/process-templates', require('./routes/process-templates'));
|
||||
app.use('/api/receiving', require('./routes/receiving'));
|
||||
app.use('/api/expense-categories', require('./routes/expense-categories'));
|
||||
app.use('/api/financial-records', require('./routes/financial-records'));
|
||||
|
||||
app.get('*', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, '../frontend/dist/index.html'));
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
-- 创建财务分类配置表
|
||||
CREATE TABLE IF NOT EXISTS expense_categories (
|
||||
id SERIAL PRIMARY KEY,
|
||||
category_level1 TEXT NOT NULL,
|
||||
category_level2 TEXT NOT NULL,
|
||||
label TEXT NOT NULL,
|
||||
description TEXT,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(category_level1, category_level2)
|
||||
);
|
||||
|
||||
-- 收入分类
|
||||
INSERT INTO expense_categories (category_level1, category_level2, label, description, sort_order) VALUES
|
||||
('income', 'contract_payment', '项目合同收款', '客户按合同打款到账', 1),
|
||||
('income', 'deposit_refund', '质保金退回', '项目质保期满退回的质保金', 2),
|
||||
('income', 'shareholder_investment', '股东投资入股', '股东注入的资金/股本', 3),
|
||||
('income', 'other_income', '其他收入', '利息、退税、资产处置等', 4);
|
||||
|
||||
-- 项目支出分类
|
||||
INSERT INTO expense_categories (category_level1, category_level2, label, description, sort_order) VALUES
|
||||
('project', 'material', '材料采购', '电杆、导线、电缆、横担、金具、水泥等', 1),
|
||||
('project', 'equipment', '设备采购', '变压器、开关柜、配电箱、计量箱等', 2),
|
||||
('project', 'subcontract', '施工分包', '外包施工队的劳务/工程费', 3),
|
||||
('project', 'labor', '人工工资', '项目现场工人、项目经理的工资或劳务费', 4),
|
||||
('project', 'travel', '差旅交通', '驻现场人员路费,含跨境交通、油费', 5),
|
||||
('project', 'accommodation', '食宿费用', '驻现场人员住宿费、餐饮费', 6),
|
||||
('project', 'freight', '运输物流', '材料/设备一次运输、二次转运', 7),
|
||||
('project', 'design', '勘测设计', '现场勘测、图纸设计费', 8),
|
||||
('project', 'tools', '小型工具', '零散小工具,非固定资产', 9),
|
||||
('project', 'client_relations', '客户/EDL关系', '项目相关的客户招待、电力局关系', 10),
|
||||
('project', 'other_project', '其他项目支出', '保险、临时杂费', 11);
|
||||
|
||||
-- 公司支出分类
|
||||
INSERT INTO expense_categories (category_level1, category_level2, label, description, sort_order) VALUES
|
||||
('company', 'salary', '工资薪酬', '办公室管理人员工资、社保、公积金', 1),
|
||||
('company', 'rent', '房租物业', '办公室/仓库租金、物业费、水电', 2),
|
||||
('company', 'office', '办公费用', '办公用品、打印耗材、网络、电话', 3),
|
||||
('company', 'commute', '交通通勤', '日常市内交通、公司车辆油费', 4),
|
||||
('company', 'vehicle_maintenance', '车辆维保', '车辆维修、保养、配件更换', 5),
|
||||
('company', 'assets', '固定资产', '购买车辆、大型工具、办公设备', 6),
|
||||
('company', 'marketing', '营销拓展', '业务拓展、广告推广、展会', 7),
|
||||
('company', 'entertainment', '招待费用', '公司级客户/合作方招待', 8),
|
||||
('company', 'welfare', '员工福利', '聚餐、团建、节日礼品、体检', 9),
|
||||
('company', 'logistics', '快递物流', '文件、样品快递费', 10),
|
||||
('company', 'other_company', '其他公司支出', '中介佣金、税费等', 11);
|
||||
|
||||
-- 索引
|
||||
CREATE INDEX IF NOT EXISTS idx_expense_categories_level1 ON expense_categories(category_level1);
|
||||
CREATE INDEX IF NOT EXISTS idx_expense_categories_active ON expense_categories(is_active);
|
||||
@@ -0,0 +1,44 @@
|
||||
-- 创建统一财务账本表
|
||||
CREATE TABLE IF NOT EXISTS financial_records (
|
||||
id SERIAL PRIMARY KEY,
|
||||
record_code TEXT UNIQUE NOT NULL,
|
||||
|
||||
txn_type TEXT NOT NULL CHECK (txn_type IN ('income', 'expense')),
|
||||
category_level1 TEXT NOT NULL,
|
||||
category_level2 TEXT NOT NULL,
|
||||
|
||||
project_id INTEGER REFERENCES projects(id),
|
||||
user_id INTEGER REFERENCES users(id),
|
||||
user_name TEXT,
|
||||
|
||||
amount_original REAL NOT NULL DEFAULT 0,
|
||||
currency TEXT NOT NULL DEFAULT 'CNY',
|
||||
exchange_rate REAL NOT NULL DEFAULT 1,
|
||||
amount_cny REAL NOT NULL DEFAULT 0,
|
||||
|
||||
record_date DATE NOT NULL,
|
||||
|
||||
counterparty_name TEXT,
|
||||
counterparty_type TEXT,
|
||||
counterparty_id INTEGER,
|
||||
|
||||
source TEXT NOT NULL DEFAULT 'manual',
|
||||
source_id INTEGER,
|
||||
source_code TEXT,
|
||||
|
||||
description TEXT,
|
||||
voucher_no TEXT,
|
||||
attachments TEXT,
|
||||
|
||||
status TEXT NOT NULL DEFAULT 'confirmed' CHECK (status IN ('confirmed', 'pending', 'voided')),
|
||||
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_financial_records_project ON financial_records(project_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_financial_records_user ON financial_records(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_financial_records_date ON financial_records(record_date);
|
||||
CREATE INDEX IF NOT EXISTS idx_financial_records_type ON financial_records(txn_type, category_level1, category_level2);
|
||||
CREATE INDEX IF NOT EXISTS idx_financial_records_source ON financial_records(source, source_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_financial_records_status ON financial_records(status);
|
||||
@@ -0,0 +1,122 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const db = require('../db');
|
||||
const { authenticate } = require('../middleware/auth');
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const { level1, active } = req.query;
|
||||
let sql = 'SELECT * FROM expense_categories WHERE 1=1';
|
||||
const params = [];
|
||||
if (level1) {
|
||||
params.push(level1);
|
||||
sql += ` AND category_level1 = $${params.length}`;
|
||||
}
|
||||
if (active !== undefined) {
|
||||
params.push(active === 'true');
|
||||
sql += ` AND is_active = $${params.length}`;
|
||||
}
|
||||
sql += ' ORDER BY category_level1, sort_order';
|
||||
const result = await db.query(sql, params);
|
||||
res.json({ success: true, data: result.rows });
|
||||
} catch (error) {
|
||||
console.error('获取分类失败:', error);
|
||||
res.status(500).json({ success: false, message: '获取分类失败' });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/grouped', async (req, res) => {
|
||||
try {
|
||||
const result = await db.query('SELECT * FROM expense_categories WHERE is_active = true ORDER BY category_level1, sort_order');
|
||||
const grouped = {
|
||||
income: [],
|
||||
project: [],
|
||||
company: []
|
||||
};
|
||||
result.rows.forEach(row => {
|
||||
if (grouped[row.category_level1]) {
|
||||
grouped[row.category_level1].push({
|
||||
value: row.category_level2,
|
||||
label: row.label,
|
||||
description: row.description
|
||||
});
|
||||
}
|
||||
});
|
||||
res.json({ success: true, data: grouped });
|
||||
} catch (error) {
|
||||
console.error('获取分组分类失败:', error);
|
||||
res.status(500).json({ success: false, message: '获取分组分类失败' });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/', authenticate, async (req, res) => {
|
||||
try {
|
||||
if (req.user.role !== 'admin') {
|
||||
return res.status(403).json({ success: false, message: '无权限操作' });
|
||||
}
|
||||
const { category_level1, category_level2, label, description, sort_order } = req.body;
|
||||
if (!category_level1 || !category_level2 || !label) {
|
||||
return res.status(400).json({ success: false, message: '缺少必填字段' });
|
||||
}
|
||||
const result = await db.query(
|
||||
`INSERT INTO expense_categories (category_level1, category_level2, label, description, sort_order)
|
||||
VALUES ($1, $2, $3, $4, $5) RETURNING *`,
|
||||
[category_level1, category_level2, label, description, sort_order || 0]
|
||||
);
|
||||
res.json({ success: true, data: result.rows[0] });
|
||||
} catch (error) {
|
||||
console.error('创建分类失败:', error);
|
||||
if (error.code === '23505') {
|
||||
return res.status(400).json({ success: false, message: '分类已存在' });
|
||||
}
|
||||
res.status(500).json({ success: false, message: '创建分类失败' });
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/:id', authenticate, async (req, res) => {
|
||||
try {
|
||||
if (req.user.role !== 'admin') {
|
||||
return res.status(403).json({ success: false, message: '无权限操作' });
|
||||
}
|
||||
const { id } = req.params;
|
||||
const { category_level1, category_level2, label, description, sort_order, is_active } = req.body;
|
||||
const result = await db.query(
|
||||
`UPDATE expense_categories SET
|
||||
category_level1 = COALESCE($1, category_level1),
|
||||
category_level2 = COALESCE($2, category_level2),
|
||||
label = COALESCE($3, label),
|
||||
description = COALESCE($4, description),
|
||||
sort_order = COALESCE($5, sort_order),
|
||||
is_active = COALESCE($6, is_active),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $7 RETURNING *`,
|
||||
[category_level1, category_level2, label, description, sort_order, is_active, 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: '更新分类失败' });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/:id', authenticate, async (req, res) => {
|
||||
try {
|
||||
if (req.user.role !== 'admin') {
|
||||
return res.status(403).json({ success: false, message: '无权限操作' });
|
||||
}
|
||||
const { id } = req.params;
|
||||
const result = await db.query('DELETE FROM expense_categories WHERE id = $1 RETURNING *', [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: '删除分类失败' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,340 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const db = require('../db');
|
||||
const { authenticate } = require('../middleware/auth');
|
||||
|
||||
function generateCode(prefix) {
|
||||
const now = new Date();
|
||||
const dateStr = now.getFullYear().toString() +
|
||||
(now.getMonth() + 1).toString().padStart(2, '0') +
|
||||
now.getDate().toString().padStart(2, '0');
|
||||
const rand = Math.floor(Math.random() * 10000).toString().padStart(4, '0');
|
||||
return `${prefix}-${dateStr}-${rand}`;
|
||||
}
|
||||
|
||||
router.get('/', authenticate, async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
txn_type, category_level1, category_level2,
|
||||
project_id, user_id, source,
|
||||
date_from, date_to,
|
||||
page = 1, pageSize = 50
|
||||
} = req.query;
|
||||
|
||||
let sql = 'SELECT fr.*, p.name as project_name FROM financial_records fr LEFT JOIN projects p ON fr.project_id = p.id WHERE 1=1';
|
||||
const params = [];
|
||||
let paramIdx = 1;
|
||||
|
||||
if (txn_type) { params.push(txn_type); sql += ` AND fr.txn_type = $${paramIdx++}`; }
|
||||
if (category_level1) { params.push(category_level1); sql += ` AND fr.category_level1 = $${paramIdx++}`; }
|
||||
if (category_level2) { params.push(category_level2); sql += ` AND fr.category_level2 = $${paramIdx++}`; }
|
||||
if (project_id) { params.push(project_id); sql += ` AND fr.project_id = $${paramIdx++}`; }
|
||||
if (user_id) { params.push(user_id); sql += ` AND fr.user_id = $${paramIdx++}`; }
|
||||
if (source) { params.push(source); sql += ` AND fr.source = $${paramIdx++}`; }
|
||||
if (date_from) { params.push(date_from); sql += ` AND fr.record_date >= $${paramIdx++}`; }
|
||||
if (date_to) { params.push(date_to); sql += ` AND fr.record_date <= $${paramIdx++}`; }
|
||||
|
||||
const countResult = await db.query(`SELECT COUNT(*) as total FROM (${sql}) sub`, params);
|
||||
const total = parseInt(countResult.rows[0].total);
|
||||
|
||||
sql += ' ORDER BY fr.record_date DESC, fr.created_at DESC';
|
||||
const offset = (parseInt(page) - 1) * parseInt(pageSize);
|
||||
params.push(parseInt(pageSize));
|
||||
sql += ` LIMIT $${paramIdx++}`;
|
||||
params.push(offset);
|
||||
sql += ` OFFSET $${paramIdx++}`;
|
||||
|
||||
const result = await db.query(sql, params);
|
||||
res.json({
|
||||
success: true,
|
||||
data: result.rows,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
total,
|
||||
totalPages: Math.ceil(total / parseInt(pageSize))
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('查询财务记录失败:', error);
|
||||
res.status(500).json({ success: false, message: '查询财务记录失败' });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/summary', authenticate, async (req, res) => {
|
||||
try {
|
||||
const { project_id, date_from, date_to } = req.query;
|
||||
let where = "WHERE status != 'voided'";
|
||||
const params = [];
|
||||
let paramIdx = 1;
|
||||
|
||||
if (project_id) { params.push(project_id); where += ` AND project_id = $${paramIdx++}`; }
|
||||
if (date_from) { params.push(date_from); where += ` AND record_date >= $${paramIdx++}`; }
|
||||
if (date_to) { params.push(date_to); where += ` AND record_date <= $${paramIdx++}`; }
|
||||
|
||||
const totalResult = await db.query(
|
||||
`SELECT
|
||||
COALESCE(SUM(CASE WHEN txn_type = 'income' THEN amount_cny ELSE 0 END), 0) as total_income,
|
||||
COALESCE(SUM(CASE WHEN txn_type = 'expense' THEN amount_cny ELSE 0 END), 0) as total_expense,
|
||||
COALESCE(SUM(CASE WHEN txn_type = 'income' THEN amount_cny ELSE -amount_cny END), 0) as net_profit
|
||||
FROM financial_records ${where}`,
|
||||
params
|
||||
);
|
||||
|
||||
const byCategory = await db.query(
|
||||
`SELECT category_level1, category_level2,
|
||||
COALESCE(SUM(amount_cny), 0) as total_amount,
|
||||
COUNT(*) as count
|
||||
FROM financial_records ${where}
|
||||
GROUP BY category_level1, category_level2
|
||||
ORDER BY category_level1, total_amount DESC`,
|
||||
params
|
||||
);
|
||||
|
||||
const byMonth = await db.query(
|
||||
`SELECT TO_CHAR(record_date, 'YYYY-MM') as month,
|
||||
COALESCE(SUM(CASE WHEN txn_type = 'income' THEN amount_cny ELSE 0 END), 0) as income,
|
||||
COALESCE(SUM(CASE WHEN txn_type = 'expense' THEN amount_cny ELSE 0 END), 0) as expense
|
||||
FROM financial_records ${where}
|
||||
GROUP BY TO_CHAR(record_date, 'YYYY-MM')
|
||||
ORDER BY month`,
|
||||
params
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
totals: totalResult.rows[0],
|
||||
byCategory: byCategory.rows,
|
||||
byMonth: byMonth.rows
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取财务汇总失败:', error);
|
||||
res.status(500).json({ success: false, message: '获取财务汇总失败' });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/project-profit/:projectId', authenticate, async (req, res) => {
|
||||
try {
|
||||
const { projectId } = req.params;
|
||||
const projectResult = await db.query('SELECT * FROM projects WHERE id = $1', [projectId]);
|
||||
if (projectResult.rows.length === 0) {
|
||||
return res.status(404).json({ success: false, message: '项目不存在' });
|
||||
}
|
||||
const project = projectResult.rows[0];
|
||||
|
||||
const incomeResult = await db.query(
|
||||
`SELECT COALESCE(SUM(amount_cny), 0) as total
|
||||
FROM financial_records WHERE project_id = $1 AND txn_type = 'income' AND status != 'voided'`,
|
||||
[projectId]
|
||||
);
|
||||
const expenseResult = await db.query(
|
||||
`SELECT COALESCE(SUM(amount_cny), 0) as total
|
||||
FROM financial_records WHERE project_id = $1 AND txn_type = 'expense' AND status != 'voided'`,
|
||||
[projectId]
|
||||
);
|
||||
|
||||
const expenseByCategory = await db.query(
|
||||
`SELECT category_level2, COALESCE(SUM(amount_cny), 0) as total_amount, COUNT(*) as count
|
||||
FROM financial_records
|
||||
WHERE project_id = $1 AND txn_type = 'expense' AND status != 'voided'
|
||||
GROUP BY category_level2 ORDER BY total_amount DESC`,
|
||||
[projectId]
|
||||
);
|
||||
|
||||
const expenseByUser = await db.query(
|
||||
`SELECT user_name, COALESCE(SUM(amount_cny), 0) as total_amount, COUNT(*) as count
|
||||
FROM financial_records
|
||||
WHERE project_id = $1 AND txn_type = 'expense' AND status != 'voided' AND user_id IS NOT NULL
|
||||
GROUP BY user_name ORDER BY total_amount DESC`,
|
||||
[projectId]
|
||||
);
|
||||
|
||||
const totalIncome = parseFloat(incomeResult.rows[0].total);
|
||||
const totalExpense = parseFloat(expenseResult.rows[0].total);
|
||||
const contractAmount = parseFloat(project.contract_amount || 0);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
project_name: project.name,
|
||||
contract_amount: contractAmount,
|
||||
total_income: totalIncome,
|
||||
total_expense: totalExpense,
|
||||
gross_profit: totalIncome - totalExpense,
|
||||
gross_margin: totalIncome > 0 ? ((totalIncome - totalExpense) / totalIncome * 100).toFixed(2) : 0,
|
||||
expense_by_category: expenseByCategory.rows,
|
||||
expense_by_user: expenseByUser.rows
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取项目利润失败:', error);
|
||||
res.status(500).json({ success: false, message: '获取项目利润失败' });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/', authenticate, async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
txn_type, category_level1, category_level2,
|
||||
project_id, user_id, user_name,
|
||||
amount_original, currency, exchange_rate, amount_cny,
|
||||
record_date,
|
||||
counterparty_name, counterparty_type, counterparty_id,
|
||||
source, source_id, source_code,
|
||||
description, voucher_no, attachments
|
||||
} = req.body;
|
||||
|
||||
if (!txn_type || !category_level1 || !category_level2 || !amount_original || !record_date) {
|
||||
return res.status(400).json({ success: false, message: '缺少必填字段' });
|
||||
}
|
||||
|
||||
const record_code = generateCode('FIN');
|
||||
const calculated_amount_cny = amount_cny || (parseFloat(amount_original) * parseFloat(exchange_rate || 1));
|
||||
|
||||
const result = await db.query(
|
||||
`INSERT INTO financial_records
|
||||
(record_code, txn_type, category_level1, category_level2,
|
||||
project_id, user_id, user_name,
|
||||
amount_original, currency, exchange_rate, amount_cny,
|
||||
record_date, counterparty_name, counterparty_type, counterparty_id,
|
||||
source, source_id, source_code,
|
||||
description, voucher_no, attachments)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21)
|
||||
RETURNING *`,
|
||||
[record_code, txn_type, category_level1, category_level2,
|
||||
project_id || null, user_id || null, user_name || null,
|
||||
amount_original, currency || 'CNY', exchange_rate || 1, calculated_amount_cny,
|
||||
record_date,
|
||||
counterparty_name || null, counterparty_type || null, counterparty_id || null,
|
||||
source || 'manual', source_id || null, source_code || null,
|
||||
description || null, voucher_no || null, attachments || null]
|
||||
);
|
||||
|
||||
res.json({ success: true, data: result.rows[0] });
|
||||
} catch (error) {
|
||||
console.error('创建财务记录失败:', error);
|
||||
res.status(500).json({ success: false, message: '创建财务记录失败' });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/batch', authenticate, async (req, res) => {
|
||||
try {
|
||||
if (req.user.role !== 'admin') {
|
||||
return res.status(403).json({ success: false, message: '无权限操作' });
|
||||
}
|
||||
|
||||
const { records } = req.body;
|
||||
if (!Array.isArray(records) || records.length === 0) {
|
||||
return res.status(400).json({ success: false, message: '没有可导入的记录' });
|
||||
}
|
||||
|
||||
const results = [];
|
||||
const errors = [];
|
||||
|
||||
for (let i = 0; i < records.length; i++) {
|
||||
const r = records[i];
|
||||
try {
|
||||
if (!r.txn_type || !r.category_level1 || !r.category_level2 || !r.amount_original || !r.record_date) {
|
||||
errors.push({ row: i + 1, message: '缺少必填字段' });
|
||||
continue;
|
||||
}
|
||||
|
||||
const record_code = generateCode('FIN');
|
||||
const calculated_amount_cny = r.amount_cny || (parseFloat(r.amount_original) * parseFloat(r.exchange_rate || 1));
|
||||
|
||||
let projectId = r.project_id || null;
|
||||
if (!projectId && r.project_name) {
|
||||
const projResult = await db.query("SELECT id FROM projects WHERE name = $1", [r.project_name]);
|
||||
if (projResult.rows.length > 0) {
|
||||
projectId = projResult.rows[0].id;
|
||||
}
|
||||
}
|
||||
|
||||
let userId = r.user_id || null;
|
||||
if (!userId && r.user_name) {
|
||||
const userResult = await db.query("SELECT id FROM users WHERE name = $1", [r.user_name]);
|
||||
if (userResult.rows.length > 0) {
|
||||
userId = userResult.rows[0].id;
|
||||
}
|
||||
}
|
||||
|
||||
const result = await db.query(
|
||||
`INSERT INTO financial_records
|
||||
(record_code, txn_type, category_level1, category_level2,
|
||||
project_id, user_id, user_name,
|
||||
amount_original, currency, exchange_rate, amount_cny,
|
||||
record_date, counterparty_name, counterparty_type, counterparty_id,
|
||||
source, source_id, source_code,
|
||||
description, voucher_no)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20)
|
||||
RETURNING *`,
|
||||
[record_code, r.txn_type, r.category_level1, r.category_level2,
|
||||
projectId, userId, r.user_name || null,
|
||||
r.amount_original, r.currency || 'CNY', r.exchange_rate || 1, calculated_amount_cny,
|
||||
r.record_date,
|
||||
r.counterparty_name || null, r.counterparty_type || null, r.counterparty_id || null,
|
||||
r.source || 'manual', r.source_id || null, r.source_code || null,
|
||||
r.description || null, r.voucher_no || null]
|
||||
);
|
||||
results.push(result.rows[0]);
|
||||
} catch (err) {
|
||||
errors.push({ row: i + 1, message: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
imported: results.length,
|
||||
failed: errors.length,
|
||||
errors
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('批量导入失败:', error);
|
||||
res.status(500).json({ success: false, message: '批量导入失败' });
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/:id', authenticate, async (req, res) => {
|
||||
try {
|
||||
if (req.user.role !== 'admin') {
|
||||
return res.status(403).json({ success: false, message: '无权限操作' });
|
||||
}
|
||||
const { id } = req.params;
|
||||
const fields = req.body;
|
||||
const sets = [];
|
||||
const params = [];
|
||||
let idx = 1;
|
||||
|
||||
const allowedFields = ['category_level1', 'category_level2', 'description', 'counterparty_name', 'counterparty_type', 'voucher_no', 'status'];
|
||||
for (const key of allowedFields) {
|
||||
if (fields[key] !== undefined) {
|
||||
params.push(fields[key]);
|
||||
sets.push(`${key} = $${idx++}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (sets.length === 0) {
|
||||
return res.status(400).json({ success: false, message: '没有可更新的字段' });
|
||||
}
|
||||
|
||||
params.push(id);
|
||||
sets.push(`updated_at = CURRENT_TIMESTAMP`);
|
||||
const result = await db.query(
|
||||
`UPDATE financial_records SET ${sets.join(', ')} WHERE id = $${idx} RETURNING *`,
|
||||
params
|
||||
);
|
||||
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: '更新财务记录失败' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -331,7 +331,97 @@ router.post('/execute', async (req, res) => {
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, CURRENT_TIMESTAMP)
|
||||
RETURNING id`,
|
||||
[recordCode, payment_type, source_id, amount, 'CNY', paymentDate, voucher_url, payee_account, remark]);
|
||||
|
||||
|
||||
let finProjectId = null;
|
||||
let finUserId = null;
|
||||
let finUserName = null;
|
||||
let finCategory1 = 'company';
|
||||
let finCategory2 = 'other_company';
|
||||
let finSourceCode = recordCode;
|
||||
let finCounterpartyName = null;
|
||||
let finDescription = remark || '';
|
||||
|
||||
switch (payment_type) {
|
||||
case 'material': {
|
||||
const planRow = await db.query('SELECT purchase_order_id FROM payment_plans WHERE id = $1', [source_id]);
|
||||
if (planRow.rows.length > 0) {
|
||||
const poRow = await db.query('SELECT project_id, code FROM purchase_orders WHERE id = $1', [planRow.rows[0].purchase_order_id]);
|
||||
if (poRow.rows.length > 0) {
|
||||
finProjectId = poRow.rows[0].project_id;
|
||||
finSourceCode = poRow.rows[0].code;
|
||||
}
|
||||
}
|
||||
finCategory1 = 'project';
|
||||
finCategory2 = 'material';
|
||||
finDescription = finDescription || '材料采购付款';
|
||||
break;
|
||||
}
|
||||
case 'primary_freight':
|
||||
case 'secondary_freight': {
|
||||
const lrRow = await db.query('SELECT purchase_order_id FROM logistics_records WHERE id = $1', [source_id]);
|
||||
if (lrRow.rows.length > 0) {
|
||||
const poRow = await db.query('SELECT project_id, code FROM purchase_orders WHERE id = $1', [lrRow.rows[0].purchase_order_id]);
|
||||
if (poRow.rows.length > 0) {
|
||||
finProjectId = poRow.rows[0].project_id;
|
||||
finSourceCode = poRow.rows[0].code;
|
||||
}
|
||||
}
|
||||
finCategory1 = 'project';
|
||||
finCategory2 = 'freight';
|
||||
finDescription = finDescription || (payment_type === 'primary_freight' ? '一次运费付款' : '二次运费付款');
|
||||
break;
|
||||
}
|
||||
case 'advance': {
|
||||
const advRow = await db.query('SELECT project_id, applicant_id, applicant, advance_code, purpose FROM advances WHERE id = $1', [source_id]);
|
||||
if (advRow.rows.length > 0) {
|
||||
finProjectId = advRow.rows[0].project_id;
|
||||
finUserId = advRow.rows[0].applicant_id;
|
||||
finUserName = advRow.rows[0].applicant;
|
||||
finSourceCode = advRow.rows[0].advance_code;
|
||||
finDescription = finDescription || advRow.rows[0].purpose || '预支款';
|
||||
}
|
||||
finCategory1 = finProjectId ? 'project' : 'company';
|
||||
finCategory2 = finProjectId ? 'other_project' : 'other_company';
|
||||
break;
|
||||
}
|
||||
case 'reimbursement': {
|
||||
const reimbRow = await db.query('SELECT project_id, applicant_id, applicant, reimbursement_code, expense_type, description as reimb_desc FROM reimbursements WHERE id = $1', [source_id]);
|
||||
if (reimbRow.rows.length > 0) {
|
||||
finProjectId = reimbRow.rows[0].project_id;
|
||||
finUserId = reimbRow.rows[0].applicant_id;
|
||||
finUserName = reimbRow.rows[0].applicant;
|
||||
finSourceCode = reimbRow.rows[0].reimbursement_code;
|
||||
finDescription = finDescription || reimbRow.rows[0].reimb_desc || '报销';
|
||||
if (reimbRow.rows[0].expense_type === 'project') {
|
||||
finCategory1 = 'project';
|
||||
finCategory2 = 'other_project';
|
||||
} else {
|
||||
finCategory1 = 'company';
|
||||
finCategory2 = 'other_company';
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const finCode = 'FIN-' + Date.now();
|
||||
const finAmountCny = parseFloat(amount) || 0;
|
||||
await db.query(`
|
||||
INSERT INTO financial_records
|
||||
(record_code, txn_type, category_level1, category_level2,
|
||||
project_id, user_id, user_name,
|
||||
amount_original, currency, exchange_rate, amount_cny,
|
||||
record_date, counterparty_name,
|
||||
source, source_id, source_code,
|
||||
description, voucher_no)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18)`,
|
||||
[finCode, 'expense', finCategory1, finCategory2,
|
||||
finProjectId, finUserId, finUserName,
|
||||
amount, 'CNY', 1, finAmountCny,
|
||||
paymentDate, finCounterpartyName,
|
||||
payment_type, source_id, finSourceCode,
|
||||
finDescription, voucher_url]);
|
||||
|
||||
await db.query('COMMIT');
|
||||
|
||||
res.json({
|
||||
|
||||
Reference in New Issue
Block a user