@@ -495,6 +506,7 @@
+
diff --git a/frontend/js/components/dashboard.js b/frontend/js/components/dashboard.js
index 81102ad..a79b24a 100755
--- a/frontend/js/components/dashboard.js
+++ b/frontend/js/components/dashboard.js
@@ -470,23 +470,57 @@ const Dashboard = (() => {
});
async function render() {
- const [dRes,pRes,yRes,tRes,mRes] = await Promise.allSettled([
- API.dashboard(),
- API.auditPddQuiet(),
- API.auditYtQuiet(),
- API.auditTxQuiet(),
- API.auditMtQuiet()
- ]);
- const data = dRes.status==='fulfilled' ? dRes.value : { goodRate: '—', shipments: '—', defects: '—', badCount: '—' };
- const pdd = pRes.status==='fulfilled' ? pRes.value : { list: [] };
- const yt = yRes.status==='fulfilled' ? yRes.value : { list: [] };
- const tx = tRes.status==='fulfilled' ? tRes.value : { list: [] };
- const mt = mRes.status==='fulfilled' ? mRes.value : { list: [] };
+ // 先返回全屏loading遮罩层,阻止点击菜单栏
+ const loadingHtml = `
+
+
+ `;
+
+ // 异步加载数据并渲染
+ const loadData = async () => {
+ const [dRes,pRes,yRes,tRes,mRes] = await Promise.allSettled([
+ API.dashboard(),
+ API.auditPddQuiet(),
+ API.auditYtQuiet(),
+ API.auditTxQuiet(),
+ API.auditMtQuiet()
+ ]);
+ const data = dRes.status==='fulfilled' ? dRes.value : { goodRate: '—', shipments: '—', defects: '—', badCount: '—' };
+ const pdd = pRes.status==='fulfilled' ? pRes.value : { list: [] };
+ const yt = yRes.status==='fulfilled' ? yRes.value : { list: [] };
+ const tx = tRes.status==='fulfilled' ? tRes.value : { list: [] };
+ const mt = mRes.status==='fulfilled' ? mRes.value : { list: [] };
+
+ renderDashboardContent(data, pdd, yt, tx, mt);
+ };
+
+ // 启动数据加载
+ setTimeout(loadData, 0);
+
+ return loadingHtml;
+ }
+
+ // 实际的仪表盘内容渲染函数
+ function renderDashboardContent(data, pdd, yt, tx, mt) {
+ const view = document.getElementById('view');
+ if(!view) return;
+
+ // 先移除loading遮罩层
+ const loadingEl = document.getElementById('dashboard-loading');
+ if(loadingEl) loadingEl.remove();
// 优化:默认只显示少量数据,完整数据在点击模态框时加载
const pddList = (pdd.list||[]).slice(0, 10).map(r=>`
${r.ts_cn||'—'}${r.batch||''}${r.mac||''}${r.note||''}`).join('')||'暂无数据';
const ytList = (yt.list||[]).slice(0, 10).map(r=>`
${r.ts_cn||'—'}${r.batch||''}${r.mac||''}${r.note||''}`).join('')||'暂无数据';
- setTimeout(()=>{
+
+ // 构建HTML内容
+ let html = '';
+
+ // 清理旧的定时器和事件
+ window.__dashboardInit = () => {
// 清理旧的定时器和事件
if(window.__auditTimer){
clearInterval(window.__auditTimer);
@@ -1225,7 +1259,7 @@ const Dashboard = (() => {
};
};
- const drawShipmentDonutChart = (stats) => {
+ const drawShipmentDonutChart = async (stats) => {
const canvas = document.getElementById('shipment-donut-chart');
if(!canvas) return;
const ctx = canvas.getContext('2d');
@@ -1243,6 +1277,20 @@ const Dashboard = (() => {
const items = Object.entries(byPlatform).filter(([, v]) => (v || 0) > 0);
items.sort((a, b) => (b[1] || 0) - (a[1] || 0));
+ // 获取机种列表以映射自定义机种名称
+ let platformList = [];
+ try {
+ const platformRes = await fetch('/api/shipments/platforms', {
+ headers: { 'Content-Type': 'application/json' },
+ credentials: 'include'
+ }).then(r => r.ok ? r.json() : null);
+ if(platformRes && platformRes.list) {
+ platformList = platformRes.list;
+ }
+ } catch(e) {
+ // 忽略错误,使用默认映射
+ }
+
const platformName = (k) => {
const map = {
pdd: '拼多多',
@@ -1253,7 +1301,13 @@ const Dashboard = (() => {
std: '标准版',
unknown: '未知'
};
- return map[k] || k;
+ // 先从固定映射查找
+ if(map[k]) return map[k];
+ // 再从机种列表查找自定义机种名称
+ const customPlatform = platformList.find(p => p.value === k);
+ if(customPlatform) return customPlatform.label;
+ // 最后返回原始值
+ return k;
};
const platformColor = (k, fallbackIndex) => {
@@ -1358,7 +1412,7 @@ const Dashboard = (() => {
window.__shipmentStatsTs = window.__shipmentStatsTs || 0;
const now = Date.now();
if(!force && (now - window.__shipmentStatsTs) < 60000 && window.__shipmentStatsCache) {
- drawShipmentDonutChart(window.__shipmentStatsCache);
+ await drawShipmentDonutChart(window.__shipmentStatsCache);
return;
}
try {
@@ -1369,7 +1423,7 @@ const Dashboard = (() => {
if(res && res.ok) {
window.__shipmentStatsCache = res;
window.__shipmentStatsTs = now;
- drawShipmentDonutChart(res);
+ await drawShipmentDonutChart(res);
}
} catch(e) {
}
@@ -2087,8 +2141,10 @@ const Dashboard = (() => {
// 刷新间隔10秒
window.__auditTimer=setInterval(refreshAll, 10000);
- },0);
- return `
+ };
+
+ // 构建HTML内容
+ html = `
@@ -2371,6 +2427,16 @@ const Dashboard = (() => {
`;
+
+ // 更新DOM内容,替换loading动画
+ if(view) {
+ view.innerHTML = html;
+ }
+
+ // 触发setTimeout中的初始化逻辑(确保DOM已更新)
+ setTimeout(() => {
+ if(window.__dashboardInit) window.__dashboardInit();
+ }, 0);
}
// 跳转到良不良统计页面
diff --git a/frontend/js/components/finance.js b/frontend/js/components/finance.js
new file mode 100644
index 0000000..fc84a93
--- /dev/null
+++ b/frontend/js/components/finance.js
@@ -0,0 +1,1284 @@
+/**
+ * 财务管理模块组件
+ * 包含:收支管理、客户对账、发票管理、供应商对账
+ */
+window.Finance = (() => {
+
+ // 当前子模块
+ let currentSubModule = 'income-expense';
+
+ // 渲染主框架
+ const render = () => {
+ return `
+
+
+
+
+
+
+ ${renderSubModule(currentSubModule)}
+
+
+
+
+ `;
+ };
+
+ // 渲染子模块内容
+ const renderSubModule = (module) => {
+ switch(module) {
+ case 'income-expense':
+ return renderIncomeExpense();
+ case 'customer-rec':
+ return renderCustomerReconciliation();
+ case 'supplier-rec':
+ return renderSupplierReconciliation();
+ case 'invoice':
+ return renderInvoice();
+ case 'stats':
+ return renderStats();
+ default:
+ return renderIncomeExpense();
+ }
+ };
+
+ // 收支管理
+ const renderIncomeExpense = () => {
+ return `
+
+ `;
+ };
+
+ // 客户对账
+ const renderCustomerReconciliation = () => {
+ return `
+
+
+
+
+
+
+
+
+
+
+
+
+
显示全部历史数据
+
+
+
+
+ | 客户 / 合同号 |
+ 订单数 |
+ 物料明细 |
+ 累计应收 |
+ 累计已收 |
+ 未收余额 |
+ 回款率 |
+ 最后交易 |
+
+
+
+ | 暂无数据 |
+
+
+
+ `;
+ };
+
+ // 供应商对账
+ const renderSupplierReconciliation = () => {
+ return `
+
+ `;
+ };
+
+ // 发票管理
+ const renderInvoice = () => {
+ return `
+
+ `;
+ };
+
+ // 统计报表
+ const renderStats = () => {
+ return `
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ };
+
+ // 初始化模块
+ const init = () => {
+ // 激活当前模块对应的标签
+ document.querySelectorAll('.finance-tabs .tab-item').forEach(t => t.classList.remove('active'));
+ const activeTab = document.querySelector(`[data-tab="${currentSubModule}"]`);
+ if(activeTab) activeTab.classList.add('active');
+
+ // 渲染当前模块内容
+ const contentEl = document.getElementById('finance-content');
+ if(contentEl) contentEl.innerHTML = renderSubModule(currentSubModule);
+
+ // 绑定标签页切换
+ document.querySelectorAll('.finance-tabs .tab-item').forEach(tab => {
+ tab.addEventListener('click', () => {
+ currentSubModule = tab.dataset.tab;
+ document.querySelectorAll('.tab-item').forEach(t => t.classList.remove('active'));
+ tab.classList.add('active');
+ document.getElementById('finance-content').innerHTML = renderSubModule(currentSubModule);
+
+ // 加载对应数据
+ switch(currentSubModule) {
+ case 'income-expense':
+ setTimeout(() => {
+ loadTransactionStats();
+ loadTransactions();
+ }, 0);
+ break;
+ case 'customer-rec':
+ setTimeout(() => loadCustomerRec(), 0);
+ break;
+ case 'supplier-rec':
+ setTimeout(() => loadSupplierRec(), 0);
+ break;
+ case 'invoice':
+ setTimeout(() => {
+ loadInvoiceStats();
+ loadInvoices();
+ }, 0);
+ break;
+ case 'stats':
+ setTimeout(() => loadStats(), 0);
+ break;
+ }
+ });
+ });
+
+ // 加载初始数据(只在收支管理标签页加载)
+ if(currentSubModule === 'income-expense') {
+ setTimeout(() => {
+ loadTransactionStats();
+ loadTransactions();
+ }, 0);
+ }
+ };
+
+ // ========== 数据加载函数 ==========
+
+ // 加载收支统计
+ const loadTransactionStats = async () => {
+ try {
+ const res = await fetch('/api/finance/transactions/stats', {
+ credentials: 'include'
+ }).then(r => r.json());
+
+ if(res.ok) {
+ document.getElementById('month-income').textContent =
+ '¥' + (res.stats.month_income || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2});
+ document.getElementById('month-expense').textContent =
+ '¥' + (res.stats.month_expense || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2});
+ document.getElementById('month-balance').textContent =
+ '¥' + (res.stats.month_net || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2});
+ }
+ } catch(e) {
+ console.error('加载统计失败:', e);
+ }
+ };
+
+ // 加载收支流水
+ const loadTransactions = async (page = 1) => {
+ try {
+ const params = new URLSearchParams();
+ const type = document.getElementById('trans-type')?.value;
+ const category = document.getElementById('trans-category')?.value;
+ const startDate = document.getElementById('start-date')?.value;
+ const endDate = document.getElementById('end-date')?.value;
+ const party = document.getElementById('related-party')?.value;
+
+ if(type) params.append('type', type);
+ if(category) params.append('category', category);
+ if(startDate) params.append('start_date', startDate);
+ if(endDate) params.append('end_date', endDate);
+ if(party) params.append('related_party', party);
+ params.append('page', page);
+
+ const res = await fetch('/api/finance/transactions?' + params, {
+ credentials: 'include'
+ }).then(r => r.json());
+
+ if(res.ok) {
+ const tbody = document.getElementById('transactions-tbody');
+ if(res.data.length === 0) {
+ tbody.innerHTML = '| 暂无数据 |
';
+ } else {
+ tbody.innerHTML = res.data.map(row => `
+
+ | ${row.date} |
+ ${row.type === 'income' ? '收入' : '支出'} |
+ ${row.category || '-'} |
+ ¥${(row.amount || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2})} |
+ ${row.related_party || '-'} |
+ ${row.order_no || '-'} |
+ ${row.payment_method || '-'} |
+ ${row.status === 'confirmed' ? '已确认' : '待确认'} |
+ ${row.description || '-'} |
+
+ `).join('');
+ }
+ }
+ } catch(e) {
+ console.error('加载流水失败:', e);
+ }
+ };
+
+ // 整合显示状态
+ let isMergeMode = false;
+
+ // 切换整合/分开显示
+ const toggleMerge = () => {
+ isMergeMode = !isMergeMode;
+ const btn = document.getElementById('merge-btn');
+ const th = document.getElementById('rec-th-customer');
+ if(btn) {
+ if(isMergeMode) {
+ btn.textContent = '分开显示';
+ btn.style.background = '#6B7280';
+ if(th) th.textContent = '客户名称';
+ } else {
+ btn.textContent = '整合显示';
+ btn.style.background = '#3B82F6';
+ if(th) th.textContent = '客户 / 合同号';
+ }
+ }
+ loadCustomerRec();
+ };
+
+ // 加载客户对账
+ const loadCustomerRec = async () => {
+ try {
+ const params = new URLSearchParams();
+ const customer = document.getElementById('customer-name')?.value;
+ const period = document.getElementById('rec-period')?.value;
+
+ console.log('DEBUG: period value =', period);
+
+ if(customer) params.append('customer', customer);
+ if(period) {
+ params.append('period', period);
+ console.log('DEBUG: adding period param =', period);
+ }
+ if(isMergeMode) params.append('merge', 'true');
+
+ console.log('DEBUG: final URL =', '/api/finance/customer-reconciliation?' + params.toString());
+
+ const res = await fetch('/api/finance/customer-reconciliation?' + params, {
+ credentials: 'include'
+ }).then(r => r.json());
+
+ console.log('DEBUG: response period_label =', res.period_label);
+
+ if(res.ok) {
+ // 更新周期标签
+ const periodLabel = document.getElementById('rec-period-label');
+ if(periodLabel && res.period_label) {
+ const modeText = isMergeMode ? '(整合显示)' : '(分开显示)';
+ periodLabel.textContent = (period ? `数据周期:${res.period_label}` : '显示全部历史数据') + modeText;
+ }
+
+ // 更新统计卡片
+ const totalReceivable = res.data.reduce((sum, r) => sum + (r.total_income || 0), 0);
+ const totalPaid = res.data.reduce((sum, r) => sum + (r.total_paid || 0), 0);
+ const totalBalance = res.data.reduce((sum, r) => sum + (r.closing_balance || 0), 0);
+
+ // 本期客户数:按客户名称去重统计(同一客户多个合同号只算1个)
+ const uniqueCustomers = new Set(res.data.map(r => r.customer_name)).size;
+
+ document.getElementById('rec-customer-count').textContent = uniqueCustomers;
+ document.getElementById('rec-total-receivable').textContent =
+ '¥' + totalReceivable.toLocaleString('zh-CN', {minimumFractionDigits: 2});
+ document.getElementById('rec-total-paid').textContent =
+ '¥' + totalPaid.toLocaleString('zh-CN', {minimumFractionDigits: 2});
+ document.getElementById('rec-total-balance').textContent =
+ '¥' + totalBalance.toLocaleString('zh-CN', {minimumFractionDigits: 2});
+
+ const tbody = document.getElementById('customer-rec-tbody');
+ if(res.data.length === 0) {
+ tbody.innerHTML = '| 暂无数据 |
';
+ } else {
+ tbody.innerHTML = res.data.map(row => {
+ // 计算回款率
+ const payRate = row.total_income > 0
+ ? ((row.total_paid / row.total_income) * 100).toFixed(1) + '%'
+ : '0%';
+
+ // 物料明细截断显示
+ const materials = row.materials
+ ? (row.materials.length > 30 ? row.materials.substring(0, 30) + '...' : row.materials)
+ : '-';
+
+ // 最后交易日期
+ const lastDate = row.last_order_date || '-';
+
+ // 显示名称:整合模式只显示客户名,分开模式显示客户名+合同号+编辑图标
+ let displayName;
+ const editIcon = ``;
+ if(isMergeMode) {
+ displayName = `${row.customer_name}${editIcon}`;
+ } else {
+ // 分开显示模式:同时显示客户名称和合同号,可点击编辑
+ if(row.customer_name && row.customer_name !== row.contract_no) {
+ displayName = `${row.customer_name}${editIcon}
${row.contract_no}`;
+ } else {
+ displayName = `${row.contract_no}${editIcon}`;
+ }
+ }
+
+ return `
+
+ | ${displayName} |
+ ${row.order_count || 0} 单 |
+ ${materials} |
+ ¥${(row.total_income || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2})} |
+ ¥${(row.total_paid || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2})} |
+ ¥${(row.closing_balance || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2})} |
+ ${payRate} |
+ ${lastDate} |
+
+ `}).join('');
+ }
+ }
+ } catch(e) {
+ console.error('加载客户对账失败:', e);
+ }
+ };
+
+ // 加载供应商对账
+ const loadSupplierRec = async () => {
+ try {
+ const params = new URLSearchParams();
+ const supplier = document.getElementById('supplier-name')?.value;
+ const period = document.getElementById('supplier-period')?.value;
+
+ if(supplier) params.append('supplier', supplier);
+ if(period) params.append('period', period);
+
+ const res = await fetch('/api/finance/supplier-reconciliation?' + params, {
+ credentials: 'include'
+ }).then(r => r.json());
+
+ if(res.ok) {
+ const tbody = document.getElementById('supplier-rec-tbody');
+ if(res.data.length === 0) {
+ tbody.innerHTML = '| 暂无数据 |
';
+ } else {
+ tbody.innerHTML = res.data.map(row => `
+
+ | ${row.supplier_name} |
+ ${row.period_start} ~ ${row.period_end} |
+ ¥${(row.opening_balance || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2})} |
+ ¥${(row.total_expense || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2})} |
+ ¥${(row.total_paid || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2})} |
+ ¥${(row.closing_balance || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2})} |
+ ${row.statement_status === 'confirmed' ? '已确认' : '待确认'} |
+ |
+
+ `).join('');
+ }
+ }
+ } catch(e) {
+ console.error('加载供应商对账失败:', e);
+ }
+ };
+
+ // 加载发票统计
+ const loadInvoiceStats = async () => {
+ try {
+ const res = await fetch('/api/finance/invoices/stats', {
+ credentials: 'include'
+ }).then(r => r.json());
+
+ if(res.ok) {
+ document.getElementById('input-invoice-count').textContent =
+ (res.stats.input.count || 0) + ' 张';
+ document.getElementById('input-invoice-amount').textContent =
+ '¥' + (res.stats.input.total || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2});
+ document.getElementById('output-invoice-count').textContent =
+ (res.stats.output.count || 0) + ' 张';
+ document.getElementById('output-invoice-amount').textContent =
+ '¥' + (res.stats.output.total || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2});
+ }
+ } catch(e) {
+ console.error('加载发票统计失败:', e);
+ }
+ };
+
+ // 加载发票列表
+ const loadInvoices = async () => {
+ try {
+ const params = new URLSearchParams();
+ const type = document.getElementById('invoice-type')?.value;
+ const status = document.getElementById('invoice-status')?.value;
+ const start = document.getElementById('invoice-start')?.value;
+ const end = document.getElementById('invoice-end')?.value;
+
+ if(type) params.append('type', type);
+ if(status) params.append('status', status);
+ if(start) params.append('start_date', start);
+ if(end) params.append('end_date', end);
+
+ const res = await fetch('/api/finance/invoices?' + params, {
+ credentials: 'include'
+ }).then(r => r.json());
+
+ if(res.ok) {
+ const tbody = document.getElementById('invoices-tbody');
+ if(res.data.length === 0) {
+ tbody.innerHTML = '| 暂无数据 |
';
+ } else {
+ tbody.innerHTML = res.data.map(row => `
+
+ | ${row.invoice_type === 'input' ? '进项' : '销项'} |
+ ${row.invoice_no} |
+ ${row.issue_date} |
+ ¥${(row.amount || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2})} |
+ ¥${(row.tax_amount || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2})} |
+ ¥${(row.total_amount || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2})} |
+ ${row.related_party || '-'} |
+ ${row.order_no || '-'} |
+ ${row.status === 'normal' ? '正常' : '作废'} |
+
+ `).join('');
+ }
+ }
+ } catch(e) {
+ console.error('加载发票失败:', e);
+ }
+ };
+
+ // 加载统计报表
+ const loadStats = async () => {
+ try {
+ const days = document.getElementById('stats-range')?.value || 30;
+
+ const res = await fetch('/api/finance/business-stats?days=' + days, {
+ credentials: 'include'
+ }).then(r => r.json());
+
+ if(res.ok) {
+ document.getElementById('total-income').textContent =
+ '¥' + (res.stats.summary.total_income || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2});
+ document.getElementById('total-expense').textContent =
+ '¥' + (res.stats.summary.total_expense || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2});
+
+ const net = (res.stats.summary.total_income || 0) - (res.stats.summary.total_expense || 0);
+ document.getElementById('net-profit').textContent =
+ '¥' + net.toLocaleString('zh-CN', {minimumFractionDigits: 2});
+
+ const margin = res.stats.summary.total_income > 0
+ ? (net / res.stats.summary.total_income * 100).toFixed(1)
+ : 0;
+ document.getElementById('profit-margin').textContent = margin + '%';
+
+ // 客户应收
+ const recTbody = document.getElementById('receivables-tbody');
+ if(res.stats.customer_receivables?.length > 0) {
+ recTbody.innerHTML = res.stats.customer_receivables.map(r => `
+ | ${r.related_party} | ¥${(r.balance || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2})} |
+ `).join('');
+ } else {
+ recTbody.innerHTML = '| 暂无数据 |
';
+ }
+
+ // 供应商应付
+ const payTbody = document.getElementById('payables-tbody');
+ if(res.stats.supplier_payables?.length > 0) {
+ payTbody.innerHTML = res.stats.supplier_payables.map(r => `
+ | ${r.related_party} | ¥${(r.balance || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2})} |
+ `).join('');
+ } else {
+ payTbody.innerHTML = '| 暂无数据 |
';
+ }
+ }
+ } catch(e) {
+ console.error('加载统计失败:', e);
+ }
+ };
+
+ // ========== 模态框函数 ==========
+
+ const showAddTransactionModal = () => {
+ const modal = document.createElement('div');
+ modal.className = 'modal-overlay';
+ modal.id = 'transaction-modal';
+ modal.innerHTML = `
+
+
新增收支记录
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ document.body.appendChild(modal);
+ };
+
+ const saveTransaction = async () => {
+ try {
+ const data = {
+ type: document.getElementById('trans-type-input').value,
+ category: document.getElementById('trans-category-input').value,
+ amount: parseFloat(document.getElementById('trans-amount').value) || 0,
+ date: document.getElementById('trans-date').value,
+ related_party: document.getElementById('trans-party').value,
+ related_party_type: 'customer',
+ order_no: document.getElementById('trans-order').value,
+ payment_method: document.getElementById('trans-payment').value,
+ description: document.getElementById('trans-desc').value
+ };
+
+ const res = await fetch('/api/finance/transactions', {
+ method: 'POST',
+ headers: {'Content-Type': 'application/json'},
+ credentials: 'include',
+ body: JSON.stringify(data)
+ }).then(r => r.json());
+
+ if(res.ok) {
+ document.getElementById('transaction-modal').remove();
+ loadTransactions();
+ loadTransactionStats();
+ } else {
+ alert('保存失败: ' + (res.error || '未知错误'));
+ }
+ } catch(e) {
+ console.error('保存失败:', e);
+ alert('保存失败');
+ }
+ };
+
+ const showAddInvoiceModal = () => {
+ const modal = document.createElement('div');
+ modal.className = 'modal-overlay';
+ modal.id = 'invoice-modal';
+ modal.innerHTML = `
+
+
新增发票
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ document.body.appendChild(modal);
+ };
+
+ const saveInvoice = async () => {
+ try {
+ const amount = parseFloat(document.getElementById('inv-amount').value) || 0;
+ const tax = parseFloat(document.getElementById('inv-tax').value) || 0;
+
+ const data = {
+ invoice_type: document.getElementById('inv-type-input').value,
+ invoice_no: document.getElementById('inv-no').value,
+ invoice_code: document.getElementById('inv-code').value,
+ issue_date: document.getElementById('inv-date').value,
+ amount: amount,
+ tax_amount: tax,
+ total_amount: amount + tax,
+ related_party: document.getElementById('inv-party').value,
+ order_no: document.getElementById('inv-order').value,
+ notes: document.getElementById('inv-notes').value
+ };
+
+ const res = await fetch('/api/finance/invoices', {
+ method: 'POST',
+ headers: {'Content-Type': 'application/json'},
+ credentials: 'include',
+ body: JSON.stringify(data)
+ }).then(r => r.json());
+
+ if(res.ok) {
+ document.getElementById('invoice-modal').remove();
+ loadInvoices();
+ loadInvoiceStats();
+ } else {
+ alert('保存失败: ' + (res.error || '未知错误'));
+ }
+ } catch(e) {
+ console.error('保存失败:', e);
+ alert('保存失败');
+ }
+ };
+
+ // 其他占位函数
+ const showGenerateStatementModal = () => alert('生成客户对账单功能开发中');
+ const showGenerateSupplierStatementModal = () => alert('生成供应商对账单功能开发中');
+ const viewStatement = (id) => alert('查看对账单 ' + id);
+ const viewSupplierStatement = (id) => alert('查看供应商对账单 ' + id);
+
+ return {
+ render,
+ init,
+ renderCustomerReconciliation,
+ renderSupplierReconciliation,
+ renderInvoice,
+ renderStats,
+ loadTransactions,
+ loadCustomerRec,
+ loadSupplierRec,
+ loadInvoices,
+ loadStats,
+ loadTransactionStats,
+ loadInvoiceStats,
+ // 重置模块状态
+ resetModule: () => {
+ currentSubModule = 'income-expense';
+ isMergeMode = false;
+ },
+ showAddTransactionModal,
+ saveTransaction,
+ showAddInvoiceModal,
+ saveInvoice,
+ showGenerateStatementModal,
+ showGenerateSupplierStatementModal,
+ viewStatement,
+ viewSupplierStatement,
+ toggleMerge,
+ editCustomerRec: (contractNo, customerName) => {
+ const modal = document.createElement('div');
+ modal.className = 'modal-overlay';
+ modal.id = 'edit-rec-modal';
+ modal.innerHTML = `
+
+
编辑客户信息
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ document.body.appendChild(modal);
+ },
+ saveCustomerRecEdit: async () => {
+ const contractNo = document.getElementById('edit-contract-no').value;
+ const customerName = document.getElementById('edit-customer-name').value;
+
+ if(!customerName.trim()) {
+ alert('请输入客户名称');
+ return;
+ }
+
+ try {
+ // 调用后端API保存
+ const res = await fetch('/api/finance/customer-reconciliation/update', {
+ method: 'POST',
+ headers: {'Content-Type': 'application/json'},
+ credentials: 'include',
+ body: JSON.stringify({
+ order_no: contractNo,
+ customer_name: customerName.trim()
+ })
+ }).then(r => r.json());
+
+ document.getElementById('edit-rec-modal').remove();
+
+ if(res.ok) {
+ // 刷新列表
+ if(window.Finance && window.Finance.loadCustomerRec) {
+ window.Finance.loadCustomerRec();
+ }
+ if(window.API && window.API.toast) {
+ window.API.toast('客户名称已更新');
+ }
+ } else {
+ alert('保存失败:' + (res.error || '未知错误'));
+ }
+ } catch(e) {
+ console.error('保存失败:', e);
+ alert('保存失败,请检查网络连接');
+ }
+ },
+ clearPeriodFilter: () => {
+ document.getElementById('rec-period').value = '';
+ if(window.Finance && window.Finance.loadCustomerRec) {
+ window.Finance.loadCustomerRec();
+ }
+ }
+ };
+})();
+
+// 注册路由
+Router.register('/finance', async () => {
+ // 重置为收支管理标签
+ if(window.Finance && window.Finance.resetModule) {
+ window.Finance.resetModule();
+ }
+ const html = window.Finance.render();
+ setTimeout(() => window.Finance.init(), 0);
+ return html;
+});
+
+Router.register('/finance/customer-rec', async () => {
+ // 设置当前模块为客户对账,避免init加载错误的数据
+ isMergeMode = false;
+ const html = window.Finance.render();
+ setTimeout(() => {
+ window.Finance.init();
+ // 手动切换到客户对账标签并加载数据
+ currentSubModule = 'customer-rec';
+ document.querySelectorAll('.tab-item').forEach(t => t.classList.remove('active'));
+ document.querySelector('[data-tab="customer-rec"]')?.classList.add('active');
+ document.getElementById('finance-content').innerHTML = window.Finance.renderCustomerReconciliation();
+ setTimeout(() => window.Finance.loadCustomerRec(), 0);
+ }, 0);
+ return html;
+});
+
+Router.register('/finance/supplier-rec', async () => {
+ isMergeMode = false;
+ const html = window.Finance.render();
+ setTimeout(() => {
+ window.Finance.init();
+ currentSubModule = 'supplier-rec';
+ document.querySelectorAll('.tab-item').forEach(t => t.classList.remove('active'));
+ document.querySelector('[data-tab="supplier-rec"]')?.classList.add('active');
+ document.getElementById('finance-content').innerHTML = window.Finance.renderSupplierReconciliation();
+ setTimeout(() => window.Finance.loadSupplierRec(), 0);
+ }, 0);
+ return html;
+});
+
+Router.register('/finance/invoice', async () => {
+ isMergeMode = false;
+ const html = window.Finance.render();
+ setTimeout(() => {
+ window.Finance.init();
+ currentSubModule = 'invoice';
+ document.querySelectorAll('.tab-item').forEach(t => t.classList.remove('active'));
+ document.querySelector('[data-tab="invoice"]')?.classList.add('active');
+ document.getElementById('finance-content').innerHTML = window.Finance.renderInvoice();
+ setTimeout(() => {
+ window.Finance.loadInvoiceStats();
+ window.Finance.loadInvoices();
+ }, 0);
+ }, 0);
+ return html;
+});
+
+Router.register('/finance/stats', async () => {
+ isMergeMode = false;
+ const html = window.Finance.render();
+ setTimeout(() => {
+ window.Finance.init();
+ currentSubModule = 'stats';
+ document.querySelectorAll('.tab-item').forEach(t => t.classList.remove('active'));
+ document.querySelector('[data-tab="stats"]')?.classList.add('active');
+ document.getElementById('finance-content').innerHTML = window.Finance.renderStats();
+ setTimeout(() => window.Finance.loadStats(), 0);
+ }, 0);
+ return html;
+});
diff --git a/frontend/js/router.js b/frontend/js/router.js
index 7aae70c..140ec14 100755
--- a/frontend/js/router.js
+++ b/frontend/js/router.js
@@ -92,7 +92,24 @@ const Router = (() => {
'customer-order': '客户订单',
'reconciliation': '对账单',
export: '导出',
- settings: '设置'
+ settings: '设置',
+ finance: '财务管理',
+ 'customer-rec': '客户对账',
+ 'supplier-rec': '供应商对账',
+ invoice: '发票管理',
+ outsoucing: '委外管理',
+ 'outsourcing-mgmt': '委外管理',
+ 'outsourcing-orders': '委外工单',
+ 'material-issue': '委外发料',
+ 'finished-goods-receipt': '成品入库',
+ 'wip-stock': '委外在制库存',
+ collect: '采集',
+ test: '测试',
+ meituan: '美团测试',
+ system: '系统',
+ 'operations-log': '操作日志',
+ 'material-purchase': '物料采购',
+ 'ai-report': 'AI报表'
};
return map[key] || key;
}
diff --git a/frontend/login.html b/frontend/login.html
index fa625e7..68530e9 100755
--- a/frontend/login.html
+++ b/frontend/login.html
@@ -4,7 +4,7 @@
登录 - 韬智生产管理系统
-
+
diff --git a/server/app.py b/server/app.py
index fa38ba1..bf40b2b 100755
--- a/server/app.py
+++ b/server/app.py
@@ -8722,6 +8722,16 @@ except Exception as e:
print(f"⚠️ AI服务集成失败: {e}")
print(" 智能报表功能将不可用,但其他功能正常")
+# 初始化财务管理路由
+try:
+ from finance_routes import finance_bp, init_finance_tables
+ app.register_blueprint(finance_bp)
+ # 初始化财务表(如果不存在)
+ init_finance_tables()
+ print("✅ 财务管理模块已成功集成")
+except Exception as e:
+ print(f"⚠️ 财务管理模块集成失败: {e}")
+
# 添加缓存头
@app.after_request
diff --git a/server/finance_routes.py b/server/finance_routes.py
new file mode 100644
index 0000000..f5ab904
--- /dev/null
+++ b/server/finance_routes.py
@@ -0,0 +1,950 @@
+# -*- coding: utf-8 -*-
+"""
+财务管理模块API路由
+包含:收支管理、客户对账、发票管理、供应商对账
+"""
+import os
+import json
+import sqlite3
+from datetime import datetime, timedelta
+from functools import wraps
+from flask import Blueprint, request, jsonify, session
+
+# 创建蓝图
+finance_bp = Blueprint('finance', __name__, url_prefix='/api/finance')
+
+BASE_DIR = os.path.dirname(os.path.abspath(__file__))
+DB_PATH = os.path.join(BASE_DIR, 'data.db')
+
+def get_db():
+ conn = sqlite3.connect(DB_PATH)
+ conn.row_factory = sqlite3.Row
+ return conn
+
+def require_login(fn):
+ @wraps(fn)
+ def wrapper(*args, **kwargs):
+ if not session.get('user_id'):
+ return jsonify({'error': 'unauthorized'}), 401
+ return fn(*args, **kwargs)
+ return wrapper
+
+def log(action, detail=''):
+ """记录操作日志"""
+ try:
+ conn = get_db()
+ c = conn.cursor()
+ uid = session.get('user_id')
+ username = session.get('username', 'system')
+ ts = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+ c.execute('INSERT INTO operations_log(user_id, action, detail, ts) VALUES (?,?,?,?)',
+ (uid, action, detail, ts))
+ conn.commit()
+ conn.close()
+ except Exception as e:
+ print(f'Log error: {e}')
+
+# ==================== 数据库表初始化 ====================
+def init_finance_tables():
+ """初始化财务管理相关表"""
+ conn = get_db()
+ c = conn.cursor()
+
+ # 收支记录表
+ c.execute('''CREATE TABLE IF NOT EXISTS finance_transactions (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ type TEXT NOT NULL, -- income:收入, expense:支出
+ category TEXT NOT NULL, -- 类别:货款、运费、工资、租金等
+ amount REAL NOT NULL,
+ currency TEXT DEFAULT 'CNY',
+ date TEXT NOT NULL,
+ description TEXT,
+ related_party TEXT, -- 关联方:客户/供应商名称
+ related_party_type TEXT, -- 关联方类型:customer/supplier
+ order_no TEXT, -- 关联订单号
+ payment_method TEXT, -- 支付方式:cash/transfer/alipay/wechat
+ status TEXT DEFAULT 'confirmed', -- confirmed/pending/cancelled
+ attachments TEXT, -- JSON格式附件列表
+ created_by INTEGER,
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
+ updated_at TEXT DEFAULT CURRENT_TIMESTAMP
+ )''')
+
+ # 客户对账表
+ c.execute('''CREATE TABLE IF NOT EXISTS finance_customer_reconciliation (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ customer_name TEXT NOT NULL,
+ customer_code TEXT,
+ period_start TEXT NOT NULL,
+ period_end TEXT NOT NULL,
+ opening_balance REAL DEFAULT 0, -- 期初余额
+ total_income REAL DEFAULT 0, -- 本期应收
+ total_paid REAL DEFAULT 0, -- 本期已收
+ closing_balance REAL DEFAULT 0, -- 期末余额
+ statement_status TEXT DEFAULT 'unconfirmed', -- unconfirmed/confirmed/disputed
+ statement_date TEXT,
+ confirmed_by TEXT,
+ confirmed_at TEXT,
+ notes TEXT,
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
+ )''')
+
+ # 供应商对账表
+ c.execute('''CREATE TABLE IF NOT EXISTS finance_supplier_reconciliation (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ supplier_name TEXT NOT NULL,
+ supplier_code TEXT,
+ period_start TEXT NOT NULL,
+ period_end TEXT NOT NULL,
+ opening_balance REAL DEFAULT 0,
+ total_expense REAL DEFAULT 0, -- 本期应付
+ total_paid REAL DEFAULT 0, -- 本期已付
+ closing_balance REAL DEFAULT 0,
+ statement_status TEXT DEFAULT 'unconfirmed',
+ statement_date TEXT,
+ confirmed_by TEXT,
+ confirmed_at TEXT,
+ notes TEXT,
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
+ )''')
+
+ # 发票管理表
+ c.execute('''CREATE TABLE IF NOT EXISTS finance_invoices (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ invoice_type TEXT NOT NULL, -- input:进项发票, output:销项发票
+ invoice_no TEXT NOT NULL UNIQUE,
+ invoice_code TEXT,
+ amount REAL NOT NULL,
+ tax_amount REAL,
+ total_amount REAL,
+ issue_date TEXT NOT NULL,
+ related_party TEXT, -- 开票方/收票方
+ related_party_type TEXT, -- customer/supplier
+ order_no TEXT,
+ status TEXT DEFAULT 'normal', -- normal/voided
+ verification_status TEXT DEFAULT 'unverified', -- unverified/verified
+ verification_date TEXT,
+ attachment_url TEXT,
+ notes TEXT,
+ created_by INTEGER,
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
+ )''')
+
+ # 账期提醒配置表
+ c.execute('''CREATE TABLE IF NOT EXISTS finance_payment_terms (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ party_type TEXT NOT NULL, -- customer/supplier
+ party_name TEXT NOT NULL,
+ party_code TEXT,
+ credit_limit REAL DEFAULT 0, -- 信用额度
+ credit_days INTEGER DEFAULT 30, -- 账期天数
+ warning_days INTEGER DEFAULT 7, -- 提前预警天数
+ current_balance REAL DEFAULT 0,
+ last_transaction_date TEXT,
+ alert_enabled INTEGER DEFAULT 1,
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
+ )''')
+
+ # 创建索引
+ c.execute('CREATE INDEX IF NOT EXISTS idx_transactions_date ON finance_transactions(date)')
+ c.execute('CREATE INDEX IF NOT EXISTS idx_transactions_type ON finance_transactions(type)')
+ c.execute('CREATE INDEX IF NOT EXISTS idx_transactions_party ON finance_transactions(related_party)')
+ c.execute('CREATE INDEX IF NOT EXISTS idx_customer_rec_period ON finance_customer_reconciliation(period_start, period_end)')
+ c.execute('CREATE INDEX IF NOT EXISTS idx_supplier_rec_period ON finance_supplier_reconciliation(period_start, period_end)')
+ c.execute('CREATE INDEX IF NOT EXISTS idx_invoices_no ON finance_invoices(invoice_no)')
+ c.execute('CREATE INDEX IF NOT EXISTS idx_invoices_date ON finance_invoices(issue_date)')
+
+ conn.commit()
+ conn.close()
+ print("财务管理表初始化完成")
+
+# ==================== API路由定义 ====================
+
+# 收支管理API
+@finance_bp.route('/transactions', methods=['GET'])
+@require_login
+def list_transactions():
+ """获取收支流水列表"""
+ try:
+ conn = get_db()
+ c = conn.cursor()
+
+ # 获取查询参数
+ page = request.args.get('page', 1, type=int)
+ per_page = request.args.get('per_page', 20, type=int)
+ type_filter = request.args.get('type') # income/expense
+ start_date = request.args.get('start_date')
+ end_date = request.args.get('end_date')
+ category = request.args.get('category')
+ related_party = request.args.get('related_party')
+
+ # 构建查询
+ where_clauses = []
+ params = []
+
+ if type_filter:
+ where_clauses.append('type = ?')
+ params.append(type_filter)
+ if start_date:
+ where_clauses.append('date >= ?')
+ params.append(start_date)
+ if end_date:
+ where_clauses.append('date <= ?')
+ params.append(end_date)
+ if category:
+ where_clauses.append('category = ?')
+ params.append(category)
+ if related_party:
+ where_clauses.append('related_party LIKE ?')
+ params.append(f'%{related_party}%')
+
+ where_sql = ' AND '.join(where_clauses) if where_clauses else '1=1'
+
+ # 获取总数
+ c.execute(f'SELECT COUNT(*) as count FROM finance_transactions WHERE {where_sql}', params)
+ total = c.fetchone()['count']
+
+ # 获取数据
+ offset = (page - 1) * per_page
+ c.execute(f'''
+ SELECT * FROM finance_transactions
+ WHERE {where_sql}
+ ORDER BY date DESC, id DESC
+ LIMIT ? OFFSET ?
+ ''', params + [per_page, offset])
+
+ rows = [dict(r) for r in c.fetchall()]
+ conn.close()
+
+ return jsonify({
+ 'ok': True,
+ 'data': rows,
+ 'pagination': {
+ 'page': page,
+ 'per_page': per_page,
+ 'total': total,
+ 'total_pages': (total + per_page - 1) // per_page
+ }
+ })
+ except Exception as e:
+ return jsonify({'error': str(e)}), 500
+
+@finance_bp.route('/transactions', methods=['POST'])
+@require_login
+def create_transaction():
+ """创建收支记录"""
+ try:
+ data = request.get_json()
+ conn = get_db()
+ c = conn.cursor()
+
+ c.execute('''
+ INSERT INTO finance_transactions
+ (type, category, amount, currency, date, description, related_party,
+ related_party_type, order_no, payment_method, status, created_by)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ ''', (
+ data.get('type'), data.get('category'), data.get('amount'),
+ data.get('currency', 'CNY'), data.get('date'), data.get('description'),
+ data.get('related_party'), data.get('related_party_type'),
+ data.get('order_no'), data.get('payment_method'),
+ data.get('status', 'confirmed'), session.get('user_id')
+ ))
+
+ conn.commit()
+ new_id = c.lastrowid
+ conn.close()
+
+ log('finance_transaction_create', f'创建收支记录 ID:{new_id}')
+
+ return jsonify({'ok': True, 'id': new_id})
+ except Exception as e:
+ return jsonify({'error': str(e)}), 500
+
+@finance_bp.route('/transactions/stats', methods=['GET'])
+@require_login
+def transaction_stats():
+ """获取收支统计"""
+ try:
+ conn = get_db()
+ c = conn.cursor()
+
+ # 默认查询本月
+ today = datetime.now()
+ start_of_month = today.replace(day=1).strftime('%Y-%m-%d')
+
+ # 本月收入
+ c.execute('''
+ SELECT COALESCE(SUM(amount), 0) as total
+ FROM finance_transactions
+ WHERE type = 'income' AND date >= ?
+ ''', (start_of_month,))
+ month_income = c.fetchone()['total']
+
+ # 本月支出
+ c.execute('''
+ SELECT COALESCE(SUM(amount), 0) as total
+ FROM finance_transactions
+ WHERE type = 'expense' AND date >= ?
+ ''', (start_of_month,))
+ month_expense = c.fetchone()['total']
+
+ # 按类别统计
+ c.execute('''
+ SELECT category, type, COALESCE(SUM(amount), 0) as total
+ FROM finance_transactions
+ WHERE date >= ?
+ GROUP BY category, type
+ ''', (start_of_month,))
+
+ by_category = {}
+ for row in c.fetchall():
+ cat = row['category']
+ if cat not in by_category:
+ by_category[cat] = {'income': 0, 'expense': 0}
+ by_category[cat][row['type']] = row['total']
+
+ conn.close()
+
+ return jsonify({
+ 'ok': True,
+ 'stats': {
+ 'month_income': month_income,
+ 'month_expense': month_expense,
+ 'month_net': month_income - month_expense,
+ 'by_category': by_category
+ }
+ })
+ except Exception as e:
+ return jsonify({'error': str(e)}), 500
+
+# 客户对账API - 从对账单表(reconciliations)聚合数据
+@finance_bp.route('/customer-reconciliation', methods=['GET'])
+@require_login
+def list_customer_reconciliation():
+ """获取客户对账列表 - 从对账单表聚合"""
+ try:
+ conn = get_db()
+ c = conn.cursor()
+
+ customer = request.args.get('customer') # 合同号/客户名称
+ period = request.args.get('period') # YYYY-MM,不传则显示所有历史数据
+ merge_by_customer = request.args.get('merge') == 'true' # 是否按客户名称整合
+
+ # 如果传了period参数,按月份筛选;否则查询所有数据
+ if period:
+ year, month = period.split('-')
+ period_start = f"{year}-{month}-01"
+ if month == '12':
+ period_end = f"{int(year)+1}-01-01"
+ else:
+ period_end = f"{year}-{int(month)+1:02d}-01"
+ date_filter = "REPLACE(r.order_date, '/', '-') >= ? AND REPLACE(r.order_date, '/', '-') < ?"
+ date_params = [period_start, period_end]
+ period_label = f"{year}年{month}月"
+ else:
+ # 查询所有历史数据
+ date_filter = "1=1"
+ date_params = []
+ period_start = "1970-01-01" # 用于期初计算
+ period_end = "2099-12-31"
+ period_label = "全部历史"
+
+ # 查询所有客户的对账单聚合
+ # 通过LEFT JOIN关联customer_orders表,根据contract_no=order_no获取客户名称
+ if merge_by_customer:
+ # 按客户名称整合模式:同一客户的多个合同号合并
+ query = f'''
+ SELECT
+ r.contract_no,
+ COALESCE(co.customer_name, r.contract_no) as customer_name,
+ COUNT(*) as order_count,
+ COALESCE(SUM(r.total_amount), 0) as total_amount,
+ COALESCE(SUM(r.quantity), 0) as total_qty,
+ MIN(REPLACE(r.order_date, '/', '-')) as first_order_date,
+ MAX(REPLACE(r.order_date, '/', '-')) as last_order_date,
+ GROUP_CONCAT(DISTINCT r.material_name) as materials
+ FROM reconciliations r
+ LEFT JOIN customer_orders co ON r.contract_no = co.order_no
+ WHERE {date_filter}
+ '''
+ params = list(date_params)
+
+ if customer:
+ query += ' AND (r.contract_no LIKE ? OR co.customer_name LIKE ?)'
+ params.extend([f'%{customer}%', f'%{customer}%'])
+
+ query += ' GROUP BY COALESCE(co.customer_name, r.contract_no) ORDER BY last_order_date DESC'
+ else:
+ # 默认分开显示模式:每个合同号单独显示
+ query = f'''
+ SELECT
+ r.contract_no,
+ COALESCE(co.customer_name, r.contract_no) as customer_name,
+ COUNT(*) as order_count,
+ COALESCE(SUM(r.total_amount), 0) as total_amount,
+ COALESCE(SUM(r.quantity), 0) as total_qty,
+ MIN(REPLACE(r.order_date, '/', '-')) as first_order_date,
+ MAX(REPLACE(r.order_date, '/', '-')) as last_order_date,
+ GROUP_CONCAT(DISTINCT r.material_name) as materials
+ FROM reconciliations r
+ LEFT JOIN customer_orders co ON r.contract_no = co.order_no
+ WHERE {date_filter}
+ '''
+ params = list(date_params)
+
+ if customer:
+ query += ' AND (r.contract_no LIKE ? OR co.customer_name LIKE ?)'
+ params.extend([f'%{customer}%', f'%{customer}%'])
+
+ query += ' GROUP BY r.contract_no ORDER BY last_order_date DESC'
+
+ c.execute(query, params)
+ rows = [dict(r) for r in c.fetchall()]
+
+ # 计算每个客户的期初余额和回款
+ result = []
+ for row in rows:
+ customer_name = row['customer_name']
+ contract_no = row['contract_no']
+
+ if merge_by_customer:
+ # 整合模式:获取该客户关联的所有合同号
+ c.execute('''
+ SELECT DISTINCT r.contract_no
+ FROM reconciliations r
+ LEFT JOIN customer_orders co ON r.contract_no = co.order_no
+ WHERE COALESCE(co.customer_name, r.contract_no) = ?
+ ''', (customer_name,))
+ contract_list = [r['contract_no'] for r in c.fetchall()]
+ else:
+ # 分开显示模式:只查当前合同号
+ contract_list = [contract_no]
+
+ # 构建合同号占位符
+ if contract_list:
+ placeholders = ','.join(['?' for _ in contract_list])
+ else:
+ placeholders = "'NULL'" # 防止空列表
+ contract_list = []
+
+ if period:
+ # 查询历史累计应收(本月之前)
+ opening_query = f'''
+ SELECT COALESCE(SUM(total_amount), 0) as opening_balance
+ FROM reconciliations
+ WHERE contract_no IN ({placeholders}) AND REPLACE(order_date, '/', '-') < ?
+ '''
+ c.execute(opening_query, contract_list + [period_start])
+ opening = c.fetchone()['opening_balance'] or 0
+
+ # 查询本期回款
+ c.execute('''
+ SELECT COALESCE(SUM(amount), 0) as total_paid
+ FROM finance_transactions
+ WHERE related_party = ? AND type = 'income'
+ AND category = '货款' AND date >= ? AND date < ?
+ ''', (customer_name, period_start, period_end))
+ paid = c.fetchone()['total_paid'] or 0
+
+ current_period_amount = row['total_amount']
+ else:
+ # 全部历史模式
+ opening = 0
+
+ # 查询所有历史回款
+ c.execute('''
+ SELECT COALESCE(SUM(amount), 0) as total_paid
+ FROM finance_transactions
+ WHERE related_party = ? AND type = 'income' AND category = '货款'
+ ''', (customer_name,))
+ paid = c.fetchone()['total_paid'] or 0
+
+ current_period_amount = row['total_amount']
+
+ # 计算期末余额
+ closing = opening + current_period_amount - paid
+
+ result.append({
+ 'contract_no': row['contract_no'],
+ 'customer_name': row['customer_name'],
+ 'period_label': period_label,
+ 'order_count': row['order_count'],
+ 'opening_balance': opening,
+ 'total_income': current_period_amount,
+ 'total_paid': paid,
+ 'closing_balance': closing,
+ 'total_qty': row['total_qty'],
+ 'materials': row['materials'],
+ 'first_order_date': row['first_order_date'],
+ 'last_order_date': row['last_order_date'],
+ 'has_period_filter': period is not None,
+ 'is_merged': merge_by_customer
+ })
+
+ conn.close()
+
+ return jsonify({'ok': True, 'data': result, 'period_label': period_label})
+ except Exception as e:
+ import traceback
+ traceback.print_exc()
+ return jsonify({'error': str(e)}), 500
+
+@finance_bp.route('/customer-reconciliation/update', methods=['POST'])
+@require_login
+def update_customer_name():
+ """更新客户订单表中的客户名称"""
+ try:
+ data = request.get_json()
+ order_no = data.get('order_no')
+ customer_name = data.get('customer_name')
+
+ if not order_no or not customer_name:
+ return jsonify({'error': '缺少必要参数'}), 400
+
+ conn = get_db()
+ c = conn.cursor()
+
+ # 检查customer_orders表中是否存在该订单号
+ c.execute('SELECT id FROM customer_orders WHERE order_no = ?', (order_no,))
+ row = c.fetchone()
+
+ if row:
+ # 更新现有记录
+ c.execute('''
+ UPDATE customer_orders
+ SET customer_name = ?, updated_at = ?
+ WHERE order_no = ?
+ ''', (customer_name, datetime.now().strftime('%Y-%m-%d %H:%M:%S'), order_no))
+ else:
+ # 插入新记录(如果合同号在customer_orders中不存在)
+ # 需要先获取一些默认字段值
+ c.execute('''
+ INSERT INTO customer_orders
+ (order_no, customer_name, order_date, material, quantity, unit_price, created_at, updated_at)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+ ''', (order_no, customer_name, datetime.now().strftime('%Y-%m-%d'),
+ '默认物料', 0, 0, datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
+ datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
+
+ conn.commit()
+ conn.close()
+
+ log('update_customer_name', f'更新合同号 {order_no} 的客户名称为 {customer_name}')
+
+ return jsonify({'ok': True, 'message': '客户名称已更新'})
+ except Exception as e:
+ import traceback
+ traceback.print_exc()
+ return jsonify({'error': str(e)}), 500
+
+@finance_bp.route('/customer-reconciliation/generate', methods=['POST'])
+@require_login
+def generate_customer_statement():
+ """生成客户对账单"""
+ try:
+ data = request.get_json()
+ customer_name = data.get('customer_name')
+ period_start = data.get('period_start')
+ period_end = data.get('period_end')
+
+ conn = get_db()
+ c = conn.cursor()
+
+ # 计算期初余额
+ c.execute('''
+ SELECT COALESCE(SUM(CASE WHEN type='income' THEN amount ELSE -amount END), 0) as balance
+ FROM finance_transactions
+ WHERE related_party = ? AND date < ?
+ ''', (customer_name, period_start))
+ opening_balance = c.fetchone()['balance']
+
+ # 计算本期应收
+ c.execute('''
+ SELECT COALESCE(SUM(amount), 0) as total
+ FROM finance_transactions
+ WHERE related_party = ? AND type='income' AND date BETWEEN ? AND ?
+ ''', (customer_name, period_start, period_end))
+ total_income = c.fetchone()['total']
+
+ # 计算本期已收
+ c.execute('''
+ SELECT COALESCE(SUM(amount), 0) as total
+ FROM finance_transactions
+ WHERE related_party = ? AND type='expense' AND category='回款' AND date BETWEEN ? AND ?
+ ''', (customer_name, period_start, period_end))
+ total_paid = c.fetchone()['total']
+
+ closing_balance = opening_balance + total_income - total_paid
+
+ # 保存对账单
+ c.execute('''
+ INSERT INTO finance_customer_reconciliation
+ (customer_name, period_start, period_end, opening_balance, total_income, total_paid, closing_balance)
+ VALUES (?, ?, ?, ?, ?, ?, ?)
+ ''', (customer_name, period_start, period_end, opening_balance, total_income, total_paid, closing_balance))
+
+ conn.commit()
+ new_id = c.lastrowid
+ conn.close()
+
+ return jsonify({'ok': True, 'id': new_id})
+ except Exception as e:
+ return jsonify({'error': str(e)}), 500
+
+# 供应商对账API
+@finance_bp.route('/supplier-reconciliation', methods=['GET'])
+@require_login
+def list_supplier_reconciliation():
+ """获取供应商对账列表"""
+ try:
+ conn = get_db()
+ c = conn.cursor()
+
+ supplier = request.args.get('supplier')
+ period = request.args.get('period')
+
+ where_clauses = []
+ params = []
+
+ if supplier:
+ where_clauses.append('supplier_name LIKE ?')
+ params.append(f'%{supplier}%')
+ if period:
+ where_clauses.append("strftime('%Y-%m', period_start) = ?")
+ params.append(period)
+
+ where_sql = ' AND '.join(where_clauses) if where_clauses else '1=1'
+
+ c.execute(f'''
+ SELECT * FROM finance_supplier_reconciliation
+ WHERE {where_sql}
+ ORDER BY period_end DESC
+ ''', params)
+
+ rows = [dict(r) for r in c.fetchall()]
+ conn.close()
+
+ return jsonify({'ok': True, 'data': rows})
+ except Exception as e:
+ return jsonify({'error': str(e)}), 500
+
+# 发票管理API
+@finance_bp.route('/invoices', methods=['GET'])
+@require_login
+def list_invoices():
+ """获取发票列表"""
+ try:
+ conn = get_db()
+ c = conn.cursor()
+
+ invoice_type = request.args.get('type') # input/output
+ status = request.args.get('status')
+ start_date = request.args.get('start_date')
+ end_date = request.args.get('end_date')
+
+ where_clauses = []
+ params = []
+
+ if invoice_type:
+ where_clauses.append('invoice_type = ?')
+ params.append(invoice_type)
+ if status:
+ where_clauses.append('status = ?')
+ params.append(status)
+ if start_date:
+ where_clauses.append('issue_date >= ?')
+ params.append(start_date)
+ if end_date:
+ where_clauses.append('issue_date <= ?')
+ params.append(end_date)
+
+ where_sql = ' AND '.join(where_clauses) if where_clauses else '1=1'
+
+ c.execute(f'''
+ SELECT * FROM finance_invoices
+ WHERE {where_sql}
+ ORDER BY issue_date DESC
+ ''', params)
+
+ rows = [dict(r) for r in c.fetchall()]
+ conn.close()
+
+ return jsonify({'ok': True, 'data': rows})
+ except Exception as e:
+ return jsonify({'error': str(e)}), 500
+
+@finance_bp.route('/invoices', methods=['POST'])
+@require_login
+def create_invoice():
+ """创建发票记录"""
+ try:
+ data = request.get_json()
+ conn = get_db()
+ c = conn.cursor()
+
+ c.execute('''
+ INSERT INTO finance_invoices
+ (invoice_type, invoice_no, invoice_code, amount, tax_amount, total_amount,
+ issue_date, related_party, related_party_type, order_no, notes, created_by)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ ''', (
+ data.get('invoice_type'), data.get('invoice_no'), data.get('invoice_code'),
+ data.get('amount'), data.get('tax_amount'), data.get('total_amount'),
+ data.get('issue_date'), data.get('related_party'), data.get('related_party_type'),
+ data.get('order_no'), data.get('notes'), session.get('user_id')
+ ))
+
+ conn.commit()
+ new_id = c.lastrowid
+ conn.close()
+
+ return jsonify({'ok': True, 'id': new_id})
+ except Exception as e:
+ return jsonify({'error': str(e)}), 500
+
+@finance_bp.route('/invoices/stats', methods=['GET'])
+@require_login
+def invoice_stats():
+ """获取发票统计"""
+ try:
+ conn = get_db()
+ c = conn.cursor()
+
+ # 默认本月
+ today = datetime.now()
+ start_of_month = today.replace(day=1).strftime('%Y-%m-%d')
+
+ # 进项发票统计
+ c.execute('''
+ SELECT COUNT(*) as count, COALESCE(SUM(total_amount), 0) as total
+ FROM finance_invoices
+ WHERE invoice_type = 'input' AND issue_date >= ? AND status = 'normal'
+ ''', (start_of_month,))
+ input_data = c.fetchone()
+
+ # 销项发票统计
+ c.execute('''
+ SELECT COUNT(*) as count, COALESCE(SUM(total_amount), 0) as total
+ FROM finance_invoices
+ WHERE invoice_type = 'output' AND issue_date >= ? AND status = 'normal'
+ ''', (start_of_month,))
+ output_data = c.fetchone()
+
+ conn.close()
+
+ return jsonify({
+ 'ok': True,
+ 'stats': {
+ 'input': {'count': input_data['count'], 'total': input_data['total']},
+ 'output': {'count': output_data['count'], 'total': output_data['total']}
+ }
+ })
+ except Exception as e:
+ return jsonify({'error': str(e)}), 500
+
+# 账期提醒API
+@finance_bp.route('/payment-terms', methods=['GET'])
+@require_login
+def list_payment_terms():
+ """获取账期提醒配置"""
+ try:
+ conn = get_db()
+ c = conn.cursor()
+
+ party_type = request.args.get('party_type') # customer/supplier
+ alert_only = request.args.get('alert_only', 'false') == 'true'
+
+ where_clauses = []
+ params = []
+
+ if party_type:
+ where_clauses.append('party_type = ?')
+ params.append(party_type)
+ if alert_only:
+ where_clauses.append('alert_enabled = 1')
+
+ where_sql = ' AND '.join(where_clauses) if where_clauses else '1=1'
+
+ c.execute(f'''
+ SELECT * FROM finance_payment_terms
+ WHERE {where_sql}
+ ORDER BY current_balance DESC
+ ''', params)
+
+ rows = [dict(r) for r in c.fetchall()]
+ conn.close()
+
+ return jsonify({'ok': True, 'data': rows})
+ except Exception as e:
+ return jsonify({'error': str(e)}), 500
+
+@finance_bp.route('/payment-terms/alerts', methods=['GET'])
+@require_login
+def get_payment_alerts():
+ """获取账期预警"""
+ try:
+ conn = get_db()
+ c = conn.cursor()
+
+ today = datetime.now().strftime('%Y-%m-%d')
+
+ # 查找超过账期或即将到期的
+ c.execute('''
+ SELECT pt.*,
+ julianday(?) - julianday(pt.last_transaction_date) as days_passed,
+ pt.credit_days - (julianday(?) - julianday(pt.last_transaction_date)) as days_remaining
+ FROM finance_payment_terms pt
+ WHERE pt.alert_enabled = 1
+ AND pt.current_balance > 0
+ AND (julianday(?) - julianday(pt.last_transaction_date)) >= (pt.credit_days - pt.warning_days)
+ ORDER BY days_passed DESC
+ ''', (today, today, today))
+
+ rows = [dict(r) for r in c.fetchall()]
+ conn.close()
+
+ return jsonify({'ok': True, 'data': rows})
+ except Exception as e:
+ return jsonify({'error': str(e)}), 500
+
+# 经营统计API
+@finance_bp.route('/business-stats', methods=['GET'])
+@require_login
+def business_stats():
+ """获取经营统计"""
+ try:
+ conn = get_db()
+ c = conn.cursor()
+
+ # 查询日期范围
+ days = request.args.get('days', 30, type=int)
+ end_date = datetime.now()
+ start_date = end_date - timedelta(days=days)
+
+ # 收支趋势(按天)
+ c.execute('''
+ SELECT date,
+ COALESCE(SUM(CASE WHEN type='income' THEN amount ELSE 0 END), 0) as income,
+ COALESCE(SUM(CASE WHEN type='expense' THEN amount ELSE 0 END), 0) as expense
+ FROM finance_transactions
+ WHERE date BETWEEN ? AND ?
+ GROUP BY date
+ ORDER BY date
+ ''', (start_date.strftime('%Y-%m-%d'), end_date.strftime('%Y-%m-%d')))
+
+ trend = [dict(r) for r in c.fetchall()]
+
+ # 总体统计
+ c.execute('''
+ SELECT
+ COALESCE(SUM(CASE WHEN type='income' THEN amount ELSE 0 END), 0) as total_income,
+ COALESCE(SUM(CASE WHEN type='expense' THEN amount ELSE 0 END), 0) as total_expense
+ FROM finance_transactions
+ WHERE date BETWEEN ? AND ?
+ ''', (start_date.strftime('%Y-%m-%d'), end_date.strftime('%Y-%m-%d')))
+
+ summary = dict(c.fetchone())
+
+ # 客户应收Top10
+ c.execute('''
+ SELECT related_party,
+ COALESCE(SUM(CASE WHEN type='income' THEN amount ELSE -amount END), 0) as balance
+ FROM finance_transactions
+ WHERE related_party_type = 'customer'
+ GROUP BY related_party
+ HAVING balance > 0
+ ORDER BY balance DESC
+ LIMIT 10
+ ''')
+
+ customer_receivables = [dict(r) for r in c.fetchall()]
+
+ # 供应商应付Top10
+ c.execute('''
+ SELECT related_party,
+ COALESCE(SUM(CASE WHEN type='expense' THEN amount ELSE -amount END), 0) as balance
+ FROM finance_transactions
+ WHERE related_party_type = 'supplier'
+ GROUP BY related_party
+ HAVING balance > 0
+ ORDER BY balance DESC
+ LIMIT 10
+ ''')
+
+ supplier_payables = [dict(r) for r in c.fetchall()]
+
+ conn.close()
+
+ return jsonify({
+ 'ok': True,
+ 'stats': {
+ 'trend': trend,
+ 'summary': summary,
+ 'customer_receivables': customer_receivables,
+ 'supplier_payables': supplier_payables
+ }
+ })
+ except Exception as e:
+ return jsonify({'error': str(e)}), 500
+
+# 对账统计API
+@finance_bp.route('/reconciliation-stats', methods=['GET'])
+@require_login
+def reconciliation_stats():
+ """获取对账统计"""
+ try:
+ conn = get_db()
+ c = conn.cursor()
+
+ # 客户对账统计
+ c.execute('''
+ SELECT
+ COUNT(*) as total_statements,
+ SUM(CASE WHEN statement_status = 'confirmed' THEN 1 ELSE 0 END) as confirmed,
+ SUM(CASE WHEN statement_status = 'unconfirmed' THEN 1 ELSE 0 END) as unconfirmed,
+ SUM(CASE WHEN statement_status = 'disputed' THEN 1 ELSE 0 END) as disputed,
+ COALESCE(SUM(closing_balance), 0) as total_balance
+ FROM finance_customer_reconciliation
+ WHERE period_end >= date('now', 'start of month')
+ ''')
+ customer_stats = dict(c.fetchone())
+
+ # 供应商对账统计
+ c.execute('''
+ SELECT
+ COUNT(*) as total_statements,
+ SUM(CASE WHEN statement_status = 'confirmed' THEN 1 ELSE 0 END) as confirmed,
+ SUM(CASE WHEN statement_status = 'unconfirmed' THEN 1 ELSE 0 END) as unconfirmed,
+ SUM(CASE WHEN statement_status = 'disputed' THEN 1 ELSE 0 END) as disputed,
+ COALESCE(SUM(closing_balance), 0) as total_balance
+ FROM finance_supplier_reconciliation
+ WHERE period_end >= date('now', 'start of month')
+ ''')
+ supplier_stats = dict(c.fetchone())
+
+ conn.close()
+
+ return jsonify({
+ 'ok': True,
+ 'stats': {
+ 'customer': customer_stats,
+ 'supplier': supplier_stats
+ }
+ })
+ except Exception as e:
+ return jsonify({'error': str(e)}), 500
+
+# 初始化函数
+@finance_bp.route('/init', methods=['POST'])
+@require_login
+def init_finance():
+ """初始化财务模块(仅管理员)"""
+ try:
+ if session.get('role') not in ['admin', 'superadmin']:
+ return jsonify({'error': 'forbidden'}), 403
+
+ init_finance_tables()
+ return jsonify({'ok': True, 'message': '财务管理模块初始化成功'})
+ except Exception as e:
+ return jsonify({'error': str(e)}), 500