新增采购需求清单和期初库存功能

This commit is contained in:
zzh 2026-03-12 13:22:48 +08:00
parent 0db85e75a0
commit 50f8a07b1b
10 changed files with 2356 additions and 853 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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) {
// 编辑模式:只能编辑单条记录

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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