2026-04-19 19:15:01 +08:00
|
|
|
/**
|
|
|
|
|
* 退库管理路由
|
|
|
|
|
* 遵循设计方案:采购-付款-物流-退库一体化流程设计方案.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: '获取退库单列表失败',
|
2026-05-15 12:02:24 +08:00
|
|
|
error: process.env.NODE_ENV === 'development' ? error.message : '操作失败'
|
2026-04-19 19:15:01 +08:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取退库单详情
|
|
|
|
|
*/
|
|
|
|
|
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: '获取退库单详情失败',
|
2026-05-15 12:02:24 +08:00
|
|
|
error: process.env.NODE_ENV === 'development' ? error.message : '操作失败'
|
2026-04-19 19:15:01 +08:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 创建退库单
|
|
|
|
|
*/
|
|
|
|
|
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)
|
2026-05-15 12:02:24 +08:00
|
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, 'pending', $11, $12, CURRENT_TIMESTAMP)
|
|
|
|
|
RETURNING id`,
|
|
|
|
|
[code, project_id, return_type || 'warehouse', return_date, applicant, itemsJson,
|
2026-04-19 19:15:01 +08:00
|
|
|
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: '创建退库单失败',
|
2026-05-15 12:02:24 +08:00
|
|
|
error: process.env.NODE_ENV === 'development' ? error.message : '操作失败'
|
2026-04-19 19:15:01 +08:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 确认退库
|
|
|
|
|
* 遵循设计方案:退库后自动减少项目材料库存
|
|
|
|
|
*/
|
|
|
|
|
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: '确认退库失败',
|
2026-05-15 12:02:24 +08:00
|
|
|
error: process.env.NODE_ENV === 'development' ? error.message : '操作失败'
|
2026-04-19 19:15:01 +08:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 驳回退库
|
|
|
|
|
*/
|
|
|
|
|
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);
|
2026-05-15 12:02:24 +08:00
|
|
|
res.status(500).json({ success: false, message: '驳回退库失败' });
|
2026-04-19 19:15:01 +08:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 更新退库单
|
|
|
|
|
*/
|
|
|
|
|
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: '更新退库单失败',
|
2026-05-15 12:02:24 +08:00
|
|
|
error: process.env.NODE_ENV === 'development' ? error.message : '操作失败'
|
2026-04-19 19:15:01 +08:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 删除退库单
|
|
|
|
|
*/
|
|
|
|
|
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: '删除退库单失败',
|
2026-05-15 12:02:24 +08:00
|
|
|
error: process.env.NODE_ENV === 'development' ? error.message : '操作失败'
|
2026-04-19 19:15:01 +08:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取项目可退库的材料列表
|
|
|
|
|
*/
|
|
|
|
|
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: '获取项目材料列表失败',
|
2026-05-15 12:02:24 +08:00
|
|
|
error: process.env.NODE_ENV === 'development' ? error.message : '操作失败'
|
2026-04-19 19:15:01 +08:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
module.exports = router;
|