diff --git a/frontend/assets/styles.css b/frontend/assets/styles.css index 292efb4..fd4a011 100755 --- a/frontend/assets/styles.css +++ b/frontend/assets/styles.css @@ -2868,7 +2868,9 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover{ #app.trackit-layout > #sidebar.sidebar.collapsed .sidebar-search{display:none} #app.trackit-layout > #sidebar.sidebar.collapsed .sidebar-footer{border-top:0;padding-top:0} - #app.trackit-layout > #sidebar.sidebar.collapsed .sidebar-actions{flex-direction:column;gap:12px;justify-content:flex-start;padding:0 0 2px} + #app.trackit-layout > #sidebar.sidebar.collapsed .sidebar-actions{flex-direction:column;gap:12px;justify-content:flex-start;align-items:center;padding:0 0 2px} + #app.trackit-layout > #sidebar.sidebar.collapsed .sidebar-actions .notification-bell-btn{margin-left:0} + #app.trackit-layout > #sidebar.sidebar.collapsed .sidebar-actions .user-avatar-btn{width:100%;justify-content:center} #app.trackit-layout > #sidebar.sidebar.collapsed .sidebar-notification-card{display:none !important} #app.trackit-layout > #sidebar.sidebar.collapsed .brand-name{display:none} diff --git a/frontend/index.html b/frontend/index.html index 0b8bf2b..5074536 100755 --- a/frontend/index.html +++ b/frontend/index.html @@ -161,6 +161,22 @@ 委外在制库存 +
+ + + 仓库管理 + + +
+ 借出单 + 借出还入单 +
+
- -
+ +
-
💡 AI助手
-
智能建议
+
🏠 仓库管理
+
借出未还汇总
- - - + + +
- -
- ${(data.smartSuggestions || []).length > 0 ? - data.smartSuggestions.map(s => ` -
-
- ${s.icon} - ${s.title} -
-
${s.message}
-
- `).join('') : - '
暂无建议
一切正常运行中
' - } +
+
加载中...
diff --git a/frontend/js/components/notifications.js b/frontend/js/components/notifications.js index e8452d3..a0573d0 100755 --- a/frontend/js/components/notifications.js +++ b/frontend/js/components/notifications.js @@ -72,6 +72,7 @@ await loadNotifications(); } } else { + badge.textContent = '0'; badge.style.display = 'none'; } } catch (e) { @@ -261,14 +262,14 @@ // 初始加载未读数量 await updateUnreadCount(); - // 每5秒更新一次未读数量(实时检查新消息) + // 每2秒更新一次未读数量(实时检查新消息) if (notificationInterval) { clearInterval(notificationInterval); } - notificationInterval = setInterval(updateUnreadCount, 5000); + notificationInterval = setInterval(updateUnreadCount, 2000); isInitialized = true; - console.log('[Notifications] 初始化完成,实时轮询已启动(5秒间隔)'); + console.log('[Notifications] 初始化完成,实时轮询已启动(2秒间隔)'); } catch (e) { console.error('初始化通知系统失败:', e); } diff --git a/frontend/js/components/warehouse.js b/frontend/js/components/warehouse.js new file mode 100644 index 0000000..795afce --- /dev/null +++ b/frontend/js/components/warehouse.js @@ -0,0 +1,437 @@ +/** + * 仓库管理模块 —— 借出单 / 借出还入单 + */ +window.Warehouse = (() => { + let currentTab = 'borrow'; + let currentUser = null; + let _approvedBorrows = []; + + const statusLabel = (s) => { + const map = { pending: '待确认', approved: '已批准', rejected: '已拒绝', received: '已同意', not_received: '未收到', partial: '部分收到', returned_back: '已退回' }; + return map[s] || s; + }; + const statusColor = (s) => { + const map = { pending: '#f59e0b', approved: '#10b981', rejected: '#ef4444', received: '#10b981', not_received: '#ef4444', partial: '#f59e0b', returned_back: '#6b7280' }; + return map[s] || '#94a3b8'; + }; + + const isSuperadmin = () => currentUser && currentUser.role === 'superadmin'; + const isAdmin = () => currentUser && currentUser.role === 'admin'; + + // ── 主渲染 ────────────────────────────────── + const render = async () => { + + return ` +
+
+
+ ${tabBtn('borrow', '借出单', currentTab)} + ${tabBtn('return', '借出还入单', currentTab)} +
+
+
+
+ + `; + }; + + const tabBtn = (key, label, cur) => + ``; + + const renderUnreturnedCard = async () => { + try { + const res = await fetch('/api/warehouse/unreturned-summary', { credentials: 'include' }); + const data = res.ok ? await res.json() : { summary: [], total_unreturned: 0 }; + const summary = data.summary || []; + if (summary.length === 0) return ''; + const rows = summary.map(s => + ` + ${s.model} + 未还 ${s.unreturned} + (借 ${s.borrowed} / 还 ${s.returned}) + ` + ).join(''); + return `
+ ⚠️ 未还汇总 + ${rows} +
`; + } catch (e) { return ''; } + }; + + const init = async () => { + renderTab(); + }; + + const switchTab = (tab) => { + currentTab = tab; + document.querySelectorAll('.wh-tab-btn').forEach(b => { + b.classList.toggle('active', b.textContent.trim() === (tab === 'borrow' ? '借出单' : '借出还入单')); + }); + renderTab(); + }; + + // ── 借出单列表 ────────────────────────────── + const renderTab = async () => { + const el = document.getElementById('warehouse-content'); + if (!el) return; + if (currentTab === 'borrow') await renderBorrowTab(el); + else await renderReturnTab(el); + }; + + const renderBorrowTab = async (el) => { + el.innerHTML = '
加载中...
'; + const [res, retRes] = await Promise.all([ + fetch('/api/warehouse/borrow-orders', { credentials: 'include' }), + fetch('/api/warehouse/return-orders', { credentials: 'include' }) + ]); + const data = res.ok ? await res.json() : { list: [] }; + const retData = retRes.ok ? await retRes.json() : { list: [] }; + const list = data.list || []; + + // 按borrow_order_no聚合已同意的还入数量 + const receivedMap = {}; + (retData.list || []).forEach(r => { + if (r.status === 'received') { + receivedMap[r.borrow_order_no] = (receivedMap[r.borrow_order_no] || 0) + (r.received_qty != null ? r.received_qty : r.qty); + } + }); + + const pendingCount = list.filter(r => r.status === 'pending').length; + + el.innerHTML = ` +
+
+ 借出单列表 + ${pendingCount > 0 ? `${pendingCount} 待确认` : ''} +
+ ${isSuperadmin() ? `` : ''} +
+
+ + + + + ${isAdmin() ? '' : ''} + + + ${list.length === 0 ? `` : ''} + ${list.map(r => { + const received = receivedMap[r.order_no] || 0; + const unreturned = r.status === 'approved' ? Math.max(r.qty - received, 0) : null; + return ` + + + + + + + + + + + + ${isAdmin() ? `` : ''} + + `;}).join('')} + +
单号型号数量未还数借出理由申请人申请时间状态拒绝原因确认人操作
暂无数据
${r.order_no}${r.model}${r.qty}${unreturned == null ? '—' : unreturned}${r.reason}${r.created_by}${(r.created_at || '').replace('T',' ').slice(0, 16)}${statusLabel(r.status)}${r.reject_reason || '—'}${r.confirmed_by || '—'} + ${r.status === 'pending' && currentUser.role === 'admin' + ? ` + ` + : '—'} +
+
+ `; + }; + + // ── 还入单列表 ────────────────────────────── + const renderReturnTab = async (el) => { + el.innerHTML = '
加载中...
'; + const [retRes, borrowRes] = await Promise.all([ + fetch('/api/warehouse/return-orders', { credentials: 'include' }), + fetch('/api/warehouse/borrow-orders?status=approved', { credentials: 'include' }) + ]); + const retData = retRes.ok ? await retRes.json() : { list: [] }; + const borrowData = borrowRes.ok ? await borrowRes.json() : { list: [] }; + const list = retData.list || []; + _approvedBorrows = borrowData.list || []; + + const pendingCount = list.filter(r => r.status === 'pending').length; + + el.innerHTML = ` +
+
+ 借出还入单列表 + ${pendingCount > 0 ? `${pendingCount} 待确认` : ''} +
+ ${isSuperadmin() ? `` : ''} +
+
+ + + + + ${isAdmin() ? '' : ''} + + + ${list.length === 0 ? `` : ''} + ${list.map(r => ` + + + + + + + + + + + ${isAdmin() ? `` : ''} + + `).join('')} + +
还入单号对应借出单型号还入数申请人申请时间状态备注确认人操作
暂无数据
${r.order_no}${r.borrow_order_no}${r.model}${r.qty}${r.created_by}${(r.created_at || '').replace('T',' ').slice(0, 16)}${statusLabel(r.status)}${r.reject_reason || '—'}${r.confirmed_by || '—'} + ${r.status === 'pending' && currentUser.role === 'admin' + ? ` + ` + : '—'} +
+
+ `; + }; + + // ── 新建借出单弹窗 ────────────────────────── + const showCreateBorrowModal = () => { + const html = ` +
+
+
新建借出单
+
+
+
+
+ + +
+
+
`; + document.body.insertAdjacentHTML('beforeend', html); + }; + + const submitBorrow = async () => { + const model = document.getElementById('wh-b-model')?.value.trim(); + const qty = document.getElementById('wh-b-qty')?.value; + const reason = document.getElementById('wh-b-reason')?.value.trim(); + if (!model || !qty || !reason) { API.toast('请填写所有必填项'); return; } + const res = await fetch('/api/warehouse/borrow-orders', { + method: 'POST', credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ model, qty: parseInt(qty), reason }) + }); + const data = await res.json(); + if (data.ok) { + API.toast(`借出单 ${data.order_no} 已提交,等待仓管确认`); + closeModal(); + renderTab(); + } else { + API.toast(data.error || '提交失败'); + } + }; + + // ── 新建还入单弹窗 ────────────────────────── + const showCreateReturnModal = () => { + const options = (_approvedBorrows || []).map(b => + `` + ).join(''); + const html = ` +
+
+
新建借出还入单
+
+ + +
+
+
+
+ + +
+
+
`; + document.body.insertAdjacentHTML('beforeend', html); + }; + + const onBorrowSelect = (sel) => { + const opt = sel.options[sel.selectedIndex]; + const model = opt.getAttribute('data-model') || ''; + const modelEl = document.getElementById('wh-r-model'); + if (modelEl) modelEl.value = model; + }; + + const submitReturn = async () => { + const borrow_order_no = document.getElementById('wh-r-borrow')?.value; + const model = document.getElementById('wh-r-model')?.value.trim(); + const qty = document.getElementById('wh-r-qty')?.value; + if (!borrow_order_no || !model || !qty) { API.toast('请填写所有必填项'); return; } + const res = await fetch('/api/warehouse/return-orders', { + method: 'POST', credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ borrow_order_no, model, qty: parseInt(qty) }) + }); + const data = await res.json(); + if (data.ok) { + API.toast(`还入单 ${data.order_no} 已提交,等待仓管确认`); + closeModal(); + renderTab(); + } else { + API.toast(data.error || '提交失败'); + } + }; + + // ── 拒绝弹窗 ──────────────────────────────── + const showRejectModal = (type, orderNo) => { + const html = ` +
+
+
拒绝单据
+
单号:${orderNo}
+
+
+ + +
+
+
`; + document.body.insertAdjacentHTML('beforeend', html); + }; + + const doReject = async (type, orderNo) => { + const reject_reason = document.getElementById('wh-reject-reason')?.value.trim(); + if (!reject_reason) { API.toast('请填写拒绝原因'); return; } + const url = type === 'borrow' + ? `/api/warehouse/borrow-orders/${orderNo}/confirm` + : `/api/warehouse/return-orders/${orderNo}/confirm`; + const res = await fetch(url, { + method: 'POST', credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'reject', reject_reason }) + }); + const data = await res.json(); + if (data.ok) { API.toast('已拒绝'); closeModal(); renderTab(); } + else API.toast(data.error || '操作失败'); + }; + + // ── 批准操作 ──────────────────────────────── + const confirmBorrow = async (orderNo, action) => { + const res = await fetch(`/api/warehouse/borrow-orders/${orderNo}/confirm`, { + method: 'POST', credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action }) + }); + const data = await res.json(); + if (data.ok) { API.toast('操作成功'); renderTab(); } + else API.toast(data.error || '操作失败'); + }; + + const confirmReturn = async (orderNo, action) => { + const res = await fetch(`/api/warehouse/return-orders/${orderNo}/confirm`, { + method: 'POST', credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action }) + }); + const data = await res.json(); + if (data.ok) { API.toast('操作成功'); renderTab(); } + else API.toast(data.error || '操作失败'); + }; + + const showNotReceivedModal = (orderNo, totalQty) => { + const html = ` +
+
+
退回还入单
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
`; + document.body.insertAdjacentHTML('beforeend', html); + }; + + const submitNotReceived = async (orderNo, totalQty) => { + const note = document.getElementById('wh-not-recv-note')?.value || ''; + const res = await fetch(`/api/warehouse/return-orders/${orderNo}/confirm`, { + method: 'POST', credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'reject', note }) + }); + const data = await res.json(); + if (data.ok) { closeModal(); API.toast('已退回'); renderTab(); } + else API.toast(data.error || '操作失败'); + }; + + const closeModal = () => { + document.getElementById('wh-modal')?.remove(); + }; + + const renderPage = async (tab) => { + currentTab = tab; + try { + const res = await fetch('/api/auth/me', { credentials: 'include' }); + currentUser = res.ok ? await res.json() : null; + } catch (e) { currentUser = null; } + return render(); + }; + + return { render, renderPage, init, switchTab, showCreateBorrowModal, submitBorrow, + showCreateReturnModal, onBorrowSelect, submitReturn, + showRejectModal, doReject, confirmBorrow, confirmReturn, + showNotReceivedModal, submitNotReceived, closeModal }; +})(); + +Router.register('/warehouse/borrow', async () => { + const html = await window.Warehouse.renderPage('borrow'); + setTimeout(() => { window.Warehouse.init(); }, 0); + return html; +}); + +Router.register('/warehouse/return', async () => { + const html = await window.Warehouse.renderPage('return'); + setTimeout(() => { window.Warehouse.init(); }, 0); + return html; +}); diff --git a/frontend/js/router.js b/frontend/js/router.js index 140ec14..072ff6b 100755 --- a/frontend/js/router.js +++ b/frontend/js/router.js @@ -109,7 +109,10 @@ const Router = (() => { system: '系统', 'operations-log': '操作日志', 'material-purchase': '物料采购', - 'ai-report': 'AI报表' + 'ai-report': 'AI报表', + warehouse: '仓库管理', + borrow: '借出单', + return: '借出还入单' }; return map[key] || key; } diff --git a/server/app.py b/server/app.py index 9f69965..db20a58 100755 --- a/server/app.py +++ b/server/app.py @@ -429,6 +429,43 @@ def init_db(): FOREIGN KEY(outsourcing_order_no) REFERENCES outsourcing_orders(order_no) )''') + # 借出单表 + c.execute('''CREATE TABLE IF NOT EXISTS borrow_orders( + id INTEGER PRIMARY KEY AUTOINCREMENT, + order_no TEXT NOT NULL UNIQUE, + model TEXT NOT NULL, + qty INTEGER NOT NULL, + reason TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'pending', + created_by TEXT NOT NULL, + created_at TEXT NOT NULL, + confirmed_by TEXT, + confirmed_at TEXT, + reject_reason TEXT, + note TEXT + )''') + + # 借出还入单表 + c.execute('''CREATE TABLE IF NOT EXISTS return_orders( + id INTEGER PRIMARY KEY AUTOINCREMENT, + order_no TEXT NOT NULL UNIQUE, + borrow_order_no TEXT NOT NULL, + model TEXT NOT NULL, + qty INTEGER NOT NULL, + received_qty INTEGER, + status TEXT NOT NULL DEFAULT 'pending', + created_by TEXT NOT NULL, + created_at TEXT NOT NULL, + confirmed_by TEXT, + confirmed_at TEXT, + reject_reason TEXT, + note TEXT + )''') + try: + c.execute('ALTER TABLE return_orders ADD COLUMN received_qty INTEGER') + except Exception: + pass + conn.commit() # create default admin c.execute('SELECT id FROM users WHERE username=?', ('admin',)) @@ -8946,6 +8983,258 @@ def favicon(): return '', 404 +# ───────────────────────────────────────────── +# 仓库管理 —— 借出单 +# ───────────────────────────────────────────── + +def _make_borrow_no(): + from datetime import datetime + conn = get_db() + c = conn.cursor() + today = datetime.now().strftime('%Y%m%d') + c.execute("SELECT COUNT(*) FROM borrow_orders WHERE order_no LIKE ?", (f'BW{today}%',)) + cnt = c.fetchone()[0] + conn.close() + return f'BW{today}{cnt+1:03d}' + +def _make_return_no(): + from datetime import datetime + conn = get_db() + c = conn.cursor() + today = datetime.now().strftime('%Y%m%d') + c.execute("SELECT COUNT(*) FROM return_orders WHERE order_no LIKE ?", (f'RT{today}%',)) + cnt = c.fetchone()[0] + conn.close() + return f'RT{today}{cnt+1:03d}' + + +@app.get('/api/warehouse/borrow-orders') +@require_login +def list_borrow_orders(): + status = request.args.get('status', '') + conn = get_db() + c = conn.cursor() + if status: + c.execute('SELECT * FROM borrow_orders WHERE status=? ORDER BY created_at DESC', (status,)) + else: + c.execute('SELECT * FROM borrow_orders ORDER BY created_at DESC') + rows = [dict(r) for r in c.fetchall()] + conn.close() + return jsonify({'ok': True, 'list': rows}) + + +@app.post('/api/warehouse/borrow-orders') +@require_login +@require_any_role('superadmin') +def create_borrow_order(): + data = request.get_json() or {} + model = (data.get('model') or '').strip() + qty = data.get('qty') + reason = (data.get('reason') or '').strip() + if not model or not qty or not reason: + return jsonify({'error': '型号、数量、借出理由不能为空'}), 400 + try: + qty = int(qty) + if qty <= 0: + raise ValueError + except Exception: + return jsonify({'error': '数量必须为正整数'}), 400 + + order_no = _make_borrow_no() + now = get_beijing_time() + username = session.get('username', '') + conn = get_db() + c = conn.cursor() + c.execute( + 'INSERT INTO borrow_orders(order_no,model,qty,reason,status,created_by,created_at) VALUES(?,?,?,?,?,?,?)', + (order_no, model, qty, reason, 'pending', username, now) + ) + conn.commit() + conn.close() + log('create_borrow_order', f'单号:{order_no}, 型号:{model}, 数量:{qty}') + notify_admins('新借出单待确认', f'单号:{order_no}, 型号:{model}, 数量:{qty}, 申请人:{username}') + return jsonify({'ok': True, 'order_no': order_no}) + + +@app.post('/api/warehouse/borrow-orders//confirm') +@require_login +@require_any_role('admin', 'superadmin') +def confirm_borrow_order(order_no): + data = request.get_json() or {} + action = data.get('action', 'approve') # approve / reject + reject_reason = (data.get('reject_reason') or '').strip() + if action == 'reject' and not reject_reason: + return jsonify({'error': '拒绝时必须填写原因'}), 400 + + conn = get_db() + c = conn.cursor() + c.execute('SELECT * FROM borrow_orders WHERE order_no=?', (order_no,)) + row = c.fetchone() + if not row: + conn.close() + return jsonify({'error': '单据不存在'}), 404 + if row['status'] != 'pending': + conn.close() + return jsonify({'error': '该单据已处理'}), 400 + + now = get_beijing_time() + username = session.get('username', '') + new_status = 'approved' if action == 'approve' else 'rejected' + c.execute( + 'UPDATE borrow_orders SET status=?,confirmed_by=?,confirmed_at=?,reject_reason=? WHERE order_no=?', + (new_status, username, now, reject_reason if action == 'reject' else None, order_no) + ) + conn.commit() + conn.close() + + action_text = '已批准' if action == 'approve' else '已拒绝' + log('confirm_borrow_order', f'单号:{order_no}, 操作:{action_text}, 操作人:{username}') + notify_superadmin( + f'借出单{action_text}', + f'单号:{order_no}, 型号:{row["model"]}, 数量:{row["qty"]}, 仓管:{username}' + + (f', 拒绝原因:{reject_reason}' if action == 'reject' else '') + ) + return jsonify({'ok': True}) + + +# ───────────────────────────────────────────── +# 仓库管理 —— 借出还入单 +# ───────────────────────────────────────────── + +@app.get('/api/warehouse/return-orders') +@require_login +def list_return_orders(): + status = request.args.get('status', '') + conn = get_db() + c = conn.cursor() + if status: + c.execute('SELECT * FROM return_orders WHERE status=? ORDER BY created_at DESC', (status,)) + else: + c.execute('SELECT * FROM return_orders ORDER BY created_at DESC') + rows = [dict(r) for r in c.fetchall()] + conn.close() + return jsonify({'ok': True, 'list': rows}) + + +@app.post('/api/warehouse/return-orders') +@require_login +@require_any_role('superadmin') +def create_return_order(): + data = request.get_json() or {} + borrow_order_no = (data.get('borrow_order_no') or '').strip() + model = (data.get('model') or '').strip() + qty = data.get('qty') + if not borrow_order_no or not model or not qty: + return jsonify({'error': '借出单号、型号、数量不能为空'}), 400 + try: + qty = int(qty) + if qty <= 0: + raise ValueError + except Exception: + return jsonify({'error': '数量必须为正整数'}), 400 + + # 验证对应借出单存在且已批准 + conn = get_db() + c = conn.cursor() + c.execute('SELECT * FROM borrow_orders WHERE order_no=?', (borrow_order_no,)) + borrow = c.fetchone() + if not borrow: + conn.close() + return jsonify({'error': '对应借出单不存在'}), 400 + if borrow['status'] != 'approved': + conn.close() + return jsonify({'error': '对应借出单未批准'}), 400 + + order_no = _make_return_no() + now = get_beijing_time() + username = session.get('username', '') + c.execute( + 'INSERT INTO return_orders(order_no,borrow_order_no,model,qty,status,created_by,created_at) VALUES(?,?,?,?,?,?,?)', + (order_no, borrow_order_no, model, qty, 'pending', username, now) + ) + conn.commit() + conn.close() + log('create_return_order', f'单号:{order_no}, 借出单:{borrow_order_no}, 型号:{model}, 数量:{qty}') + notify_admins('新还入单待确认', f'单号:{order_no}, 借出单:{borrow_order_no}, 型号:{model}, 数量:{qty}, 申请人:{username}') + return jsonify({'ok': True, 'order_no': order_no}) + + +@app.post('/api/warehouse/return-orders//confirm') +@require_login +@require_any_role('admin', 'superadmin') +def confirm_return_order(order_no): + data = request.get_json() or {} + action = data.get('action', 'approve') + not_received_qty = data.get('not_received_qty') # 未收到数量(仅action==reject时有效) + note = (data.get('note') or '').strip() + + conn = get_db() + c = conn.cursor() + c.execute('SELECT * FROM return_orders WHERE order_no=?', (order_no,)) + row = c.fetchone() + if not row: + conn.close() + return jsonify({'error': '单据不存在'}), 404 + if row['status'] != 'pending': + conn.close() + return jsonify({'error': '该单据已处理'}), 400 + + total_qty = row['qty'] + now = get_beijing_time() + username = session.get('username', '') + + if action == 'approve': + new_status = 'received' + received_qty = total_qty + reject_reason = None + action_text = '已同意(全部收到)' + else: + # 退回:整单不计入已还 + new_status = 'returned_back' + received_qty = 0 + reject_reason = note or '退回' + action_text = '已退回' + + c.execute( + 'UPDATE return_orders SET status=?,confirmed_by=?,confirmed_at=?,reject_reason=?,received_qty=? WHERE order_no=?', + (new_status, username, now, reject_reason, received_qty, order_no) + ) + conn.commit() + conn.close() + + log('confirm_return_order', f'单号:{order_no}, 操作:{action_text}, 操作人:{username}') + notify_superadmin( + f'还入单{action_text}', + f'单号:{order_no}, 借出单:{row["borrow_order_no"]}, 型号:{row["model"]}, 数量:{total_qty}, 仓管:{username}' + + (f', 备注:{reject_reason}' if action == 'reject' and reject_reason != '退回' else '') + ) + return jsonify({'ok': True}) + + +@app.get('/api/warehouse/unreturned-summary') +@require_login +def unreturned_summary(): + """计算各型号未还数量:已批准借出数 - 已实际收到的还入数(received_qty)""" + conn = get_db() + c = conn.cursor() + c.execute("SELECT model, SUM(qty) as total FROM borrow_orders WHERE status='approved' GROUP BY model") + borrowed = {r['model']: r['total'] for r in c.fetchall()} + # 仅计入状态为received的单据(退回的不计入) + c.execute("SELECT model, SUM(COALESCE(received_qty, qty)) as total FROM return_orders WHERE status='received' GROUP BY model") + returned = {r['model']: r['total'] for r in c.fetchall()} + conn.close() + summary = [] + all_models = set(list(borrowed.keys()) + list(returned.keys())) + for model in sorted(all_models): + b = borrowed.get(model, 0) + r = returned.get(model, 0) + unreturned = b - r + if unreturned != 0: + summary.append({'model': model, 'borrowed': b, 'returned': r, 'unreturned': unreturned}) + total_unreturned = sum(s['unreturned'] for s in summary) + return jsonify({'ok': True, 'summary': summary, 'total_unreturned': total_unreturned}) + + if __name__ == '__main__': print("🚀 启动服务器(已启用静态资源缓存)...") app.run(host='0.0.0.0', port=int(os.environ.get('PORT', '5000')), threaded=True) diff --git a/server/data.db b/server/data.db index 237d9dc..e113837 100644 Binary files a/server/data.db and b/server/data.db differ