feat: 完善采购申请流程 - 添加审批、执行、列表筛选排序功能
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
-- 创建供应商收款信息表
|
||||
CREATE TABLE IF NOT EXISTS supplier_payment_infos (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
supplier_id INTEGER NOT NULL,
|
||||
account_name TEXT NOT NULL,
|
||||
bank_account TEXT NOT NULL,
|
||||
bank_name TEXT NOT NULL,
|
||||
qr_code TEXT,
|
||||
is_primary INTEGER DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (supplier_id) REFERENCES suppliers(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX IF NOT EXISTS idx_supplier_payment_infos_supplier_id ON supplier_payment_infos(supplier_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_supplier_payment_infos_is_primary ON supplier_payment_infos(is_primary);
|
||||
@@ -22,7 +22,7 @@ const validate = (req, res, next) => {
|
||||
dotenv.config();
|
||||
|
||||
const app = express();
|
||||
const PORT = 3005;
|
||||
const PORT = process.env.PORT || 3001;
|
||||
|
||||
// 中间件
|
||||
app.use(cors());
|
||||
@@ -32,6 +32,97 @@ app.use(express.urlencoded({ extended: true }));
|
||||
// 静态文件服务 - 前端应用
|
||||
app.use(express.static(path.join(__dirname, '../frontend/dist')));
|
||||
|
||||
// 创建供应商收款信息表
|
||||
async function createSupplierPaymentInfosTable() {
|
||||
try {
|
||||
await db.query(`
|
||||
CREATE TABLE IF NOT EXISTS supplier_payment_infos (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
supplier_id INTEGER NOT NULL,
|
||||
account_name TEXT NOT NULL,
|
||||
bank_account TEXT NOT NULL,
|
||||
bank_name TEXT NOT NULL,
|
||||
qr_code TEXT,
|
||||
is_primary INTEGER DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (supplier_id) REFERENCES suppliers(id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
console.log('供应商收款信息表创建成功');
|
||||
} catch (error) {
|
||||
console.error('创建供应商收款信息表失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加purchase_type字段到purchase_requests表
|
||||
async function addPurchaseTypeColumn() {
|
||||
try {
|
||||
// 检查字段是否存在
|
||||
const result = await db.query(`PRAGMA table_info(purchase_requests)`);
|
||||
const hasPurchaseType = result.rows.some(row => row.name === 'purchase_type');
|
||||
|
||||
if (!hasPurchaseType) {
|
||||
await db.query(`ALTER TABLE purchase_requests ADD COLUMN purchase_type TEXT DEFAULT 'inventory'`);
|
||||
console.log('purchase_type字段添加成功');
|
||||
} else {
|
||||
console.log('purchase_type字段已存在');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('添加purchase_type字段失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加brief_description字段到purchase_requests表
|
||||
async function addBriefDescriptionColumn() {
|
||||
try {
|
||||
// 检查字段是否存在
|
||||
const result = await db.query(`PRAGMA table_info(purchase_requests)`);
|
||||
const hasBriefDescription = result.rows.some(row => row.name === 'brief_description');
|
||||
|
||||
if (!hasBriefDescription) {
|
||||
await db.query(`ALTER TABLE purchase_requests ADD COLUMN brief_description TEXT`);
|
||||
console.log('brief_description字段添加成功');
|
||||
} else {
|
||||
console.log('brief_description字段已存在');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('添加brief_description字段失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加execute_date和execute_method字段到purchase_requests表
|
||||
async function addExecuteColumns() {
|
||||
try {
|
||||
// 检查字段是否存在
|
||||
const result = await db.query(`PRAGMA table_info(purchase_requests)`);
|
||||
const hasExecuteDate = result.rows.some(row => row.name === 'execute_date');
|
||||
const hasExecuteMethod = result.rows.some(row => row.name === 'execute_method');
|
||||
|
||||
if (!hasExecuteDate) {
|
||||
await db.query(`ALTER TABLE purchase_requests ADD COLUMN execute_date TEXT`);
|
||||
console.log('execute_date字段添加成功');
|
||||
} else {
|
||||
console.log('execute_date字段已存在');
|
||||
}
|
||||
|
||||
if (!hasExecuteMethod) {
|
||||
await db.query(`ALTER TABLE purchase_requests ADD COLUMN execute_method TEXT`);
|
||||
console.log('execute_method字段添加成功');
|
||||
} else {
|
||||
console.log('execute_method字段已存在');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('添加执行字段失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化数据库表
|
||||
createSupplierPaymentInfosTable();
|
||||
addPurchaseTypeColumn();
|
||||
addBriefDescriptionColumn();
|
||||
addExecuteColumns();
|
||||
|
||||
// ==================== 健康检查 ====================
|
||||
app.get('/api/health', (req, res) => {
|
||||
res.json({
|
||||
@@ -435,10 +526,34 @@ app.get('/api/suppliers', async (req, res) => {
|
||||
LIMIT 50
|
||||
`);
|
||||
|
||||
// 为每个供应商获取收款信息
|
||||
const suppliersWithPaymentInfos = await Promise.all(
|
||||
result.rows.map(async (supplier) => {
|
||||
const paymentInfosResult = await db.query(
|
||||
`SELECT * FROM supplier_payment_infos WHERE supplier_id = ? ORDER BY is_primary DESC`,
|
||||
[supplier.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 {
|
||||
...supplier,
|
||||
payment_infos: paymentInfos
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result.rows,
|
||||
count: result.rows.length
|
||||
data: suppliersWithPaymentInfos,
|
||||
count: suppliersWithPaymentInfos.length
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取供应商失败:', error);
|
||||
@@ -478,6 +593,23 @@ app.get('/api/suppliers/:id', async (req, res) => {
|
||||
is_primary: contact.is_primary === 1
|
||||
}));
|
||||
|
||||
// 获取供应商的所有收款信息
|
||||
const paymentInfosResult = await db.query(`
|
||||
SELECT * FROM supplier_payment_infos
|
||||
WHERE supplier_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 formattedSupplier = {
|
||||
id: supplier.id,
|
||||
@@ -486,6 +618,7 @@ app.get('/api/suppliers/:id', async (req, res) => {
|
||||
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: 0, // 默认为0
|
||||
total_paid: 0, // 默认为0
|
||||
@@ -517,7 +650,7 @@ app.get('/api/suppliers/:id', async (req, res) => {
|
||||
|
||||
app.post('/api/suppliers', async (req, res) => {
|
||||
try {
|
||||
const { name, supply_category, country, remark, contacts } = req.body;
|
||||
const { name, supply_category, country, remark, contacts, payment_infos } = req.body;
|
||||
|
||||
// 从contacts中获取主联系人信息
|
||||
const primaryContact = contacts?.find(c => c.is_primary) || contacts?.[0];
|
||||
@@ -546,6 +679,17 @@ app.post('/api/suppliers', async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 插入收款信息数据
|
||||
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 (?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))`,
|
||||
[supplierId, paymentInfo.account_name, paymentInfo.bank_account, paymentInfo.bank_name, paymentInfo.qr_code, paymentInfo.is_primary ? 1 : 0]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '供应商创建成功',
|
||||
@@ -556,6 +700,7 @@ app.post('/api/suppliers', async (req, res) => {
|
||||
supply_category,
|
||||
country,
|
||||
contacts: contacts || [],
|
||||
payment_infos: payment_infos || [],
|
||||
remark,
|
||||
total_purchase_amount: 0,
|
||||
total_paid: 0,
|
||||
@@ -576,7 +721,7 @@ app.post('/api/suppliers', async (req, res) => {
|
||||
app.put('/api/suppliers/:id', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { name, supply_category, country, remark, contacts } = req.body;
|
||||
const { name, supply_category, country, remark, contacts, payment_infos } = req.body;
|
||||
|
||||
// 从contacts中获取主联系人信息
|
||||
const primaryContact = contacts?.find(c => c.is_primary) || contacts?.[0];
|
||||
@@ -607,6 +752,20 @@ app.put('/api/suppliers/:id', async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 删除旧的收款信息数据
|
||||
await db.query(`DELETE FROM supplier_payment_infos WHERE supplier_id = ?`, [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 (?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))`,
|
||||
[id, paymentInfo.account_name, paymentInfo.bank_account, paymentInfo.bank_name, paymentInfo.qr_code, paymentInfo.is_primary ? 1 : 0]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '供应商更新成功',
|
||||
@@ -617,6 +776,7 @@ app.put('/api/suppliers/:id', async (req, res) => {
|
||||
supply_category,
|
||||
country,
|
||||
contacts: contacts || [],
|
||||
payment_infos: payment_infos || [],
|
||||
remark,
|
||||
total_purchase_amount: 0,
|
||||
total_paid: 0,
|
||||
@@ -3507,12 +3667,14 @@ app.get('/api/executions/pending', async (req, res) => {
|
||||
const reimbursements = await db.query('SELECT * FROM reimbursements WHERE status = ?', ['approved']);
|
||||
const payments = await db.query('SELECT * FROM payment_requests WHERE status = ?', ['approved']);
|
||||
const verifications = await db.query('SELECT * FROM verifications WHERE status = ?', ['approved']);
|
||||
const purchaseRequests = await db.query('SELECT * FROM purchase_requests WHERE status = ?', ['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 }))
|
||||
...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 });
|
||||
@@ -3529,12 +3691,23 @@ app.get('/api/executions/executed', async (req, res) => {
|
||||
const reimbursements = await db.query('SELECT * FROM reimbursements WHERE status = ?', ['executed']);
|
||||
const payments = await db.query('SELECT * FROM payment_requests WHERE status = ?', ['executed']);
|
||||
const verifications = await db.query('SELECT * FROM verifications WHERE status = ?', ['executed']);
|
||||
const purchaseRequests = await db.query('SELECT * FROM purchase_requests WHERE status = ?', ['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 }))
|
||||
...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 });
|
||||
@@ -3610,6 +3783,9 @@ app.post('/api/executions', async (req, res) => {
|
||||
throw error;
|
||||
}
|
||||
break;
|
||||
case 'purchase':
|
||||
await db.query('UPDATE purchase_requests SET status = ?, execute_date = ?, execute_method = ? WHERE id = ?', [status, executeDate, execute_method, apply_id]);
|
||||
break;
|
||||
}
|
||||
|
||||
res.json({ success: true, message: '执行操作成功' });
|
||||
@@ -3910,6 +4086,24 @@ app.get('/api/purchase-requests/:id', async (req, res) => {
|
||||
|
||||
purchaseRequest.items = itemsResult.rows;
|
||||
|
||||
// 获取供应商的付款信息
|
||||
if (purchaseRequest.supplier_id) {
|
||||
const paymentInfosResult = await db.query(`
|
||||
SELECT * FROM supplier_payment_infos
|
||||
WHERE supplier_id = ?
|
||||
ORDER BY is_primary DESC
|
||||
`, [purchaseRequest.supplier_id]);
|
||||
|
||||
purchaseRequest.supplier_payment_infos = 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
|
||||
}));
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: purchaseRequest
|
||||
@@ -3928,7 +4122,7 @@ app.post('/api/purchase-requests', async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
project_id, applicant, request_date, supplier_id, supplier_name,
|
||||
expense_category, total_amount, currency, remark, attachments, items
|
||||
expense_category, total_amount, currency, remark, attachments, items, purchase_type, brief_description
|
||||
} = req.body;
|
||||
|
||||
const date = new Date();
|
||||
@@ -3936,9 +4130,9 @@ app.post('/api/purchase-requests', async (req, res) => {
|
||||
|
||||
const result = await db.query(`
|
||||
INSERT INTO purchase_requests
|
||||
(request_code, project_id, applicant, request_date, supplier_id, supplier_name, expense_category, total_amount, currency, remark, attachments, status, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', datetime('now'), datetime('now'))
|
||||
`, [requestCode, project_id, applicant, request_date, supplier_id, supplier_name, expense_category, total_amount || 0, currency || 'CNY', remark, attachments ? JSON.stringify(attachments) : null]);
|
||||
(request_code, project_id, applicant, request_date, supplier_id, supplier_name, expense_category, total_amount, currency, remark, attachments, status, purchase_type, brief_description, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending_edit', ?, ?, datetime('now'), datetime('now'))
|
||||
`, [requestCode, project_id, applicant, request_date, supplier_id, supplier_name, expense_category, total_amount || 0, currency || 'CNY', remark, attachments ? JSON.stringify(attachments) : null, purchase_type || 'inventory', brief_description]);
|
||||
|
||||
const purchaseRequestId = result.lastID;
|
||||
|
||||
@@ -3972,15 +4166,16 @@ app.put('/api/purchase-requests/:id', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const {
|
||||
project_id, applicant, request_date, supplier_id, supplier_name,
|
||||
expense_category, total_amount, currency, remark, attachments, items
|
||||
expense_category, total_amount, currency, remark, attachments, items, purchase_type, brief_description
|
||||
} = req.body;
|
||||
|
||||
const result = await db.query(`
|
||||
UPDATE purchase_requests
|
||||
SET project_id = ?, applicant = ?, request_date = ?, supplier_id = ?, supplier_name = ?,
|
||||
expense_category = ?, total_amount = ?, currency = ?, remark = ?, attachments = ?, updated_at = datetime('now')
|
||||
expense_category = ?, total_amount = ?, currency = ?, remark = ?, attachments = ?,
|
||||
purchase_type = ?, brief_description = ?, updated_at = datetime('now')
|
||||
WHERE id = ?
|
||||
`, [project_id, applicant, request_date, supplier_id, supplier_name, expense_category, total_amount, currency, remark, attachments ? JSON.stringify(attachments) : null, id]);
|
||||
`, [project_id, applicant, request_date, supplier_id, supplier_name, expense_category, total_amount, currency, remark, attachments ? JSON.stringify(attachments) : null, purchase_type || 'inventory', brief_description, id]);
|
||||
|
||||
if (result.changes === 0) {
|
||||
return res.status(404).json({ success: false, message: '采购申请不存在' });
|
||||
@@ -4040,7 +4235,7 @@ app.post('/api/purchase-requests/:id/submit', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const result = await db.query('UPDATE purchase_requests SET status = ?, updated_at = datetime(\'now\') WHERE id = ?', ['submitted', id]);
|
||||
const result = await db.query('UPDATE purchase_requests SET status = ?, updated_at = datetime(\'now\') WHERE id = ?', ['pending', id]);
|
||||
|
||||
if (result.changes === 0) {
|
||||
return res.status(404).json({ success: false, message: '采购申请不存在' });
|
||||
@@ -4074,7 +4269,7 @@ app.post('/api/purchase-requests/:id/reject', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const result = await db.query('UPDATE purchase_requests SET status = ?, updated_at = datetime(\'now\') WHERE id = ?', ['rejected', id]);
|
||||
const result = await db.query('UPDATE purchase_requests SET status = ?, updated_at = datetime(\'now\') WHERE id = ?', ['pending_edit', id]);
|
||||
|
||||
if (result.changes === 0) {
|
||||
return res.status(404).json({ success: false, message: '采购申请不存在' });
|
||||
@@ -4111,6 +4306,23 @@ app.post('/api/purchase-requests/:id/execute', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/purchase-requests/:id/withdraw', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const result = await db.query('UPDATE purchase_requests SET status = ?, updated_at = datetime(\'now\') WHERE id = ?', ['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 });
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== 库存管理API ====================
|
||||
app.get('/api/inventory', async (req, res) => {
|
||||
try {
|
||||
|
||||
@@ -49,7 +49,7 @@ async function testPurchaseFlow() {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}, {
|
||||
project_id: 1,
|
||||
project_id: 3,
|
||||
applicant: '测试申请人',
|
||||
request_date: '2026-03-25',
|
||||
expense_category: 'material',
|
||||
@@ -171,7 +171,7 @@ async function testInventoryFlow() {
|
||||
}
|
||||
|
||||
// 3. 创建出库记录
|
||||
console.log('\n3. 创建出库记录...');
|
||||
console.log('3. 创建出库记录...');
|
||||
const outResponse = await request({
|
||||
hostname: 'localhost',
|
||||
port: 3005,
|
||||
@@ -181,7 +181,7 @@ async function testInventoryFlow() {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}, {
|
||||
project_id: 1,
|
||||
project_id: 3,
|
||||
product_id: 1,
|
||||
quantity: 10,
|
||||
operator: '测试操作员'
|
||||
@@ -204,7 +204,7 @@ async function testCostStatistics() {
|
||||
const costResponse = await request({
|
||||
hostname: 'localhost',
|
||||
port: 3005,
|
||||
path: '/api/projects/1/cost-summary',
|
||||
path: '/api/projects/3/cost-summary',
|
||||
method: 'GET'
|
||||
});
|
||||
console.log('获取项目成本统计响应:', costResponse.status, costResponse.body);
|
||||
|
||||
Reference in New Issue
Block a user