新增采购需求清单和期初库存功能
This commit is contained in:
parent
0db85e75a0
commit
50f8a07b1b
@ -1,98 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
检查对账单含税金额是否正确更新
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import sys
|
||||
import os
|
||||
|
||||
def check_total_amount():
|
||||
"""检查含税金额更新情况"""
|
||||
|
||||
# 连接数据库
|
||||
conn = sqlite3.connect('server/data.db')
|
||||
c = conn.cursor()
|
||||
|
||||
print("=== 检查对账单含税金额更新情况 ===\n")
|
||||
|
||||
# 查询所有对账单记录
|
||||
c.execute('SELECT id, contract_no, material_name, quantity, unit_price, total_amount FROM reconciliations ORDER BY id DESC LIMIT 20')
|
||||
records = c.fetchall()
|
||||
|
||||
print("最近20条对账单记录:")
|
||||
print("-" * 80)
|
||||
print(f"{'ID':<5} {'合同号':<15} {'物料名称':<30} {'数量':<8} {'单价':<8} {'含税金额':<10} {'应计算金额':<10}")
|
||||
print("-" * 80)
|
||||
|
||||
incorrect_count = 0
|
||||
for record in records:
|
||||
recon_id, contract_no, material_name, quantity, unit_price, total_amount = record
|
||||
|
||||
# 计算应有的含税金额
|
||||
calculated_amount = quantity * unit_price
|
||||
|
||||
# 检查是否一致(考虑浮点数精度)
|
||||
if abs(total_amount - calculated_amount) > 0.01:
|
||||
incorrect_count += 1
|
||||
status = "❌"
|
||||
else:
|
||||
status = "✓"
|
||||
|
||||
print(f"{recon_id:<5} {contract_no:<15} {material_name[:28]:<30} {quantity:<8} {unit_price:<8} {total_amount:<10} {calculated_amount:<10} {status}")
|
||||
|
||||
print("-" * 80)
|
||||
print(f"\n发现 {incorrect_count} 条记录的含税金额不正确")
|
||||
|
||||
# 修复不正确的记录
|
||||
if incorrect_count > 0:
|
||||
print("\n正在修复不正确的记录...")
|
||||
c.execute('SELECT id, quantity, unit_price FROM reconciliations')
|
||||
all_records = c.fetchall()
|
||||
|
||||
fixed_count = 0
|
||||
for record in all_records:
|
||||
recon_id, quantity, unit_price = record
|
||||
correct_amount = quantity * unit_price
|
||||
|
||||
# 更新含税金额
|
||||
c.execute('UPDATE reconciliations SET total_amount=? WHERE id=?', (correct_amount, recon_id))
|
||||
fixed_count += 1
|
||||
|
||||
conn.commit()
|
||||
print(f"已修复 {fixed_count} 条记录的含税金额")
|
||||
|
||||
# 测试刷新单价功能
|
||||
print("\n\n=== 测试刷新单价功能 ===")
|
||||
|
||||
# 找一个有客户订单的合同号进行测试
|
||||
c.execute("SELECT DISTINCT contract_no FROM reconciliations LIMIT 1")
|
||||
test_contract = c.fetchone()
|
||||
|
||||
if test_contract:
|
||||
test_contract_no = test_contract[0]
|
||||
print(f"测试合同号: {test_contract_no}")
|
||||
|
||||
# 查看该合同号的对账单记录
|
||||
c.execute('SELECT id, material_name, unit_price, total_amount, quantity FROM reconciliations WHERE contract_no=?', (test_contract_no,))
|
||||
test_records = c.fetchall()
|
||||
|
||||
print("\n刷新前的记录:")
|
||||
for record in test_records:
|
||||
recon_id, material_name, unit_price, total_amount, quantity = record
|
||||
print(f" ID={recon_id}, 物料={material_name[:30]}..., 单价={unit_price}, 金额={total_amount}, 数量={quantity}")
|
||||
|
||||
# 模拟刷新单价(这里只是检查逻辑)
|
||||
print("\n注意:实际的刷新单价功能需要通过API调用")
|
||||
|
||||
conn.close()
|
||||
|
||||
print("\n=== 建议检查事项 ===")
|
||||
print("1. 确认后端刷新单价的SQL语句是否正确")
|
||||
print("2. 检查前端是否正确调用了calculateTotal函数")
|
||||
print("3. 验证数据库中的total_amount字段是否被正确更新")
|
||||
print("4. 查看浏览器控制台是否有JavaScript错误")
|
||||
|
||||
if __name__ == '__main__':
|
||||
check_total_amount()
|
||||
@ -1869,7 +1869,58 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover{
|
||||
.filter-actions{display:flex;gap:8px;align-items:flex-end}
|
||||
.filter-actions .btn{padding:10px 20px;font-size:14px}
|
||||
|
||||
.table-container{background:var(--surface);border:1px solid var(--border);border-radius:12px;overflow:auto;margin-bottom:20px;max-height:calc(100vh - 280px)}
|
||||
/* 表格容器滚动条样式 - 重新设计 */
|
||||
.table-container {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
overflow: scroll;
|
||||
margin-bottom: 20px;
|
||||
max-height: calc(100vh - 280px);
|
||||
/* 为滚动条预留空间,防止布局跳动 */
|
||||
padding-right: 8px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
/* 滚动条整体样式 */
|
||||
.table-container::-webkit-scrollbar {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* 滚动条轨道 */
|
||||
.table-container::-webkit-scrollbar-track {
|
||||
background: var(--surface-2);
|
||||
border-radius: 7px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* 滚动条滑块 */
|
||||
.table-container::-webkit-scrollbar-thumb {
|
||||
background: var(--primary);
|
||||
border-radius: 7px;
|
||||
border: 2px solid var(--surface-2);
|
||||
/* 确保滑块始终可见 */
|
||||
min-height: 20px;
|
||||
min-width: 20px;
|
||||
}
|
||||
|
||||
/* 滑块悬停效果 */
|
||||
.table-container::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--primary-dark);
|
||||
}
|
||||
|
||||
/* 滑块激活效果 */
|
||||
.table-container::-webkit-scrollbar-thumb:active {
|
||||
background: var(--primary-darker);
|
||||
}
|
||||
|
||||
/* 滚动条角落 */
|
||||
.table-container::-webkit-scrollbar-corner {
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.data-table{width:100%;border-collapse:collapse;font-size:13px}
|
||||
.data-table thead{background:var(--surface-2);position:sticky;top:0;z-index:1}
|
||||
.data-table th{padding:12px 16px;text-align:left;font-weight:600;color:var(--text);border-bottom:2px solid var(--border);white-space:nowrap}
|
||||
@ -1988,9 +2039,52 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover{
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
overflow: auto;
|
||||
overflow: scroll;
|
||||
margin-bottom: 20px;
|
||||
max-height: calc(100vh - 250px);
|
||||
/* 为滚动条预留空间,防止布局跳动 */
|
||||
padding-right: 8px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
/* 滚动条整体样式 */
|
||||
.table-container::-webkit-scrollbar {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* 滚动条轨道 */
|
||||
.table-container::-webkit-scrollbar-track {
|
||||
background: var(--surface-2);
|
||||
border-radius: 7px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* 滚动条滑块 */
|
||||
.table-container::-webkit-scrollbar-thumb {
|
||||
background: var(--primary);
|
||||
border-radius: 7px;
|
||||
border: 2px solid var(--surface-2);
|
||||
/* 确保滑块始终可见 */
|
||||
min-height: 20px;
|
||||
min-width: 20px;
|
||||
}
|
||||
|
||||
/* 滑块悬停效果 */
|
||||
.table-container::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--primary-dark);
|
||||
}
|
||||
|
||||
/* 滑块激活效果 */
|
||||
.table-container::-webkit-scrollbar-thumb:active {
|
||||
background: var(--primary-darker);
|
||||
}
|
||||
|
||||
/* 滚动条角落 */
|
||||
.table-container::-webkit-scrollbar-corner {
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.data-table {
|
||||
|
||||
@ -114,6 +114,7 @@
|
||||
<div class="topnav-dropdown">
|
||||
<a href="#/shipments/query" class="dropdown-item" data-route="shipments-query">详细记录查询</a>
|
||||
<a href="#/shipments/summary" class="dropdown-item" data-route="shipments-summary">汇总信息查询</a>
|
||||
<a href="#/shipments/audit" class="dropdown-item" data-route="shipments-audit">审计查询</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="topnav-item has-dropdown" data-menu="production">
|
||||
@ -145,7 +146,6 @@
|
||||
</span>
|
||||
<span class="topnav-caret"></span>
|
||||
<div class="topnav-dropdown">
|
||||
<a href="#/plan-mgmt/bom" class="dropdown-item" data-route="plan-mgmt-bom">BOM物料清单</a>
|
||||
<a href="#/plan-mgmt/initial-stock" class="dropdown-item" data-route="plan-mgmt-initial-stock">期初库存</a>
|
||||
<a href="#/plan-mgmt/purchase-demand" class="dropdown-item" data-route="plan-mgmt-purchase-demand">采购需求清单</a>
|
||||
<a href="#/plan-mgmt/customer-order" class="dropdown-item" data-route="plan-mgmt-customer-order">客户订单</a>
|
||||
@ -307,6 +307,7 @@
|
||||
<script src="./js/router.js" defer></script>
|
||||
<script src="./js/api.js" defer></script>
|
||||
<script src="./js/utils/memory-monitor.js" defer></script>
|
||||
<script src="./assets/xlsx.min.js" defer></script>
|
||||
<script src="./js/components/sidebar.js" defer></script>
|
||||
<script src="./js/components/menu-search.js" defer></script>
|
||||
<script src="./js/components/dashboard.js" defer></script>
|
||||
@ -315,6 +316,7 @@
|
||||
<script src="./js/components/shipments.js" defer></script>
|
||||
<script src="./js/components/shipment-query.js" defer></script>
|
||||
<script src="./js/components/shipment-summary.js" defer></script>
|
||||
<script src="./js/components/shipment-audit.js" defer></script>
|
||||
<script src="./js/components/defects.js" defer></script>
|
||||
<script src="./js/components/devices.js" defer></script>
|
||||
<script src="./js/components/environment.js" defer></script>
|
||||
@ -324,7 +326,7 @@
|
||||
<script src="./js/components/work-order.js" defer></script>
|
||||
<script src="./js/components/bom.js" defer></script>
|
||||
<script src="./js/components/initial-stock.js" defer></script>
|
||||
<script src="./js/components/purchase-demand.js" defer></script>
|
||||
<script src="./js/components/purchase-demand.js?v=20260312" defer></script>
|
||||
<script src="./js/components/customer-order.js" defer></script>
|
||||
<script src="./js/components/reconciliation.js" defer></script>
|
||||
<script src="./js/components/export.js" defer></script>
|
||||
|
||||
@ -107,53 +107,11 @@ const API = (() => {
|
||||
xhr.send(formData);
|
||||
});
|
||||
}
|
||||
// 存储当前的toast定时器
|
||||
let toastTimer = null;
|
||||
|
||||
function toast(msg, type = 'info', duration = 2000) {
|
||||
function toast(msg) {
|
||||
const t = document.getElementById('toast');
|
||||
|
||||
// 清除之前的定时器
|
||||
if (toastTimer) {
|
||||
clearTimeout(toastTimer);
|
||||
toastTimer = null;
|
||||
}
|
||||
|
||||
t.textContent = msg;
|
||||
|
||||
// 添加类型样式
|
||||
if (type === 'success') {
|
||||
t.style.backgroundColor = '#10b981';
|
||||
t.style.color = 'white';
|
||||
} else if (type === 'error') {
|
||||
t.style.backgroundColor = '#ef4444';
|
||||
t.style.color = 'white';
|
||||
} else if (type === 'warning') {
|
||||
t.style.backgroundColor = '#f59e0b';
|
||||
t.style.color = 'white';
|
||||
} else {
|
||||
t.style.backgroundColor = '#3b82f6';
|
||||
t.style.color = 'white';
|
||||
}
|
||||
|
||||
// 支持多行文本
|
||||
t.style.whiteSpace = 'pre-line';
|
||||
|
||||
// 添加show类来显示toast
|
||||
t.classList.add('show');
|
||||
|
||||
// 调试信息
|
||||
console.log(`Toast显示: 类型=${type}, 持续时间=${duration}ms, 消息长度=${msg.length}`);
|
||||
|
||||
toastTimer = setTimeout(() => {
|
||||
t.classList.remove('show');
|
||||
// 重置样式
|
||||
t.style.backgroundColor = '';
|
||||
t.style.color = '';
|
||||
t.style.whiteSpace = '';
|
||||
toastTimer = null;
|
||||
console.log('Toast已隐藏');
|
||||
}, duration);
|
||||
setTimeout(() => t.classList.remove('show'), 2000);
|
||||
}
|
||||
return {
|
||||
// 通用HTTP方法
|
||||
|
||||
@ -227,10 +227,36 @@
|
||||
}
|
||||
|
||||
let materialRowIndex = 0;
|
||||
let productList = []; // 缓存BOM产品列表
|
||||
|
||||
async function loadProductList() {
|
||||
try {
|
||||
const res = await fetch('/api/bom/products');
|
||||
const data = await res.json();
|
||||
productList = (data.list || []).map(p => p.product_name).filter(Boolean);
|
||||
// 同时从已有订单中提取物料名称(去重)
|
||||
const orderRes = await fetch('/api/customer-orders');
|
||||
const orderData = await orderRes.json();
|
||||
if (orderData.list) {
|
||||
for (const o of orderData.list) {
|
||||
if (o.material && !productList.includes(o.material)) {
|
||||
productList.push(o.material);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载产品列表失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function buildMaterialOptions() {
|
||||
return productList.map(name => `<option value="${name}">`).join('');
|
||||
}
|
||||
|
||||
function addMaterialRow(material = '', quantity = '', unitPrice = '') {
|
||||
const container = document.getElementById('materials-container');
|
||||
const index = materialRowIndex++;
|
||||
const datalistId = `material-options-${index}`;
|
||||
|
||||
const row = document.createElement('div');
|
||||
row.className = 'material-row';
|
||||
@ -240,7 +266,8 @@
|
||||
row.innerHTML = `
|
||||
<div class="field" style="margin: 0;">
|
||||
<label>物料名称 <span style="color: var(--danger);">*</span></label>
|
||||
<input type="text" class="input material-name" placeholder="如:ETAP05 基站-5.0" value="${material}" required />
|
||||
<input type="text" class="input material-name" list="${datalistId}" placeholder="输入或选择物料名称" value="${material}" required autocomplete="off" />
|
||||
<datalist id="${datalistId}">${buildMaterialOptions()}</datalist>
|
||||
</div>
|
||||
<div class="field" style="margin: 0;">
|
||||
<label>数量 <span style="color: var(--danger);">*</span></label>
|
||||
@ -275,7 +302,10 @@
|
||||
|
||||
let currentEditId = null;
|
||||
|
||||
function openModal(order = null) {
|
||||
async function openModal(order = null) {
|
||||
// 加载产品列表用于下拉选择
|
||||
await loadProductList();
|
||||
|
||||
const modal = document.getElementById('order-modal');
|
||||
const title = document.getElementById('modal-title');
|
||||
|
||||
@ -371,6 +401,46 @@
|
||||
});
|
||||
}
|
||||
|
||||
// 保存前确认金额
|
||||
let totalAmount = 0;
|
||||
const detailLines = materials.map(m => {
|
||||
const amount = m.quantity * m.unit_price;
|
||||
totalAmount += amount;
|
||||
return `<tr><td style="padding:6px 12px;">${m.material}</td><td style="padding:6px 12px;text-align:right;">${m.quantity}</td><td style="padding:6px 12px;text-align:right;">${m.unit_price}</td><td style="padding:6px 12px;text-align:right;font-weight:600;">${amount.toFixed(2)}</td></tr>`;
|
||||
}).join('');
|
||||
|
||||
const confirmed = await new Promise(resolve => {
|
||||
const overlay = document.createElement('div');
|
||||
overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:10000;';
|
||||
overlay.innerHTML = `
|
||||
<div style="background:var(--surface,#fff);border-radius:12px;padding:24px;max-width:600px;width:90%;box-shadow:0 8px 32px rgba(0,0,0,0.2);">
|
||||
<h3 style="margin:0 0 16px 0;font-size:18px;">确认订单金额</h3>
|
||||
<table style="width:100%;border-collapse:collapse;margin-bottom:16px;">
|
||||
<thead><tr style="background:var(--border,#eee);">
|
||||
<th style="padding:8px 12px;text-align:left;">物料</th>
|
||||
<th style="padding:8px 12px;text-align:right;">数量</th>
|
||||
<th style="padding:8px 12px;text-align:right;">单价</th>
|
||||
<th style="padding:8px 12px;text-align:right;">金额</th>
|
||||
</tr></thead>
|
||||
<tbody>${detailLines}</tbody>
|
||||
<tfoot><tr style="border-top:2px solid var(--border,#ccc);">
|
||||
<td colspan="3" style="padding:8px 12px;font-weight:700;font-size:16px;">合计</td>
|
||||
<td style="padding:8px 12px;text-align:right;font-weight:700;font-size:16px;color:var(--primary,#1989FA);">${totalAmount.toFixed(2)}</td>
|
||||
</tr></tfoot>
|
||||
</table>
|
||||
<div style="display:flex;justify-content:flex-end;gap:12px;">
|
||||
<button id="confirm-cancel" class="btn btn-secondary" style="padding:8px 24px;">取消</button>
|
||||
<button id="confirm-ok" class="btn btn-primary" style="padding:8px 24px;">确认保存</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(overlay);
|
||||
overlay.querySelector('#confirm-ok').onclick = () => { document.body.removeChild(overlay); resolve(true); };
|
||||
overlay.querySelector('#confirm-cancel').onclick = () => { document.body.removeChild(overlay); resolve(false); };
|
||||
});
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
if (currentEditId) {
|
||||
// 编辑模式:只能编辑单条记录
|
||||
|
||||
@ -7,28 +7,142 @@
|
||||
|
||||
Router.register('/plan-mgmt/initial-stock', async () => {
|
||||
const html = `
|
||||
<div class="page-header">
|
||||
<h1>期初库存管理</h1>
|
||||
<div class="page-actions">
|
||||
<button id="add-stock-btn" class="btn btn-primary">新增库存</button>
|
||||
<button id="import-stock-btn" class="btn btn-secondary">📥 导入Excel</button>
|
||||
<button id="download-template-btn" class="btn btn-secondary">📄 下载模板</button>
|
||||
<button id="batch-delete-btn" class="btn btn-danger">批量删除</button>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
#initial-stock-page {
|
||||
padding: 20px;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
#initial-stock-page .page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#initial-stock-page h1 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
#initial-stock-page .content-area {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
#initial-stock-page .filter-section {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
#initial-stock-page .table-wrapper {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
#initial-stock-page table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#initial-stock-page thead {
|
||||
background: var(--border);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
#initial-stock-page th {
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
border-bottom: 2px solid var(--border);
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#initial-stock-page td {
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#initial-stock-page tbody tr:hover {
|
||||
background: var(--hover);
|
||||
}
|
||||
|
||||
#initial-stock-page .text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#initial-stock-page .custom-checkbox {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#initial-stock-page .custom-checkbox input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#initial-stock-page .checkmark {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 3px;
|
||||
background: white;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#initial-stock-page .custom-checkbox input:checked ~ .checkmark {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
#initial-stock-page .custom-checkbox input:checked ~ .checkmark:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
top: 1px;
|
||||
width: 4px;
|
||||
height: 8px;
|
||||
border: solid white;
|
||||
border-width: 0 2px 2px 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="filter-section" style="margin-bottom: 16px;">
|
||||
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
|
||||
<input type="text" id="search-keyword" class="input" placeholder="搜索物料编码/名称..." style="width: 300px;" />
|
||||
<button class="btn btn-primary" onclick="InitialStock.search()">搜索</button>
|
||||
<button class="btn btn-secondary" onclick="InitialStock.resetSearch()">重置</button>
|
||||
</div>
|
||||
<div id="initial-stock-page">
|
||||
<div class="page-header">
|
||||
<h1 style="margin: 0; font-size: 24px;">期初库存管理</h1>
|
||||
<div class="page-actions" style="margin-top: 16px;">
|
||||
<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="batch-delete-btn" class="btn btn-danger">批量删除</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-area">
|
||||
<div class="filter-section" style="padding: 20px; display: flex; gap: 15px; align-items: center; justify-content: flex-start;">
|
||||
<input type="text" id="search-keyword" class="input" placeholder="搜索物料编码、名称或供应商" style="flex: 1; max-width: 400px;" />
|
||||
<button class="btn btn-secondary" onclick="InitialStock.search()">搜索</button>
|
||||
<button class="btn btn-secondary" onclick="InitialStock.resetSearch()">重置</button>
|
||||
</div>
|
||||
|
||||
<div class="table-container" style="overflow-x: auto;">
|
||||
<table class="data-table">
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 40px;"><label class="custom-checkbox"><input type="checkbox" id="select-all" onchange="InitialStock.toggleSelectAll(this)" /><span class="checkmark"></span></label></th>
|
||||
@ -49,7 +163,7 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="pagination" id="pagination" style="margin-top: 16px; display: flex; justify-content: center; gap: 8px;"></div>
|
||||
<div class="pagination" id="pagination" style="padding: 20px; display: flex; justify-content: center; gap: 8px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -187,7 +301,8 @@
|
||||
if (keyword) {
|
||||
filtered = stockList.filter(item =>
|
||||
(item.material_code || '').toLowerCase().includes(keyword) ||
|
||||
(item.material_name || '').toLowerCase().includes(keyword)
|
||||
(item.material_name || '').toLowerCase().includes(keyword) ||
|
||||
(item.supplier || '').toLowerCase().includes(keyword)
|
||||
);
|
||||
}
|
||||
|
||||
@ -200,7 +315,7 @@
|
||||
} else {
|
||||
tbody.innerHTML = pageData.map(item => `
|
||||
<tr>
|
||||
<td><label class="custom-checkbox"><input type="checkbox" class="row-checkbox" data-id="${item.id}" /><span class="checkmark"></span></label></td>
|
||||
<td><label class="custom-checkbox"><input type="checkbox" class="row-checkbox" data-id="${item.id}" onchange="InitialStock.updateSelectAll()" /><span class="checkmark"></span></label></td>
|
||||
<td>${escapeHtml(item.material_code || '')}</td>
|
||||
<td>${escapeHtml(item.material_name || '')}</td>
|
||||
<td style="font-weight: 600; color: var(--primary);">${item.stock_qty || 0}</td>
|
||||
@ -320,6 +435,12 @@
|
||||
function toggleSelectAll(checkbox) {
|
||||
document.querySelectorAll('.row-checkbox').forEach(cb => cb.checked = checkbox.checked);
|
||||
}
|
||||
|
||||
function updateSelectAll() {
|
||||
const selectAllCheckbox = document.getElementById('select-all');
|
||||
const rowCheckboxes = document.querySelectorAll('.row-checkbox');
|
||||
selectAllCheckbox.checked = rowCheckboxes.length > 0 && Array.from(rowCheckboxes).every(cb => cb.checked);
|
||||
}
|
||||
|
||||
function search() {
|
||||
currentPage = 1;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -108,8 +108,6 @@
|
||||
<button id="batch-delete-btn" class="btn btn-danger" style="margin-right: 10px; display: none;">批量删除</button>
|
||||
<button id="export-reconciliation-btn" class="btn btn-secondary" style="margin-right: 10px;">导出对账单</button>
|
||||
<button id="upload-shipment-btn" class="btn btn-secondary" style="margin-right: 10px;">上传发货单</button>
|
||||
<button id="refresh-prices-btn" class="btn btn-info" style="margin-right: 10px;" title="从客户订单同步最新单价">刷新单价</button>
|
||||
<button id="fix-total-amount-btn" class="btn btn-warning" style="margin-right: 10px;" title="修复所有含税金额计算错误的记录">修复含税金额</button>
|
||||
<button id="add-reconciliation-btn" class="btn btn-primary">新增对账单</button>
|
||||
<input type="file" id="shipment-file-input" accept=".xls,.xlsx,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" style="display: none;" />
|
||||
</div>
|
||||
@ -218,10 +216,7 @@
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>含税单价 <span style="color: var(--danger);">*</span></label>
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<input type="number" id="unit-price" class="input" placeholder="单价" step="0.1" min="0" required />
|
||||
<button type="button" id="fetch-price-btn" class="btn btn-secondary" style="white-space: nowrap;" title="根据合同号和物料名称从客户订单获取单价">获取单价</button>
|
||||
</div>
|
||||
<input type="number" id="unit-price" class="input" placeholder="单价" step="0.1" min="0" required />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>含税金额</label>
|
||||
@ -329,13 +324,13 @@
|
||||
let message = data.message;
|
||||
if (data.errors && data.errors.length > 0) {
|
||||
message += '\n\n错误详情:\n' + data.errors.join('\n');
|
||||
API.toast(message, 'warning', 10000);
|
||||
API.toast(message, 'warning');
|
||||
} else {
|
||||
API.toast(message, 'success', 5000);
|
||||
API.toast(message, 'success');
|
||||
}
|
||||
await loadReconciliations();
|
||||
} else {
|
||||
API.toast(data.error || '上传失败', 'error', 5000);
|
||||
API.toast(data.error || '上传失败', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('上传发货单失败:', err);
|
||||
@ -346,69 +341,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
// 刷新单价按钮
|
||||
const refreshPricesBtn = document.getElementById('refresh-prices-btn');
|
||||
if (refreshPricesBtn) {
|
||||
refreshPricesBtn.addEventListener('click', async () => {
|
||||
if (!confirm('确定要从客户订单刷新所有对账单的含税单价吗?\n此操作将根据客户订单中的单价更新对账单中的单价。')) {
|
||||
return;
|
||||
}
|
||||
|
||||
API.toast('正在刷新单价...', 'info');
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/reconciliations/refresh-prices', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (res.ok && data.ok) {
|
||||
// 延长显示时间到15秒,支持多行显示
|
||||
API.toast(data.message, 'success', 15000);
|
||||
await loadReconciliations();
|
||||
} else {
|
||||
API.toast(data.error || '刷新单价失败', 'error', 5000);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('刷新单价失败:', err);
|
||||
API.toast('刷新单价失败', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 修复含税金额按钮
|
||||
const fixTotalAmountBtn = document.getElementById('fix-total-amount-btn');
|
||||
if (fixTotalAmountBtn) {
|
||||
fixTotalAmountBtn.addEventListener('click', async () => {
|
||||
if (!confirm('确定要修复所有含税金额计算错误的记录吗?\n此操作将根据数量和单价重新计算含税金额。')) {
|
||||
return;
|
||||
}
|
||||
|
||||
API.toast('正在修复含税金额...', 'info');
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/reconciliations/fix-total-amount', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (res.ok && data.ok) {
|
||||
API.toast(data.message, 'success', 5000);
|
||||
await loadReconciliations();
|
||||
} else {
|
||||
API.toast(data.error || '修复含税金额失败', 'error', 5000);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('修复含税金额失败:', err);
|
||||
API.toast('修复含税金额失败', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const quantityInput = document.getElementById('quantity');
|
||||
const unitPriceInput = document.getElementById('unit-price');
|
||||
const totalAmountInput = document.getElementById('total-amount');
|
||||
@ -416,56 +348,13 @@
|
||||
const calculateTotal = () => {
|
||||
const qty = parseFloat(quantityInput.value) || 0;
|
||||
const price = parseFloat(unitPriceInput.value) || 0;
|
||||
totalAmountInput.value = (qty * price).toFixed(2);
|
||||
const total = qty * price;
|
||||
totalAmountInput.value = total.toFixed(1);
|
||||
};
|
||||
|
||||
if (quantityInput) quantityInput.addEventListener('input', calculateTotal);
|
||||
if (unitPriceInput) unitPriceInput.addEventListener('input', calculateTotal);
|
||||
|
||||
// 获取单价按钮
|
||||
const fetchPriceBtn = document.getElementById('fetch-price-btn');
|
||||
if (fetchPriceBtn) {
|
||||
fetchPriceBtn.addEventListener('click', async () => {
|
||||
const contractNo = document.getElementById('contract-no').value.trim();
|
||||
const materialName = document.getElementById('material-name').value.trim();
|
||||
|
||||
if (!contractNo || !materialName) {
|
||||
API.toast('请先填写合同号和物料名称', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
fetchPriceBtn.disabled = true;
|
||||
fetchPriceBtn.textContent = '获取中...';
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/reconciliations/fetch-price', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
contract_no: contractNo,
|
||||
material_name: materialName
|
||||
})
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (res.ok && data.ok) {
|
||||
document.getElementById('unit-price').value = data.unit_price;
|
||||
calculateTotal();
|
||||
API.toast(data.message, 'success');
|
||||
} else {
|
||||
API.toast(data.error || '获取单价失败', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取单价失败:', err);
|
||||
API.toast('获取单价失败', 'error');
|
||||
} finally {
|
||||
fetchPriceBtn.disabled = false;
|
||||
fetchPriceBtn.textContent = '获取单价';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadReconciliations();
|
||||
}, 100);
|
||||
|
||||
|
||||
1286
server/app.py
1286
server/app.py
File diff suppressed because it is too large
Load Diff
@ -1,116 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
测试对账单单价刷新功能
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 添加项目路径
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
def test_price_refresh():
|
||||
"""测试价格刷新功能"""
|
||||
|
||||
# 连接数据库
|
||||
conn = sqlite3.connect('server/data.db')
|
||||
c = conn.cursor()
|
||||
|
||||
print("=== 测试对账单单价刷新功能 ===\n")
|
||||
|
||||
# 1. 检查数据库结构
|
||||
print("1. 检查数据库表结构...")
|
||||
c.execute("PRAGMA table_info(customer_orders)")
|
||||
customer_orders_columns = c.fetchall()
|
||||
print(f" customer_orders 表字段: {[col[1] for col in customer_orders_columns]}")
|
||||
|
||||
c.execute("PRAGMA table_info(reconciliations)")
|
||||
reconciliations_columns = c.fetchall()
|
||||
print(f" reconciliations 表字段: {[col[1] for col in reconciliations_columns]}")
|
||||
|
||||
# 2. 查看现有数据
|
||||
print("\n2. 查看现有数据...")
|
||||
c.execute("SELECT COUNT(*) FROM customer_orders")
|
||||
customer_orders_count = c.fetchone()[0]
|
||||
print(f" 客户订单记录数: {customer_orders_count}")
|
||||
|
||||
c.execute("SELECT COUNT(*) FROM reconciliations")
|
||||
reconciliations_count = c.fetchone()[0]
|
||||
print(f" 对账单记录数: {reconciliations_count}")
|
||||
|
||||
# 3. 显示部分示例数据
|
||||
print("\n3. 客户订单示例数据:")
|
||||
c.execute("SELECT order_no, material, unit_price FROM customer_orders LIMIT 5")
|
||||
for row in c.fetchall():
|
||||
print(f" 订单号: {row[0]}, 物料: {row[1][:30]}..., 单价: {row[2]}")
|
||||
|
||||
print("\n4. 对账单示例数据:")
|
||||
c.execute("SELECT contract_no, material_name, unit_price FROM reconciliations LIMIT 5")
|
||||
for row in c.fetchall():
|
||||
print(f" 合同号: {row[0]}, 物料: {row[1][:30]}..., 单价: {row[2]}")
|
||||
|
||||
# 4. 测试价格匹配逻辑
|
||||
print("\n5. 测试价格匹配逻辑...")
|
||||
|
||||
# 获取所有客户订单
|
||||
c.execute('SELECT order_no, material, unit_price FROM customer_orders')
|
||||
customer_orders = c.fetchall()
|
||||
|
||||
# 构建订单单价字典
|
||||
order_prices = {}
|
||||
for order in customer_orders:
|
||||
order_no = order[0]
|
||||
if order_no not in order_prices:
|
||||
order_prices[order_no] = {}
|
||||
# 处理物料名称(支持换行符分割的多个物料)
|
||||
materials = str(order[1]).split('\n')
|
||||
for material in materials:
|
||||
material = material.strip()
|
||||
if material:
|
||||
order_prices[order_no][material] = order[2]
|
||||
|
||||
# 测试对账单匹配
|
||||
c.execute('SELECT id, contract_no, material_name, unit_price FROM reconciliations LIMIT 10')
|
||||
test_reconciliations = c.fetchall()
|
||||
|
||||
matched_count = 0
|
||||
for recon in test_reconciliations:
|
||||
recon_id, contract_no, material_name, current_price = recon
|
||||
new_price = None
|
||||
|
||||
# 精确匹配合同号
|
||||
if contract_no in order_prices:
|
||||
# 尝试精确匹配物料名
|
||||
if material_name in order_prices[contract_no]:
|
||||
new_price = order_prices[contract_no][material_name]
|
||||
else:
|
||||
# 尝试部分匹配
|
||||
for order_material, price in order_prices[contract_no].items():
|
||||
if material_name in order_material or order_material in material_name:
|
||||
new_price = price
|
||||
break
|
||||
|
||||
if new_price is not None:
|
||||
print(f" ✓ 匹配成功: 对账单ID={recon_id}, 合同号={contract_no}, 当前单价={current_price}, 匹配单价={new_price}")
|
||||
matched_count += 1
|
||||
else:
|
||||
print(f" ✗ 未匹配: 对账单ID={recon_id}, 合同号={contract_no}, 物料={material_name[:30]}...")
|
||||
|
||||
print(f"\n 测试结果: {matched_count}/{len(test_reconciliations)} 条记录成功匹配")
|
||||
|
||||
conn.close()
|
||||
|
||||
print("\n=== 测试完成 ===")
|
||||
print("\n功能说明:")
|
||||
print("1. 已添加 /api/reconciliations/refresh-prices 接口,用于批量刷新所有对账单单价")
|
||||
print("2. 已添加 /api/reconciliations/fetch-price 接口,用于根据合同号和物料名获取单个单价")
|
||||
print("3. 前端已添加'刷新单价'按钮,支持手动批量刷新")
|
||||
print("4. 新增/编辑对账单时,可通过'获取单价'按钮从客户订单获取最新单价")
|
||||
print("\n使用方法:")
|
||||
print("- 批量刷新: 在对账单管理页面点击'刷新单价'按钮")
|
||||
print("- 单个获取: 在新增/编辑对账单时,填写合同号和物料名称后点击'获取单价'")
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_price_refresh()
|
||||
Loading…
Reference in New Issue
Block a user