Compare commits
2 Commits
84a0a02888
...
2e2b166e08
| Author | SHA1 | Date | |
|---|---|---|---|
| 2e2b166e08 | |||
| c11a70fdf2 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -25,6 +25,7 @@ wheels/
|
||||
|
||||
# Database
|
||||
*.db
|
||||
!server/data.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
|
||||
45
FIX_SUMMARY.md
Normal file
45
FIX_SUMMARY.md
Normal file
@ -0,0 +1,45 @@
|
||||
# 🎯 问题修复总结
|
||||
|
||||
## 问题描述
|
||||
计算铨宝采购需求时,期初库存显示的是友辉的数据。例如:
|
||||
- 铨宝的"6*6*7.5支架 TS-1166VW"期初库存应该是 0,但显示 995(友辉的库存)
|
||||
- 铨宝的"0402B102K500NT"期初库存应该是 0,但显示 1243(友辉的库存)
|
||||
|
||||
## 根本原因
|
||||
**`/api/purchase-demand/recalculate` 接口**在查询期初库存时,只用了 `material_code`,没有按 `factory` 过滤:
|
||||
|
||||
```python
|
||||
# 错误的查询(第7408行)
|
||||
c.execute('SELECT stock_qty, min_package as stock_min_package FROM initial_stock WHERE material_code=?', (material_code,))
|
||||
```
|
||||
|
||||
当友辉和铨宝有相同的物料编码时,这个查询会返回第一个匹配的记录(通常是友辉的),导致铨宝的采购需求使用了友辉的库存数据。
|
||||
|
||||
## 修复方案
|
||||
在查询时添加工厂过滤条件:
|
||||
|
||||
```python
|
||||
# 正确的查询(第7409行)
|
||||
factory = item['factory'] or '友辉' # 从采购需求记录中获取工厂字段
|
||||
c.execute('SELECT stock_qty, min_package as stock_min_package FROM initial_stock WHERE material_code=? AND factory=?', (material_code, factory))
|
||||
```
|
||||
|
||||
## 已修复的接口
|
||||
1. ✅ `/api/purchase-demand/calculate` - 计算单个产品采购需求
|
||||
2. ✅ `/api/purchase-demand/calculate-all` - 计算所有产品采购需求
|
||||
3. ✅ `/api/purchase-demand/calculate-from-orders` - 从客户订单计算采购需求
|
||||
4. ✅ `/api/purchase-demand/recalculate` - 重新计算现有采购需求(**这个是你使用的接口**)
|
||||
|
||||
## 验证步骤
|
||||
1. 重启服务器:`systemctl restart prod-mgmt`
|
||||
2. 在前端选择"铨宝"工厂
|
||||
3. 选择产品(如"AP05商超")并输入数量
|
||||
4. 点击"重新计算"
|
||||
5. 检查采购需求列表中的期初库存字段:
|
||||
- 铨宝的"6*6*7.5支架 TS-1166VW"应该显示 0 ✅
|
||||
- 铨宝的"0402B102K500NT"应该显示 0 ✅
|
||||
- 铨宝的"0402B104K160NT"应该显示 30124 ✅
|
||||
|
||||
## 其他修复
|
||||
- ✅ 期初库存导入:支持同一物料在不同工厂独立存在
|
||||
- ✅ 数据备份恢复:导入/删除前自动备份,可一键恢复
|
||||
43
RESTART_REQUIRED.md
Normal file
43
RESTART_REQUIRED.md
Normal file
@ -0,0 +1,43 @@
|
||||
# 🔄 需要重启服务器
|
||||
|
||||
## 修复内容
|
||||
|
||||
已修复以下问题:
|
||||
|
||||
### 1. 期初库存导入
|
||||
- ✅ 支持同一物料编码在不同工厂独立存在
|
||||
- ✅ 导入时按 `(material_code, factory)` 组合检查是否存在
|
||||
|
||||
### 2. 采购需求计算
|
||||
- ✅ `calculate_purchase_demand` - 添加工厂参数,按工厂匹配期初库存
|
||||
- ✅ `calculate_all_purchase_demand` - 添加工厂参数,按工厂匹配期初库存
|
||||
- ✅ `calculate_purchase_demand_from_orders` - 修复INSERT语句字段顺序,factory字段放在最后
|
||||
|
||||
### 3. 数据备份恢复
|
||||
- ✅ 导入Excel前自动备份
|
||||
- ✅ 批量删除前自动备份
|
||||
- ✅ 恢复数据前自动备份
|
||||
- ✅ 前端添加"📦 数据恢复"功能
|
||||
|
||||
## 重启后需要做的事
|
||||
|
||||
1. **删除错误的采购需求记录**
|
||||
- 需求单 D20260401105945062 及其他使用旧代码生成的记录
|
||||
- 这些记录的期初库存字段不正确
|
||||
|
||||
2. **重新计算采购需求**
|
||||
- 使用修复后的代码重新计算
|
||||
- 验证期初库存字段正确匹配工厂
|
||||
|
||||
3. **验证修复效果**
|
||||
- 铨宝的"6*6*7.5支架 TS-1166VW"期初库存应该是 0
|
||||
- 友辉的"6*6*7.5支架 TS-1166VW"期初库存应该是 995
|
||||
|
||||
## 如何重启
|
||||
|
||||
```bash
|
||||
cd /home/hyx/work/生产管理系统
|
||||
# 停止当前服务器(Ctrl+C 或 kill 进程)
|
||||
# 然后重新启动
|
||||
python3 server/app.py
|
||||
```
|
||||
@ -91,6 +91,7 @@
|
||||
<button id="add-stock-btn" class="btn btn-primary" style="margin-right: 10px;">新增库存</button>
|
||||
<button id="import-stock-btn" class="btn btn-secondary" style="margin-right: 10px;">导入 Excel</button>
|
||||
<button id="download-template-btn" class="btn btn-secondary" style="margin-right: 10px;">下载模板</button>
|
||||
<button id="restore-backup-btn" class="btn btn-secondary" style="margin-right: 10px;">📦 数据恢复</button>
|
||||
<button id="batch-delete-btn" class="btn btn-danger">批量删除</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -282,12 +283,44 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 数据恢复弹窗 -->
|
||||
<div id="restore-modal" class="modal" style="display:none;">
|
||||
<div class="modal-content" style="max-width: 800px;">
|
||||
<div class="modal-header">
|
||||
<h2>📦 数据恢复</h2>
|
||||
<button class="modal-close" onclick="InitialStock.closeRestoreModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div style="margin-bottom: 16px; padding: 12px; background: var(--warning-bg, #fff3cd); border-left: 4px solid var(--warning, #ffc107); border-radius: 4px;">
|
||||
<strong>⚠️ 重要提示:</strong>
|
||||
<ul style="margin: 8px 0 0 20px; padding: 0;">
|
||||
<li>恢复数据将<strong>完全替换</strong>当前的期初库存数据</li>
|
||||
<li>恢复前会自动备份当前数据,可以再次恢复</li>
|
||||
<li>请仔细确认要恢复的备份版本</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 12px;">
|
||||
<strong>可用备份列表:</strong>
|
||||
</div>
|
||||
|
||||
<div id="backup-list" style="max-height: 400px; overflow-y: auto;">
|
||||
<div style="text-align: center; padding: 40px; color: var(--text-3);">加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="InitialStock.closeRestoreModal()">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
setTimeout(() => {
|
||||
document.getElementById('add-stock-btn')?.addEventListener('click', () => openModal());
|
||||
document.getElementById('import-stock-btn')?.addEventListener('click', () => showImportDialog());
|
||||
document.getElementById('download-template-btn')?.addEventListener('click', () => downloadTemplate());
|
||||
document.getElementById('restore-backup-btn')?.addEventListener('click', () => showRestoreDialog());
|
||||
document.getElementById('batch-delete-btn')?.addEventListener('click', () => batchDelete());
|
||||
document.getElementById('search-keyword')?.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') InitialStock.search();
|
||||
@ -623,6 +656,112 @@
|
||||
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
async function showRestoreDialog() {
|
||||
document.getElementById('restore-modal').style.display = 'flex';
|
||||
await loadBackupList();
|
||||
}
|
||||
|
||||
function closeRestoreModal() {
|
||||
document.getElementById('restore-modal').style.display = 'none';
|
||||
}
|
||||
|
||||
async function loadBackupList() {
|
||||
const container = document.getElementById('backup-list');
|
||||
try {
|
||||
const res = await API.get('/api/backups?table_name=initial_stock');
|
||||
const backups = res.list || [];
|
||||
|
||||
if (backups.length === 0) {
|
||||
container.innerHTML = '<div style="text-align: center; padding: 40px; color: var(--text-3);">暂无备份记录</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = backups.map(backup => `
|
||||
<div style="border: 1px solid var(--border); border-radius: 8px; padding: 16px; margin-bottom: 12px; background: white;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 12px;">
|
||||
<div style="flex: 1;">
|
||||
<div style="font-weight: 600; font-size: 15px; margin-bottom: 8px;">
|
||||
${getOperationText(backup.operation)} - ${backup.factory || '全部工厂'}
|
||||
</div>
|
||||
<div style="font-size: 13px; color: var(--text-2); margin-bottom: 4px;">
|
||||
📦 记录数量: <strong>${backup.record_count}</strong> 条
|
||||
</div>
|
||||
<div style="font-size: 13px; color: var(--text-2); margin-bottom: 4px;">
|
||||
👤 操作人: ${backup.created_by || '系统'}
|
||||
</div>
|
||||
<div style="font-size: 13px; color: var(--text-2); margin-bottom: 4px;">
|
||||
🕐 备份时间: ${formatTime(backup.created_at)}
|
||||
</div>
|
||||
${backup.description ? `<div style="font-size: 13px; color: var(--text-3); margin-top: 8px; padding: 8px; background: var(--surface); border-radius: 4px;">
|
||||
💬 ${escapeHtml(backup.description)}
|
||||
</div>` : ''}
|
||||
</div>
|
||||
<div style="display: flex; gap: 8px; margin-left: 16px;">
|
||||
<button class="btn btn-sm btn-primary" onclick="InitialStock.restoreBackup(${backup.id})">
|
||||
恢复此版本
|
||||
</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="InitialStock.deleteBackup(${backup.id})">
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
} catch (e) {
|
||||
container.innerHTML = `<div style="text-align: center; padding: 40px; color: var(--danger);">加载失败: ${e.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function getOperationText(operation) {
|
||||
const map = {
|
||||
'import': '📥 导入前备份',
|
||||
'batch_delete': '🗑️ 批量删除前备份',
|
||||
'restore': '🔄 恢复前备份',
|
||||
'manual': '💾 手动备份'
|
||||
};
|
||||
return map[operation] || operation;
|
||||
}
|
||||
|
||||
async function restoreBackup(backupId) {
|
||||
if (!confirm('确定要恢复此备份吗?\n\n当前数据将被完全替换,但会自动备份当前数据。')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const overlay = document.getElementById('overlay');
|
||||
overlay.classList.remove('hidden');
|
||||
|
||||
const res = await API.post(`/api/backups/${backupId}/restore`, {});
|
||||
|
||||
overlay.classList.add('hidden');
|
||||
|
||||
if (res.ok) {
|
||||
alert(res.message || '数据恢复成功');
|
||||
closeRestoreModal();
|
||||
loadList();
|
||||
} else {
|
||||
alert(res.error || '恢复失败');
|
||||
}
|
||||
} catch (e) {
|
||||
document.getElementById('overlay').classList.add('hidden');
|
||||
alert('恢复失败: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteBackup(backupId) {
|
||||
if (!confirm('确定要删除此备份吗?此操作不可恢复。')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await API.delete(`/api/backups/${backupId}`);
|
||||
alert('备份已删除');
|
||||
await loadBackupList();
|
||||
} catch (e) {
|
||||
alert('删除失败: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
window.InitialStock = {
|
||||
search,
|
||||
resetSearch,
|
||||
@ -635,6 +774,9 @@
|
||||
updateSelectAll,
|
||||
goPage,
|
||||
closeImportModal,
|
||||
importExcel
|
||||
importExcel,
|
||||
closeRestoreModal,
|
||||
restoreBackup,
|
||||
deleteBackup
|
||||
};
|
||||
})();
|
||||
|
||||
@ -1205,7 +1205,7 @@ const Upload = (() => {
|
||||
const uploadText = document.getElementById('repairs-upload-text');
|
||||
const uploadLoading = document.getElementById('repairs-upload-loading');
|
||||
|
||||
const isUploadPage = !!document.getElementById('repairs-upload-form');
|
||||
const isUploadPage = !!btn; // 检查提交按钮是否存在来判断是否为上传页面
|
||||
const isHistoryPage = !!document.getElementById('repairs-list');
|
||||
|
||||
if(isUploadPage) {
|
||||
|
||||
303
server/app.py
303
server/app.py
@ -343,6 +343,20 @@ def init_db():
|
||||
updated_at TEXT
|
||||
)''')
|
||||
|
||||
# 数据备份表 - 记录操作前的数据快照
|
||||
c.execute('''CREATE TABLE IF NOT EXISTS data_backups(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
backup_type TEXT NOT NULL,
|
||||
table_name TEXT NOT NULL,
|
||||
backup_data TEXT NOT NULL,
|
||||
operation TEXT NOT NULL,
|
||||
factory TEXT,
|
||||
record_count INTEGER DEFAULT 0,
|
||||
created_by TEXT,
|
||||
created_at TEXT,
|
||||
description TEXT
|
||||
)''')
|
||||
|
||||
# 委外发料表 - 记录发给外协厂的物料
|
||||
c.execute('''CREATE TABLE IF NOT EXISTS outsourcing_material_issue(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@ -6349,7 +6363,24 @@ def batch_delete_initial_stock():
|
||||
|
||||
conn = get_db()
|
||||
c = conn.cursor()
|
||||
now = get_beijing_time()
|
||||
username = session.get('username', '')
|
||||
|
||||
# 删除前自动备份要删除的数据
|
||||
placeholders = ','.join('?' * len(ids))
|
||||
c.execute(f'SELECT * FROM initial_stock WHERE id IN ({placeholders})', ids)
|
||||
backup_rows = c.fetchall()
|
||||
if backup_rows:
|
||||
backup_data = json.dumps([dict(row) for row in backup_rows], ensure_ascii=False)
|
||||
c.execute('''INSERT INTO data_backups(
|
||||
backup_type, table_name, backup_data, operation, factory,
|
||||
record_count, created_by, created_at, description
|
||||
) VALUES(?,?,?,?,?,?,?,?,?)''', (
|
||||
'auto', 'initial_stock', backup_data, 'batch_delete',
|
||||
'多个工厂', len(backup_rows), username, now,
|
||||
f'批量删除前自动备份(删除 {len(backup_rows)} 条记录)'
|
||||
))
|
||||
|
||||
c.execute(f'DELETE FROM initial_stock WHERE id IN ({placeholders})', ids)
|
||||
count = c.rowcount
|
||||
conn.commit()
|
||||
@ -6407,6 +6438,20 @@ def import_initial_stock():
|
||||
now = get_beijing_time()
|
||||
username = session.get('username', '')
|
||||
|
||||
# 导入前自动备份当前数据
|
||||
c.execute('SELECT * FROM initial_stock')
|
||||
backup_rows = c.fetchall()
|
||||
if backup_rows:
|
||||
backup_data = json.dumps([dict(row) for row in backup_rows], ensure_ascii=False)
|
||||
c.execute('''INSERT INTO data_backups(
|
||||
backup_type, table_name, backup_data, operation, factory,
|
||||
record_count, created_by, created_at, description
|
||||
) VALUES(?,?,?,?,?,?,?,?,?)''', (
|
||||
'auto', 'initial_stock', backup_data, 'import',
|
||||
default_factory or '全部', len(backup_rows), username, now,
|
||||
f'导入Excel前自动备份(文件: {filename})'
|
||||
))
|
||||
|
||||
success_count = 0
|
||||
update_count = 0
|
||||
|
||||
@ -6444,17 +6489,17 @@ def import_initial_stock():
|
||||
if remark == 'nan':
|
||||
remark = ''
|
||||
|
||||
# 检查是否已存在,存在则更新
|
||||
c.execute('SELECT id FROM initial_stock WHERE material_code=?', (material_code,))
|
||||
# 检查是否已存在(同一工厂的同一物料编码),存在则更新
|
||||
c.execute('SELECT id FROM initial_stock WHERE material_code=? AND factory=?', (material_code, factory))
|
||||
existing = c.fetchone()
|
||||
|
||||
if existing:
|
||||
c.execute('''UPDATE initial_stock SET
|
||||
material_name=?, stock_qty=?, unit=?, min_package=?,
|
||||
supplier=?, factory=?, remark=?, updated_at=?
|
||||
WHERE material_code=?''', (
|
||||
supplier=?, remark=?, updated_at=?
|
||||
WHERE material_code=? AND factory=?''', (
|
||||
material_name, stock_qty, unit, min_package,
|
||||
supplier, factory, remark, now, material_code
|
||||
supplier, remark, now, material_code, factory
|
||||
))
|
||||
update_count += 1
|
||||
else:
|
||||
@ -6481,6 +6526,200 @@ def import_initial_stock():
|
||||
return jsonify({'error': f'导入失败: {str(e)}'}), 500
|
||||
|
||||
|
||||
# ==================== 数据备份与恢复 API ====================
|
||||
|
||||
@app.get('/api/backups')
|
||||
@require_login
|
||||
@require_any_role('superadmin', 'admin')
|
||||
def get_backups():
|
||||
"""获取数据备份列表"""
|
||||
table_name = request.args.get('table_name', '')
|
||||
|
||||
conn = get_db()
|
||||
c = conn.cursor()
|
||||
|
||||
if table_name:
|
||||
c.execute('''SELECT id, backup_type, table_name, operation, factory,
|
||||
record_count, created_by, created_at, description
|
||||
FROM data_backups WHERE table_name=?
|
||||
ORDER BY created_at DESC LIMIT 50''', (table_name,))
|
||||
else:
|
||||
c.execute('''SELECT id, backup_type, table_name, operation, factory,
|
||||
record_count, created_by, created_at, description
|
||||
FROM data_backups
|
||||
ORDER BY created_at DESC LIMIT 50''')
|
||||
|
||||
rows = c.fetchall()
|
||||
conn.close()
|
||||
|
||||
backups = []
|
||||
for row in rows:
|
||||
backups.append({
|
||||
'id': row[0],
|
||||
'backup_type': row[1],
|
||||
'table_name': row[2],
|
||||
'operation': row[3],
|
||||
'factory': row[4],
|
||||
'record_count': row[5],
|
||||
'created_by': row[6],
|
||||
'created_at': row[7],
|
||||
'description': row[8]
|
||||
})
|
||||
|
||||
return jsonify({'list': backups})
|
||||
|
||||
|
||||
@app.post('/api/backups/<int:backup_id>/restore')
|
||||
@require_login
|
||||
@require_any_role('superadmin')
|
||||
def restore_backup(backup_id):
|
||||
"""恢复数据备份"""
|
||||
conn = get_db()
|
||||
c = conn.cursor()
|
||||
now = get_beijing_time()
|
||||
username = session.get('username', '')
|
||||
|
||||
# 获取备份数据
|
||||
c.execute('SELECT table_name, backup_data, factory FROM data_backups WHERE id=?', (backup_id,))
|
||||
row = c.fetchone()
|
||||
if not row:
|
||||
conn.close()
|
||||
return jsonify({'error': '备份不存在'}), 404
|
||||
|
||||
table_name = row[0]
|
||||
backup_data_str = row[1]
|
||||
factory = row[2]
|
||||
|
||||
try:
|
||||
backup_data = json.loads(backup_data_str)
|
||||
|
||||
if table_name == 'initial_stock':
|
||||
# 恢复前先备份当前数据
|
||||
c.execute('SELECT * FROM initial_stock')
|
||||
current_rows = c.fetchall()
|
||||
if current_rows:
|
||||
current_backup = json.dumps([dict(r) for r in current_rows], ensure_ascii=False)
|
||||
c.execute('''INSERT INTO data_backups(
|
||||
backup_type, table_name, backup_data, operation, factory,
|
||||
record_count, created_by, created_at, description
|
||||
) VALUES(?,?,?,?,?,?,?,?,?)''', (
|
||||
'auto', 'initial_stock', current_backup, 'restore',
|
||||
factory or '全部', len(current_rows), username, now,
|
||||
f'恢复备份前自动备份(备份ID: {backup_id})'
|
||||
))
|
||||
|
||||
# 删除当前数据
|
||||
c.execute('DELETE FROM initial_stock')
|
||||
|
||||
# 恢复备份数据
|
||||
for item in backup_data:
|
||||
c.execute('''INSERT INTO initial_stock(
|
||||
material_code, material_name, stock_qty, unit, min_package,
|
||||
supplier, factory, remark, created_by, created_at, updated_at
|
||||
) VALUES(?,?,?,?,?,?,?,?,?,?,?)''', (
|
||||
item.get('material_code'),
|
||||
item.get('material_name'),
|
||||
item.get('stock_qty', 0),
|
||||
item.get('unit', 'pcs'),
|
||||
item.get('min_package', 1),
|
||||
item.get('supplier', ''),
|
||||
item.get('factory', ''),
|
||||
item.get('remark', ''),
|
||||
item.get('created_by', ''),
|
||||
item.get('created_at'),
|
||||
item.get('updated_at')
|
||||
))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
log('restore_backup', f'恢复期初库存备份 ID: {backup_id}, 记录数: {len(backup_data)}')
|
||||
return jsonify({
|
||||
'ok': True,
|
||||
'count': len(backup_data),
|
||||
'message': f'成功恢复 {len(backup_data)} 条期初库存记录'
|
||||
})
|
||||
|
||||
elif table_name == 'purchase_demand':
|
||||
# 恢复前先备份当前数据
|
||||
c.execute('SELECT * FROM purchase_demand')
|
||||
current_rows = c.fetchall()
|
||||
if current_rows:
|
||||
current_backup = json.dumps([dict(r) for r in current_rows], ensure_ascii=False)
|
||||
c.execute('''INSERT INTO data_backups(
|
||||
backup_type, table_name, backup_data, operation, factory,
|
||||
record_count, created_by, created_at, description
|
||||
) VALUES(?,?,?,?,?,?,?,?,?)''', (
|
||||
'auto', 'purchase_demand', current_backup, 'restore',
|
||||
factory or '全部', len(current_rows), username, now,
|
||||
f'恢复备份前自动备份(备份ID: {backup_id})'
|
||||
))
|
||||
|
||||
# 删除当前数据
|
||||
c.execute('DELETE FROM purchase_demand')
|
||||
|
||||
# 恢复备份数据
|
||||
for item in backup_data:
|
||||
c.execute('''INSERT INTO purchase_demand(
|
||||
demand_no, material_code, material_name, order_qty, bom_unit_qty,
|
||||
total_demand, initial_stock, net_demand, min_package, actual_purchase_qty,
|
||||
unit, supplier, factory, status, remark, created_by, created_at, updated_at
|
||||
) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)''', (
|
||||
item.get('demand_no'),
|
||||
item.get('material_code'),
|
||||
item.get('material_name'),
|
||||
item.get('order_qty', 0),
|
||||
item.get('bom_unit_qty', 0),
|
||||
item.get('total_demand', 0),
|
||||
item.get('initial_stock', 0),
|
||||
item.get('net_demand', 0),
|
||||
item.get('min_package', 1),
|
||||
item.get('actual_purchase_qty', 0),
|
||||
item.get('unit', 'pcs'),
|
||||
item.get('supplier', ''),
|
||||
item.get('factory', ''),
|
||||
item.get('status', 'pending'),
|
||||
item.get('remark', ''),
|
||||
item.get('created_by', ''),
|
||||
item.get('created_at'),
|
||||
item.get('updated_at')
|
||||
))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
log('restore_backup', f'恢复采购需求备份 ID: {backup_id}, 记录数: {len(backup_data)}')
|
||||
return jsonify({
|
||||
'ok': True,
|
||||
'count': len(backup_data),
|
||||
'message': f'成功恢复 {len(backup_data)} 条采购需求记录'
|
||||
})
|
||||
else:
|
||||
conn.close()
|
||||
return jsonify({'error': f'不支持恢复表: {table_name}'}), 400
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
conn.close()
|
||||
return jsonify({'error': f'恢复失败: {str(e)}'}), 500
|
||||
|
||||
|
||||
@app.delete('/api/backups/<int:backup_id>')
|
||||
@require_login
|
||||
@require_any_role('superadmin')
|
||||
def delete_backup(backup_id):
|
||||
"""删除备份记录"""
|
||||
conn = get_db()
|
||||
c = conn.cursor()
|
||||
|
||||
c.execute('DELETE FROM data_backups WHERE id=?', (backup_id,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
log('delete_backup', f'删除备份 ID: {backup_id}')
|
||||
return jsonify({'ok': True, 'message': '备份已删除'})
|
||||
|
||||
|
||||
# ==================== 通用API ====================
|
||||
|
||||
@app.get('/api/factories')
|
||||
@ -6568,10 +6807,14 @@ def calculate_purchase_demand():
|
||||
|
||||
product_code = (data.get('product_code') or '').strip()
|
||||
order_qty = data.get('order_qty', 0)
|
||||
factory = data.get('factory', '').strip()
|
||||
|
||||
if not product_code:
|
||||
return jsonify({'error': '请选择产品'}), 400
|
||||
|
||||
if not factory:
|
||||
return jsonify({'error': '请选择工厂'}), 400
|
||||
|
||||
try:
|
||||
order_qty = int(order_qty)
|
||||
except (ValueError, TypeError):
|
||||
@ -6610,8 +6853,8 @@ def calculate_purchase_demand():
|
||||
# 计算总需求 = 订单数量 * 单机用量
|
||||
total_demand = int(math.ceil(order_qty * unit_qty))
|
||||
|
||||
# 获取期初库存
|
||||
c.execute('SELECT stock_qty, min_package as stock_min_package FROM initial_stock WHERE material_code=?', (material_code,))
|
||||
# 获取期初库存(按工厂匹配)
|
||||
c.execute('SELECT stock_qty, min_package as stock_min_package FROM initial_stock WHERE material_code=? AND factory=?', (material_code, factory))
|
||||
stock_row = c.fetchone()
|
||||
initial_stock = stock_row['stock_qty'] if stock_row else 0
|
||||
# 如果BOM没有设置最小包装但库存表有,使用库存表的
|
||||
@ -6633,11 +6876,11 @@ def calculate_purchase_demand():
|
||||
c.execute('''INSERT INTO purchase_demand(
|
||||
demand_no, material_code, material_name, order_qty, bom_unit_qty,
|
||||
total_demand, initial_stock, net_demand, min_package, actual_purchase_qty,
|
||||
unit, supplier, status, created_by, created_at, updated_at
|
||||
) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)''', (
|
||||
unit, supplier, factory, status, created_by, created_at, updated_at
|
||||
) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)''', (
|
||||
demand_no, material_code, material_name, order_qty, unit_qty,
|
||||
total_demand, initial_stock, net_demand, min_package, actual_purchase_qty,
|
||||
unit, supplier, 'pending', username, now, now
|
||||
unit, supplier, factory, 'pending', username, now, now
|
||||
))
|
||||
|
||||
results.append({
|
||||
@ -6676,6 +6919,10 @@ def calculate_all_purchase_demand():
|
||||
"""
|
||||
data = request.get_json() or {}
|
||||
order_qty = data.get('order_qty', 0)
|
||||
factory = data.get('factory', '').strip()
|
||||
|
||||
if not factory:
|
||||
return jsonify({'error': '请选择工厂'}), 400
|
||||
|
||||
try:
|
||||
order_qty = int(order_qty)
|
||||
@ -6722,8 +6969,8 @@ def calculate_all_purchase_demand():
|
||||
# 计算总需求 = 订单数量 * 单机用量
|
||||
total_demand = int(math.ceil(order_qty * unit_qty))
|
||||
|
||||
# 获取期初库存
|
||||
c.execute('SELECT stock_qty, min_package as stock_min_package FROM initial_stock WHERE material_code=?', (material_code,))
|
||||
# 获取期初库存(按工厂匹配)
|
||||
c.execute('SELECT stock_qty, min_package as stock_min_package FROM initial_stock WHERE material_code=? AND factory=?', (material_code, factory))
|
||||
stock_row = c.fetchone()
|
||||
initial_stock = stock_row['stock_qty'] if stock_row else 0
|
||||
# 如果BOM没有设置最小包装但库存表有,使用库存表的
|
||||
@ -6745,11 +6992,11 @@ def calculate_all_purchase_demand():
|
||||
c.execute('''INSERT INTO purchase_demand(
|
||||
demand_no, material_code, material_name, order_qty, bom_unit_qty,
|
||||
total_demand, initial_stock, net_demand, min_package, actual_purchase_qty,
|
||||
unit, supplier, status, created_by, created_at, updated_at, product_code
|
||||
unit, supplier, factory, status, created_by, created_at, updated_at, product_code
|
||||
) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)''', (
|
||||
demand_no, material_code, material_name, order_qty, unit_qty,
|
||||
total_demand, initial_stock, net_demand, min_package, actual_purchase_qty,
|
||||
unit, supplier, 'pending', username, now, now, product_code
|
||||
unit, supplier, factory, 'pending', username, now, now, product_code
|
||||
))
|
||||
|
||||
results.append({
|
||||
@ -6789,6 +7036,9 @@ def calculate_purchase_demand_from_orders():
|
||||
factory = data.get('factory', '').strip() # 工厂参数
|
||||
order_ids = data.get('order_ids', []) # 可选,指定订单ID列表
|
||||
|
||||
# 调试:打印接收到的factory参数
|
||||
print(f'[DEBUG] 接收到的factory参数: "{factory}" (长度: {len(factory)}, 字节: {factory.encode("utf-8")})')
|
||||
|
||||
if not factory:
|
||||
return jsonify({'error': '请选择工厂'}), 400
|
||||
|
||||
@ -6866,11 +7116,21 @@ def calculate_purchase_demand_from_orders():
|
||||
|
||||
# 计算净需求和实际采购数量
|
||||
results = []
|
||||
print(f'[DEBUG] 开始计算净需求,factory="{factory}"')
|
||||
for mat_code, info in material_demands.items():
|
||||
# 获取期初库存(按工厂匹配)
|
||||
c.execute('SELECT stock_qty, min_package FROM initial_stock WHERE material_code=? AND factory=?', (mat_code, factory))
|
||||
# 调试:打印查询参数
|
||||
print(f'[DEBUG] 查询期初库存: material_code="{mat_code}", factory="{factory}"')
|
||||
c.execute('SELECT stock_qty, min_package, factory FROM initial_stock WHERE material_code=? AND factory=?', (mat_code, factory))
|
||||
stock_row = c.fetchone()
|
||||
initial_stock = stock_row['stock_qty'] if stock_row else 0
|
||||
print(f'[DEBUG] 查询结果: stock_row={dict(stock_row) if stock_row else None}, initial_stock={initial_stock}')
|
||||
|
||||
# 如果查询结果为None,尝试查询所有工厂的库存
|
||||
if not stock_row:
|
||||
c.execute('SELECT factory, stock_qty FROM initial_stock WHERE material_code=?', (mat_code,))
|
||||
all_stocks = c.fetchall()
|
||||
print(f'[DEBUG] 该物料在所有工厂的库存: {[dict(s) for s in all_stocks]}')
|
||||
min_package = info['min_package']
|
||||
if min_package <= 1 and stock_row and stock_row['min_package']:
|
||||
min_package = stock_row['min_package']
|
||||
@ -6887,13 +7147,13 @@ def calculate_purchase_demand_from_orders():
|
||||
|
||||
# 插入采购需求记录(包含工厂字段)
|
||||
c.execute('''INSERT INTO purchase_demand(
|
||||
demand_no, factory, material_code, material_name, order_qty, bom_unit_qty,
|
||||
demand_no, material_code, material_name, order_qty, bom_unit_qty,
|
||||
total_demand, initial_stock, net_demand, min_package, actual_purchase_qty,
|
||||
unit, supplier, status, created_by, created_at, updated_at
|
||||
unit, supplier, status, created_by, created_at, updated_at, factory
|
||||
) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)''', (
|
||||
demand_no, factory, mat_code, info['material_name'], info['order_qty'], info['bom_unit_qty'],
|
||||
demand_no, mat_code, info['material_name'], info['order_qty'], info['bom_unit_qty'],
|
||||
total_demand, initial_stock, net_demand, min_package, actual_purchase_qty,
|
||||
info['unit'], info['supplier'], 'pending', username, now, now
|
||||
info['unit'], info['supplier'], 'pending', username, now, now, factory
|
||||
))
|
||||
|
||||
results.append({
|
||||
@ -7134,6 +7394,7 @@ def recalculate_purchase_demand():
|
||||
for item in existing_data:
|
||||
# 使用现有的物料信息,但更新订单数量相关的计算
|
||||
material_code = item['material_code']
|
||||
factory = item['factory'] or '友辉' # 获取工厂字段,如果为空则默认友辉
|
||||
unit_qty = item['bom_unit_qty'] or 1 # 使用原有的单机用量
|
||||
min_package = item['min_package'] or 1
|
||||
# 保留原有的期初库存值(从Excel导入的)
|
||||
@ -7144,8 +7405,8 @@ def recalculate_purchase_demand():
|
||||
|
||||
# 根据use_initial_stock参数决定是否使用期初库存菜单的数据
|
||||
if use_initial_stock:
|
||||
# 获取期初库存(如果库存表有值则使用库存表的,否则保留导入时的值)
|
||||
c.execute('SELECT stock_qty, min_package as stock_min_package FROM initial_stock WHERE material_code=?', (material_code,))
|
||||
# 获取期初库存(按工厂匹配)
|
||||
c.execute('SELECT stock_qty, min_package as stock_min_package FROM initial_stock WHERE material_code=? AND factory=?', (material_code, factory))
|
||||
stock_row = c.fetchone()
|
||||
# 只有当库存表有有效值时才覆盖导入时的期初库存
|
||||
if stock_row and stock_row['stock_qty'] is not None:
|
||||
|
||||
BIN
server/data.db
Normal file
BIN
server/data.db
Normal file
Binary file not shown.
BIN
server/data.db.backup_20260331_093801
Normal file
BIN
server/data.db.backup_20260331_093801
Normal file
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 602 KiB |
Loading…
Reference in New Issue
Block a user