diff --git a/static/modules/finance.js b/static/modules/finance.js index f720675..cdfa79a 100644 --- a/static/modules/finance.js +++ b/static/modules/finance.js @@ -185,13 +185,9 @@ function renderFinance() {

总已付

¥0

-
-

总项目费用

-

¥0

-
- +
月份确收毛利回款应付已付项目费用
月份确收毛利回款应付已付
@@ -229,24 +225,22 @@ function renderFinance() {
${(() => { const METRIC_CARDS = [ - { label: '签约项目总数', value: ctx => ctx.signCnt, color: 'text-slate-700' }, { label: '签约金额', value: ctx => moneyWan(ctx.signTotal), color: 'text-slate-700' }, { label: '已确收', value: ctx => moneyWan(ctx.sumRev), color: 'text-blue-700' }, - { label: '执行率', value: ctx => ctx.sumRev && ctx.signTotal ? Math.round(ctx.sumRev / ctx.signTotal * 100) + '%' : '—', color: 'text-slate-500' }, { label: '毛利', value: ctx => money(ctx.sumGross), color: 'text-green-700' }, - { label: '毛利率', value: ctx => ctx.sumRev && ctx.sumGross ? Math.round(ctx.sumGross / ctx.sumRev * 100) + '%' : '—', color: 'text-slate-500' }, - { label: '项目费用', value: ctx => money(ctx.sumExpense), color: 'text-orange-700' }, - { label: '已回款', value: ctx => moneyWan(ctx.sumPay), color: 'text-amber-700' }, - { label: '回款率', value: ctx => ctx.sumRev && ctx.sumPay ? Math.round(ctx.sumPay / ctx.sumRev * 100) + '%' : '—', color: 'text-slate-500' }, { label: '应付', value: ctx => moneyWan(ctx.sumCost), color: 'text-rose-700' }, + { label: '项目利润', value: ctx => { const v = ctx.sumGross; return { val: moneyWan(v), cls: v >= 0 ? 'text-green-600' : 'text-red-600' }; }, color: null }, + { label: '已回款', value: ctx => moneyWan(ctx.sumPay), color: 'text-amber-700' }, { label: '已付', value: ctx => moneyWan(ctx.sumPaid), color: 'text-purple-700' }, - { label: '付款率', value: ctx => ctx.sumCost && ctx.sumPaid ? Math.round(ctx.sumPaid / ctx.sumCost * 100) + '%' : '—', color: 'text-slate-500' }, { label: '现金流', value: ctx => { const v = ctx.sumPay - ctx.sumPaid; return { val: money(v), cls: v >= 0 ? 'text-green-600' : 'text-red-600' }; }, color: null }, - { label: '项目利润', value: ctx => { const v = ctx.sumGross - ctx.sumExpense; return { val: moneyWan(v), cls: v >= 0 ? 'text-green-600' : 'text-red-600' }; }, color: null }, + { label: '执行率', value: ctx => ctx.sumRev && ctx.signTotal ? Math.round(ctx.sumRev / ctx.signTotal * 100) + '%' : '—', color: 'text-slate-500' }, + { label: '毛利率', value: ctx => ctx.sumRev && ctx.sumGross ? Math.round(ctx.sumGross / ctx.sumRev * 100) + '%' : '—', color: 'text-slate-500' }, + { label: '回款率', value: ctx => ctx.sumRev && ctx.sumPay ? Math.round(ctx.sumPay / ctx.sumRev * 100) + '%' : '—', color: 'text-slate-500' }, + { label: '付款率', value: ctx => ctx.sumCost && ctx.sumPaid ? Math.round(ctx.sumPaid / ctx.sumCost * 100) + '%' : '—', color: 'text-slate-500' }, ]; - const renderView = (rows, sumRev, sumPay, sumCost, sumPaid, sumGross, sumExpense, signTotal, signCnt, emptyText) => { - const ctx = { sumRev, sumPay, sumCost, sumPaid, sumGross, sumExpense, signTotal, signCnt }; + const renderView = (rows, sumRev, sumPay, sumCost, sumPaid, sumGross, signTotal, signCnt, emptyText) => { + const ctx = { sumRev, sumPay, sumCost, sumPaid, sumGross, signTotal, signCnt }; const cards = METRIC_CARDS.map(c => { const v = c.value(ctx); return typeof v === 'object' ? [c.label, v.val, v.cls] : [c.label, v, c.color]; @@ -256,26 +250,25 @@ function renderFinance() { '状态' + '签约金额' + '已确收' + - '执行率' + '毛利' + - '毛利率' + - '已回款' + - '回款率' + '应付' + - '已付' + - '付款率' + - '项目费用' + - '现金流' + '项目利润' + + '已回款' + + '已付' + + '现金流' + + '执行率' + + '毛利率' + + '回款率' + + '付款率' + ''; const tbody = rows.length ? `${rows.join("")}` : `${emptyText}`; - return card(`
${cards.map(([l, v, c]) => '
' + l + '' + v + '
').join("")}
`, "p-4") + - card(`
${thead}${tbody}
`, "p-4"); + return card(`
${cards.map(([l, v, c]) => '
' + l + '' + v + '
').join("")}
`, "p-4") + + card(`

项目财务明细

合同 → 确收 → 毛利 → 应付 → 项目利润 → 回款 → 已付 → 现金流 → 执行率 → 毛利率 → 回款率 → 付款率

${thead}${tbody}
`, "p-4"); }; const fmtNoUnit = (v) => v ? `${Number(v||0).toLocaleString('zh-CN')}` : ''; - const tdRow = (pf, rev, payment, cost, paid, gross, expense) => { + const tdRow = (pf, rev, payment, cost, paid, gross) => { const cashflow = payment - paid; const exe = rev && pf.sign_amount ? Math.round(rev / pf.sign_amount * 100) + '%' : ''; const payR = rev && payment ? Math.round(payment / rev * 100) + '%' : ''; @@ -284,10 +277,10 @@ function renderFinance() { const st = pf.status === '已签约' ? '已签约' : '待签约'; const cf = cashflow ? money(cashflow) : ''; const cfCls = cashflow >= 0 ? 'text-green-600' : 'text-red-600'; - const profit = gross - expense; + const profit = gross; const pfCls = profit >= 0 ? 'text-green-600' : 'text-red-600'; const pfVal = profit ? money(profit) : ''; - return `${esc(pf.customer_name)}${st}${money(pf.sign_amount)}${fmtNoUnit(rev)}${exe}${fmtNoUnit(gross)}${grossR}${fmtNoUnit(payment)}${payR}${fmtNoUnit(cost)}${fmtNoUnit(paid)}${paidR}${fmtNoUnit(expense)}${cf}${pfVal}`; + return `${esc(pf.customer_name)}${st}${money(pf.sign_amount)}${fmtNoUnit(rev)}${fmtNoUnit(gross)}${fmtNoUnit(cost)}${pfVal}${fmtNoUnit(payment)}${fmtNoUnit(paid)}${cf}${exe}${grossR}${payR}${paidR}`; }; if (state.finView === 'monthly') { @@ -297,7 +290,7 @@ function renderFinance() { if (!state.finMonth) state.finMonth = defaultMonth; const selMonth = state.finMonth; const rows = []; - let sumRev = 0, sumPay = 0, sumCost = 0, sumPaid = 0, sumGross = 0, sumExpense = 0; + let sumRev = 0, sumPay = 0, sumCost = 0, sumPaid = 0, sumGross = 0; allPfs.forEach(pf => { let bd = []; try { bd = JSON.parse(pf.budget_data || "[]"); } catch (e) {} const b = bd.find(x => (x.month || "") === selMonth) || {}; @@ -306,14 +299,13 @@ function renderFinance() { const cost = Math.round(parseFloat(b.cost || 0) || 0); const paid = Math.round(parseFloat(b.paid || 0) || 0); const gross = Math.round(parseFloat(b.gross || 0) || 0); - const expense = Math.round(parseFloat(b.expense || 0) || 0); - if (!rev && !payment && !cost && !paid && !gross && !expense) return; - sumRev += rev; sumPay += payment; sumCost += cost; sumPaid += paid; sumGross += gross; sumExpense += expense; - rows.push(tdRow(pf, rev, payment, cost, paid, gross, expense)); + if (!rev && !payment && !cost && !paid && !gross) return; + sumRev += rev; sumPay += payment; sumCost += cost; sumPaid += paid; sumGross += gross; + rows.push(tdRow(pf, rev, payment, cost, paid, gross)); }); const signTotal = Math.round(allPfs.filter(x => (x.sign_month || "").substring(0, 7) === selMonth).reduce((a, p) => a + (p.sign_amount || 0), 0)); const signCnt = allPfs.filter(x => x.status === "已签约" && (x.sign_month || "").substring(0, 7) === selMonth).length; - return renderView(rows, sumRev, sumPay, sumCost, sumPaid, sumGross, sumExpense, signTotal, signCnt, '该月份暂无数据'); + return renderView(rows, sumRev, sumPay, sumCost, sumPaid, sumGross, signTotal, signCnt, '该月份暂无数据'); } if (state.finView === 'quarterly') { @@ -331,7 +323,7 @@ function renderFinance() { try { JSON.parse(pf.budget_data || "[]").forEach(b => { const m = parseInt((b.month || "").substring(5)) || 0; if (qRange.includes(m)) total += parseFloat(b[field] || 0); }); } catch (e) {} return total; }; - let sumRev = 0, sumPay = 0, sumCost = 0, sumPaid = 0, sumGross = 0, sumExpense = 0; + let sumRev = 0, sumPay = 0, sumCost = 0, sumPaid = 0, sumGross = 0; const rows = []; allPfs.forEach(pf => { const rev = sumBudget(pf, "rev"); @@ -339,34 +331,33 @@ function renderFinance() { const cost = sumBudget(pf, "cost"); const paid = sumBudget(pf, "paid"); const gross = sumBudget(pf, "gross"); - const expense = sumBudget(pf, "expense"); - sumRev += rev; sumPay += payment; sumCost += cost; sumPaid += paid; sumGross += gross; sumExpense += expense; - rows.push(tdRow(pf, rev, payment, cost, paid, gross, expense)); + sumRev += rev; sumPay += payment; sumCost += cost; sumPaid += paid; sumGross += gross; + rows.push(tdRow(pf, rev, payment, cost, paid, gross)); }); const signTotal = Math.round(allPfs.filter(x => { const m = parseInt((x.sign_month || "0").substring(5)) || 0; return qRange.includes(m); }).reduce((a, p) => a + (p.sign_amount || 0), 0)); const signCnt = allPfs.filter(x => x.status === "已签约" && qRange.includes(parseInt((x.sign_month || "0").substring(5)) || 0)).length; - return renderView(rows, sumRev, sumPay, sumCost, sumPaid, sumGross, sumExpense, signTotal, signCnt, '该季度暂无数据'); + return renderView(rows, sumRev, sumPay, sumCost, sumPaid, sumGross, signTotal, signCnt, '该季度暂无数据'); } // 总视图 const calcTotals = (pf) => { let budget = []; try { budget = JSON.parse(pf.budget_data || "[]"); } catch (e) {} - let rev = 0, payment = 0, cost = 0, gross = 0, expense = 0; - budget.forEach(b => { rev += parseFloat(b.rev || 0) || 0; gross += parseFloat(b.gross || 0) || 0; payment += parseFloat(b.payment || 0) || 0; cost += parseFloat(b.cost || 0) || 0; expense += parseFloat(b.expense || 0) || 0; }); + let rev = 0, payment = 0, cost = 0, gross = 0; + budget.forEach(b => { rev += parseFloat(b.rev || 0) || 0; gross += parseFloat(b.gross || 0) || 0; payment += parseFloat(b.payment || 0) || 0; cost += parseFloat(b.cost || 0) || 0; }); const paid = parseFloat(pf.total_paid) || 0; - return { rev: Math.round(rev), payment: Math.round(payment), cost: Math.round(cost), paid: Math.round(paid), gross: Math.round(gross), expense: Math.round(expense) }; + return { rev: Math.round(rev), payment: Math.round(payment), cost: Math.round(cost), paid: Math.round(paid), gross: Math.round(gross) }; }; const allPfs = pfs.filter(x => x.status === state.finFilter); - let sumRev = 0, sumPay = 0, sumCost = 0, sumPaid = 0, sumGross = 0, sumExpense = 0; + let sumRev = 0, sumPay = 0, sumCost = 0, sumPaid = 0, sumGross = 0; const rows = []; allPfs.forEach(pf => { const t = calcTotals(pf); - sumRev += t.rev; sumPay += t.payment; sumCost += t.cost; sumPaid += t.paid; sumGross += t.gross; sumExpense += t.expense; - rows.push(tdRow(pf, t.rev, t.payment, t.cost, t.paid, t.gross, t.expense)); + sumRev += t.rev; sumPay += t.payment; sumCost += t.cost; sumPaid += t.paid; sumGross += t.gross; + rows.push(tdRow(pf, t.rev, t.payment, t.cost, t.paid, t.gross)); }); const signTotal = Math.round(allPfs.reduce((a, p) => a + (p.sign_amount || 0), 0)); const signCnt = allPfs.filter(x => x.status === "已签约").length; - return renderView(rows, sumRev, sumPay, sumCost, sumPaid, sumGross, sumExpense, signTotal, signCnt, '暂无数据'); + return renderView(rows, sumRev, sumPay, sumCost, sumPaid, sumGross, signTotal, signCnt, '暂无数据'); })()} `; if (window.lucide) window.lucide.createIcons(); @@ -387,7 +378,7 @@ window.openFinanceModal = () => { modal.classList.remove("hidden"); }; -window.addBudgetRow = (month = '', rev = '', gross = '', payment = '', cost = '', paid = '', expense = '') => { +window.addBudgetRow = (month = '', rev = '', gross = '', payment = '', cost = '', paid = '') => { const tbody = document.querySelector("#budgetTbody"); if (!tbody) return; const row = document.createElement("tr"); @@ -397,7 +388,6 @@ window.addBudgetRow = (month = '', rev = '', gross = '', payment = '', cost = '' - `; tbody.appendChild(row); if (window.lucide) window.lucide.createIcons(); @@ -408,27 +398,23 @@ window.updateBudgetSummary = () => { const grossEl = document.querySelector("#budgetTotalGross"); const paymentEl = document.querySelector("#budgetTotalPayment"); const costEl = document.querySelector("#budgetTotalCost"); - const expenseEl = document.querySelector("#budgetTotalExpense"); const paidEl = document.querySelector("#budgetTotalPaid"); if (!revEl || !grossEl) return; const revInputs = document.querySelectorAll('[name="budget_rev[]"]'); const grossInputs = document.querySelectorAll('[name="budget_gross[]"]'); const paymentInputs = document.querySelectorAll('[name="budget_payment[]"]'); const costInputs = document.querySelectorAll('[name="budget_cost[]"]'); - const expenseInputs = document.querySelectorAll('[name="budget_expense[]"]'); const paidInputs = document.querySelectorAll('[name="budget_paid[]"]'); - let totalRev = 0, totalGross = 0, totalPayment = 0, totalCost = 0, totalExpense = 0, totalPaid = 0; + let totalRev = 0, totalGross = 0, totalPayment = 0, totalCost = 0, totalPaid = 0; revInputs.forEach(el => { totalRev += parseFloat(el.value) || 0; }); grossInputs.forEach(el => { totalGross += parseFloat(el.value) || 0; }); paymentInputs.forEach(el => { totalPayment += parseFloat(el.value) || 0; }); costInputs.forEach(el => { totalCost += parseFloat(el.value) || 0; }); - expenseInputs.forEach(el => { totalExpense += parseFloat(el.value) || 0; }); paidInputs.forEach(el => { totalPaid += parseFloat(el.value) || 0; }); revEl.textContent = money(totalRev); grossEl.textContent = money(totalGross); if (paymentEl) paymentEl.textContent = money(totalPayment); if (costEl) costEl.textContent = money(totalCost); - if (expenseEl) expenseEl.textContent = money(totalExpense); if (paidEl) paidEl.textContent = money(totalPaid); }; @@ -437,7 +423,7 @@ window.initBudgetTable = (budgetData) => { if (!tbody) return; tbody.innerHTML = ""; const rows = budgetData || []; - rows.forEach(r => addBudgetRow(r.month || '', r.rev || '', r.gross || '', r.payment || '', r.cost || '', r.paid || '', r.expense || '')); + rows.forEach(r => addBudgetRow(r.month || '', r.rev || '', r.gross || '', r.payment || '', r.cost || '', r.paid || '')); setTimeout(() => updateBudgetSummary(), 50); }; @@ -705,7 +691,6 @@ window.createFinance = async (event) => { const grosses = form.querySelectorAll('[name="budget_gross[]"]'); const payments = form.querySelectorAll('[name="budget_payment[]"]'); const costs = form.querySelectorAll('[name="budget_cost[]"]'); - const expenses = form.querySelectorAll('[name="budget_expense[]"]'); const paids = form.querySelectorAll('[name="budget_paid[]"]'); const budgetRows = []; let totalRev = 0, totalGross = 0, totalPaidFromBudget = 0; @@ -716,9 +701,8 @@ window.createFinance = async (event) => { const gross = parseFloat(grosses[i].value) || 0; const payment = parseFloat(payments[i].value) || 0; const cost = parseFloat(costs[i].value) || 0; - const expense = parseFloat(expenses[i].value) || 0; const paid = parseFloat(paids[i].value) || 0; - budgetRows.push({ month: m, rev, gross, payment, cost, expense, paid }); + budgetRows.push({ month: m, rev, gross, payment, cost, paid }); totalRev += rev; totalGross += gross; totalPaidFromBudget += paid; @@ -727,12 +711,10 @@ window.createFinance = async (event) => { data.total_rev = totalRev; data.total_gross = totalGross; data.total_paid = totalPaidFromBudget; - let totalPayment = 0, totalCost = 0, totalExpense = 0; - for (const r of budgetRows) { totalPayment += r.payment; totalCost += r.cost; totalExpense += r.expense; } + let totalPayment = 0, totalCost = 0; + for (const r of budgetRows) { totalPayment += r.payment; totalCost += r.cost; } data.total_payment = totalPayment; data.total_cost = totalCost; - data.total_expense = totalExpense; - data.total_cost = totalCost; // 收集任务管理数据 const taskTypeInputs = form.querySelectorAll('[name="task_type[]"]'); const taskCountInputs = form.querySelectorAll('[name="task_count[]"]'); diff --git a/static/modules/home.js b/static/modules/home.js index 80e925c..3d74a26 100644 --- a/static/modules/home.js +++ b/static/modules/home.js @@ -74,25 +74,25 @@ function renderHome() { const sign = pct >= 0 ? '+' : ''; return '' + sign + pct + '%'; }; - const COLORS = ['text-slate-700','text-blue-600','text-green-600','text-amber-600','text-rose-600','text-purple-600','text-orange-600','text-cyan-600','text-indigo-600']; - const LABELS = ['合同金额','确收金额','确收毛利','回款金额','应付金额','已付','项目费用','现金流','项目利润']; - const Q_FIELDS = [m.signed_q2||0, m.revenue_q2, m.gross_q2, m.payment_q2||0, m.cost_q2||0, m.paid_q2||0, m.proj_expense_q2||0, m.cashflow_q2||0, m.profit_q2||0]; - const Q_PREV = [m.signed_prev_q||0, m.revenue_prev_q||0, m.gross_prev_q||0, m.payment_prev_q||0, m.cost_prev_q||0, m.paid_prev_q||0, m.proj_expense_prev_q||0, m.cashflow_prev_q||0, m.profit_prev_q||0]; - const M_FIELDS = [m.signed_month||0, m.monthly_revenue, m.monthly_net_profit, m.payment_month||0, m.cost_month||0, m.paid_month||0, m.proj_expense_month||0, m.cashflow_month||0, m.profit_month||0]; - const M_PREV = [m.signed_prev_month||0, m.revenue_prev_month||0, m.gross_prev_month||0, m.payment_prev_month||0, m.cost_prev_month||0, m.paid_prev_month||0, m.proj_expense_prev_month||0, m.cashflow_prev_month||0, m.profit_prev_month||0]; - const ROWS = [rows1, rows2, rows3, rows4, rows5, rows7, rowsProjExpense, rows8, rows9]; + const COLORS = ['text-slate-700','text-blue-600','text-green-600','text-rose-600','text-indigo-600','text-amber-600','text-purple-600','text-cyan-600']; + const LABELS = ['合同金额','确收金额','确收毛利','应付金额','项目利润','回款金额','已付','现金流']; + const Q_FIELDS = [m.signed_q2||0, m.revenue_q2, m.gross_q2, m.cost_q2||0, m.profit_q2||0, m.payment_q2||0, m.paid_q2||0, m.cashflow_q2||0]; + const Q_PREV = [m.signed_prev_q||0, m.revenue_prev_q||0, m.gross_prev_q||0, m.cost_prev_q||0, m.profit_prev_q||0, m.payment_prev_q||0, m.paid_prev_q||0, m.cashflow_prev_q||0]; + const M_FIELDS = [m.signed_month||0, m.monthly_revenue, m.monthly_net_profit, m.cost_month||0, m.profit_month||0, m.payment_month||0, m.paid_month||0, m.cashflow_month||0]; + const M_PREV = [m.signed_prev_month||0, m.revenue_prev_month||0, m.gross_prev_month||0, m.cost_prev_month||0, m.profit_prev_month||0, m.payment_prev_month||0, m.paid_prev_month||0, m.cashflow_prev_month||0]; + const ROWS = [rows1, rows2, rows3, rows5, rows9, rows4, rows7, rows8]; // 现金流和项目利润的原始值数组,用于正负着色 const CF_RAW = [m.cashflow_annual||0, m.cashflow_q2||0, m.cashflow_month||0, m.cashflow_prev_q||0, m.cashflow_prev_month||0]; const PF_RAW = [m.profit_annual||0, m.profit_q2||0, m.profit_month||0, m.profit_prev_q||0, m.profit_prev_month||0]; - var valCls = function(ri) { return (ri === 7 || ri === 8) ? '' : 'text-slate-800'; }; + var valCls = function(ri) { return (ri === 4 || ri === 7) ? '' : 'text-slate-800'; }; var tdVal = function(ri, col, val) { + if (ri === 4) return '' + val + ''; if (ri === 7) return '' + val + ''; - if (ri === 8) return '' + val + ''; return '' + val + ''; }; - var thead = '指标年度累计上本季度累计上月累计本季度累计季环比本月累计月环比'; + var thead = '指标年度累计上季度累计上月累计本季度累计季环比本月累计月环比'; var tbody = ''; - for (var ri = 0; ri < 9; ri++) { + for (var ri = 0; ri < 8; ri++) { var vals = ROWS[ri]; tbody += '' + LABELS[ri] + '' + tdVal(ri, 0, vals[0][1]) + @@ -113,7 +113,7 @@ function renderHome() { ["产品迭代", m.upcoming_products, "products"], ].map(([label, value, tab]) => ``).join("")} `} - ${card(`

部门经营情况

+ ${card(`

部门经营情况

指标年度累计上本季度累计上月累计本季度累计季环比本月累计月环比
${(() => { var d = [ ['部门毛利', [m.profit_annual||0, m.profit_q2||0, m.profit_month||0, m.profit_prev_q||0, m.profit_prev_month||0], @@ -145,7 +145,7 @@ function renderHome() { return h; })()}
指标年度累计上季度累计上月累计本季度累计季环比本月累计月环比
`, "p-4")} - ${card(`

业务(项目)财务概览

${thead}${tbody}
`, "p-4")} + ${card(`

业务(项目)财务概览

合同 → 确收 → 毛利 → 应付 → 项目利润 → 回款 → 已付 → 现金流

${thead}${tbody}
`, "p-4")}
${card(`

月度签约趋势

2026
`, "p-4")} ${card(`

月度确收与毛利

2026
`, "p-4")}