Compare commits

...

2 Commits

Author SHA1 Message Date
zzh
2e2b166e08 add file 2026-04-08 11:25:01 +08:00
zzh
c11a70fdf2 修复采购需求计算和返修记录上传功能
- 修复采购需求计算时工厂匹配问题
  - calculate_purchase_demand: 添加工厂参数并按工厂过滤期初库存
  - calculate_all_purchase_demand: 添加工厂参数并按工厂过滤期初库存
  - calculate_purchase_demand_from_orders: 修复INSERT语句字段顺序
  - recalculate_purchase_demand: 添加工厂参数并按工厂过滤期初库存

- 修复返修记录上传页面按钮无响应问题
  - 修正isUploadPage判断逻辑,使事件监听器正确绑定
  - 图片上传、提交、清空按钮现在可以正常工作

- 添加数据库到Git仓库
  - 修改.gitignore允许server/data.db被提交
  - 包含所有生产数据和配置
2026-04-08 11:24:26 +08:00
9 changed files with 515 additions and 23 deletions

1
.gitignore vendored
View File

@ -25,6 +25,7 @@ wheels/
# Database
*.db
!server/data.db
*.sqlite
*.sqlite3

45
FIX_SUMMARY.md Normal file
View 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
View 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
```

View File

@ -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()">&times;</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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
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
};
})();

View File

@ -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) {

View File

@ -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

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 KiB