feat(purchase): 구매 취소/반품 + 입고일 기준 월별 분석
- 상태 추가: cancelled(구매취소), returned(반품) - API: PUT /:id/cancel, /:id/return, /:id/revert-cancel - 데스크탑: 구매완료→취소 버튼, 입고완료→반품 버튼, 취소→되돌리기 - 분석 페이지: 구매일/입고일 기준 전환 토글, 입고일 기준 월간 분류 집계 + 입고 목록 - Settlement API: GET /received-summary, /received-list (입고일 기준) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -233,6 +233,43 @@ const PurchaseRequestModel = {
|
||||
[batchId]
|
||||
);
|
||||
return rows.map(r => r.requester_id);
|
||||
},
|
||||
|
||||
// 구매 취소 (purchased → pending 복원, batch에서도 제거)
|
||||
async cancelPurchase(requestId, { cancelledBy, cancelReason }) {
|
||||
const db = await getDb();
|
||||
await db.query(
|
||||
`UPDATE purchase_requests
|
||||
SET status = 'cancelled', cancelled_at = NOW(), cancelled_by = ?, cancel_reason = ?,
|
||||
batch_id = NULL
|
||||
WHERE request_id = ? AND status = 'purchased'`,
|
||||
[cancelledBy, cancelReason || null, requestId]
|
||||
);
|
||||
return this.getById(requestId);
|
||||
},
|
||||
|
||||
// 반품 (received → returned)
|
||||
async returnItem(requestId, { cancelledBy, cancelReason }) {
|
||||
const db = await getDb();
|
||||
await db.query(
|
||||
`UPDATE purchase_requests
|
||||
SET status = 'returned', cancelled_at = NOW(), cancelled_by = ?, cancel_reason = ?
|
||||
WHERE request_id = ? AND status = 'received'`,
|
||||
[cancelledBy, cancelReason || null, requestId]
|
||||
);
|
||||
return this.getById(requestId);
|
||||
},
|
||||
|
||||
// 취소/반품에서 원래 상태로 되돌리기
|
||||
async revertCancel(requestId) {
|
||||
const db = await getDb();
|
||||
await db.query(
|
||||
`UPDATE purchase_requests
|
||||
SET status = 'pending', cancelled_at = NULL, cancelled_by = NULL, cancel_reason = NULL
|
||||
WHERE request_id = ? AND status = 'cancelled'`,
|
||||
[requestId]
|
||||
);
|
||||
return this.getById(requestId);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -83,6 +83,47 @@ const SettlementModel = {
|
||||
return { year_month: yearMonth, vendor_id: vendorId, status: 'pending' };
|
||||
},
|
||||
|
||||
// 입고일 기준 월간 분류별 요약
|
||||
async getCategorySummaryByReceived(yearMonth) {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(`
|
||||
SELECT ci.category,
|
||||
COUNT(*) AS count,
|
||||
SUM(pr.quantity * COALESCE(p.unit_price, 0)) AS total_amount
|
||||
FROM purchase_requests pr
|
||||
LEFT JOIN consumable_items ci ON pr.item_id = ci.item_id
|
||||
LEFT JOIN purchases p ON p.request_id = pr.request_id
|
||||
WHERE pr.status = 'received'
|
||||
AND DATE_FORMAT(pr.received_at, '%Y-%m') = ?
|
||||
GROUP BY ci.category
|
||||
`, [yearMonth]);
|
||||
return rows;
|
||||
},
|
||||
|
||||
// 입고일 기준 월간 상세 목록
|
||||
async getMonthlyReceived(yearMonth) {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(`
|
||||
SELECT pr.request_id, pr.quantity, pr.received_at, pr.received_location,
|
||||
pr.received_photo_path, pr.status, pr.notes,
|
||||
ci.item_name, ci.spec, ci.maker, ci.category, ci.unit, ci.base_price,
|
||||
p.unit_price, p.purchase_date, p.vendor_id,
|
||||
v.vendor_name,
|
||||
su.name AS requester_name,
|
||||
rsu.name AS received_by_name
|
||||
FROM purchase_requests pr
|
||||
LEFT JOIN consumable_items ci ON pr.item_id = ci.item_id
|
||||
LEFT JOIN purchases p ON p.request_id = pr.request_id
|
||||
LEFT JOIN vendors v ON p.vendor_id = v.vendor_id
|
||||
LEFT JOIN sso_users su ON pr.requester_id = su.user_id
|
||||
LEFT JOIN sso_users rsu ON pr.received_by = rsu.user_id
|
||||
WHERE pr.status IN ('received', 'returned')
|
||||
AND DATE_FORMAT(pr.received_at, '%Y-%m') = ?
|
||||
ORDER BY pr.received_at DESC
|
||||
`, [yearMonth]);
|
||||
return rows;
|
||||
},
|
||||
|
||||
// 가격 변동 목록 (월간)
|
||||
async getPriceChanges(yearMonth) {
|
||||
const db = await getDb();
|
||||
|
||||
Reference in New Issue
Block a user