diff --git a/static/modules/finance.js b/static/modules/finance.js index 64f6de3..746ad27 100644 --- a/static/modules/finance.js +++ b/static/modules/finance.js @@ -113,7 +113,7 @@ function renderFinance() { const finHeaderBase = `
|按季度:${qLabels.map((q,i) => ``).join('')}|按月度:${activeMonths.map(m => { const ms = `2026-${String(m).padStart(2,'0')}`; return ``; }).join('')}
`; const finAddBtn = ``; - const finFilterTabs = `
`; + const finFilterTabs = `
`; document.querySelector("#finance").innerHTML = `
${finFilterTabs} @@ -192,12 +192,12 @@ function renderFinance() {

¥0

-

总已付

+

总支出

¥0

- +
月份费用类型成本已付备注
月份费用类型成本支出备注
@@ -252,11 +252,9 @@ function renderFinance() { { label: '已确收', hideUnsigned: true, value: ctx => moneyWan(ctx.sumRev), color: 'text-blue-700' }, { label: '毛利', value: ctx => money(ctx.sumGross), color: 'text-green-700' }, { 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: '已回款', hideUnsigned: true, value: ctx => moneyWan(ctx.sumPay), color: 'text-amber-700' }, - { label: '已付', hideUnsigned: true, value: ctx => moneyWan(ctx.sumPaid), color: 'text-purple-700' }, + { label: '支出', hideUnsigned: true, value: ctx => moneyWan(ctx.sumPaid), color: 'text-purple-700' }, { label: '现金流', hideUnsigned: true, value: ctx => { const v = ctx.sumPay - ctx.sumPaid; return { val: money(v), cls: v >= 0 ? 'text-green-600' : 'text-red-600' }; }, color: null }, - { label: '执行率', hideUnsigned: true, value: ctx => ctx.sumRev && ctx.signTotal ? Math.round(ctx.sumRev / ctx.signTotal * 100) + '%' : '—', color: 'text-slate-500' }, { label: '毛利率', hideUnsigned: true, value: ctx => ctx.sumRev && ctx.sumGross ? Math.round(ctx.sumGross / ctx.sumRev * 100) + '%' : '—', color: 'text-slate-500' }, { label: '回款率', hideUnsigned: true, value: ctx => ctx.sumRev && ctx.sumPay ? Math.round(ctx.sumPay / ctx.sumRev * 100) + '%' : '—', color: 'text-slate-500' }, { label: '付款率', hideUnsigned: true, value: ctx => ctx.sumCost && ctx.sumPaid ? Math.round(ctx.sumPaid / ctx.sumCost * 100) + '%' : '—', color: 'text-slate-500' }, @@ -288,8 +286,9 @@ function renderFinance() { const SIGNED_COLS = [ { key: 'client_name', label: '客户' }, { key: 'customer_name', label: '项目名称' }, { key: 'sign_amount', label: '签约金额' }, { key: 'rev', label: '已确收' }, { key: 'gross', label: '毛利' }, { key: 'gross_rate', label: '毛利率' }, - { key: 'payment', label: '回款' }, { key: 'cashflow', label: '现金流' }, { key: 'pay_rate', label: '回款率' }, - { key: 'cost', label: '成本' }, { key: 'paid', label: '已付' }, { key: 'paid_rate', label: '付款率' }, + { key: 'payment', label: '回款' }, { key: 'pay_rate', label: '回款率' }, + { key: 'cost', label: '成本' }, { key: 'paid', label: '支出' }, { key: 'paid_rate', label: '付款率' }, + { key: 'cashflow', label: '现金流' }, ]; const UNSIGNED_COLS = [ { key: 'client_name', label: '客户' }, { key: 'customer_name', label: '项目名称' }, { key: 'sign_amount', label: '签约金额' }, @@ -310,7 +309,7 @@ function renderFinance() { const cardGrid = isUnsigned ? 'grid-cols-4' : 'grid-cols-6'; const subtitle = isUnsigned ? '合同 → 毛利 → 成本 → 项目利润' - : '合同 → 确收 → 毛利 → 成本 → 项目利润 → 回款 → 已付 → 现金流 → 执行率 → 毛利率 → 回款率 → 付款率'; + : '合同 → 确收 → 毛利 → 成本 → 项目利润 → 回款 → 支出 → 现金流 → 执行率 → 毛利率 → 回款率 → 付款率'; return card(`
${cards.map(([l, v, c]) => '
' + l + '' + v + '
').join("")}
`, "p-4") + card(`

项目财务明细

${subtitle}

${thead}${tbody}
`, "p-4"); }; @@ -326,7 +325,7 @@ function renderFinance() { const grossR = rev && gross ? Math.round(gross / rev * 100) + '%' : ''; const cf = cashflow ? money(cashflow) : ''; const cfCls = cashflow >= 0 ? 'text-green-600' : 'text-red-600'; - return `${esc(pf.client_name || "")}${esc(pf.customer_name)}${money(pf.sign_amount)}${fmtNoUnit(rev)}${fmtNoUnit(gross)}${grossR}${fmtNoUnit(payment)}${cf}${payR}${fmtNoUnit(cost)}${fmtNoUnit(paid)}${paidR}`; + return `${esc(pf.client_name || "")}${esc(pf.customer_name)}${money(pf.sign_amount)}${fmtNoUnit(rev)}${fmtNoUnit(gross)}${grossR}${fmtNoUnit(payment)}${payR}${fmtNoUnit(cost)}${fmtNoUnit(paid)}${paidR}${cf}`; }; // 待签约简版行 const tdRowUnsigned = (pf, cost, gross) => { @@ -926,7 +925,7 @@ window.deleteFinanceItem = async () => { } } -// 月度总览渲染 +// 经营月报渲染 function renderFinanceOverview() { const { summary, financeMonthly } = state.data; if (!summary) return; @@ -936,22 +935,24 @@ function renderFinanceOverview() { const card = (body, cls) => '
' + body + '
'; const fm = financeMonthly || []; const mLbl = ['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月']; - const LABELS = ['合同金额','确收金额','确收毛利','成本','已付']; - const annualVals = [m.signed_annual||m.signed_amount||0, m.revenue_annual||0, m.gross_annual||0, m.cost_annual||0, m.paid_annual||0]; - const fmFields = ['sign','revenue','gross','cost','paid']; + const LABELS = ['合同金额','确收金额','确收毛利','成本','支出','回款','项目现金流']; + const annualVals = [m.signed_annual||m.signed_amount||0, m.revenue_annual||0, m.gross_annual||0, m.cost_annual||0, m.paid_annual||0, m.payment_annual||0, (m.payment_annual||0) - (m.paid_annual||0)]; + const fmFields = ['sign','revenue','gross','cost','paid','payment',null]; var thead = '指标年度累计'; for (var mi = 0; mi < 12; mi++) { thead += '' + mLbl[mi] + ''; } thead += ''; var tbody = ''; - for (var ri = 0; ri < 5; ri++) { + for (var ri = 0; ri < 7; ri++) { var rowHtml = '' + LABELS[ri] + ''; rowHtml += '' + moneyIntL(annualVals[ri]) + ''; for (var mi = 0; mi < 12; mi++) { var val = 0; if (fm[mi]) { - var fk = fmFields[ri]; if (fk) val = fm[mi][fk] || 0; + if (ri === 6) { val = (fm[mi].payment || 0) - (fm[mi].paid || 0); } + else { var fk = fmFields[ri]; if (fk) val = fm[mi][fk] || 0; } } var cls = 'text-slate-700'; + if (ri === 6) cls = val >= 0 ? 'text-green-600' : 'text-red-600'; rowHtml += '' + moneyIntL(val) + ''; } tbody += rowHtml + ''; @@ -1007,7 +1008,7 @@ function renderFinanceOverview() { var blCfAnnual = cfAnnual - expPaidAnnual; var cfRows = [ { label: '项目现金流', vals: cfMonthly, annual: cfAnnual }, - { label: '平台费用(已付)', vals: expPaidMonthly, annual: expPaidAnnual }, + { label: '平台费用(支出)', vals: expPaidMonthly, annual: expPaidAnnual }, { label: '业务线现金流', vals: blCfMonthly, annual: blCfAnnual } ]; var cfTbody = ''; @@ -1024,16 +1025,16 @@ function renderFinanceOverview() { } cfTbody += cfRowHtml + ''; } - var cfCard = card('

业务线现金流

项目现金流 - 平台费用(已付)= 业务线现金流

' + deptThead + '' + cfTbody + '
', 'p-4'); + var cfCard = card('

业务线现金流

项目现金流 - 平台费用(支出)= 业务线现金流

' + deptThead + '' + cfTbody + '
', 'p-4'); document.querySelector("#financeOverview").innerHTML = '
' + - card('

业务(项目)财务月度概览

合同 → 确收 → 毛利 → 成本 → 已付

' + thead + '' + tbody + '
', 'p-4') + + card('

业务(项目)财务月度概览

合同 → 确收 → 毛利 → 成本 → 支出

' + thead + '' + tbody + '
', 'p-4') + deptCard + cfCard + '
' + card('

月度签约趋势

2026
', 'p-4') + card('

月度确收与毛利

2026
', 'p-4') + - card('

月度回款与已付

2026
', 'p-4') + + card('

月度回款与支出

2026
', 'p-4') + '
'; if (window.lucide) window.lucide.createIcons(); @@ -1042,7 +1043,7 @@ function renderFinanceOverview() { } } -// 季度总览渲染 +// 季度月报渲染 function renderFinanceQuarterlyOverview() { const { summary, financeMonthly } = state.data; if (!summary) return; @@ -1050,7 +1051,7 @@ function renderFinanceQuarterlyOverview() { const moneyIntL = (v) => Math.round(Number(v || 0)).toLocaleString("zh-CN"); const card = (body, cls) => '
' + body + '
'; const fm = financeMonthly || []; - const LABELS = ['合同金额','确收金额','确收毛利','成本','已付']; + const LABELS = ['合同金额','确收金额','确收毛利','成本','支出']; const qLbl = ['Q1','Q2','Q3','Q4']; const qRanges = [[0,1,2],[3,4,5],[6,7,8],[9,10,11]]; const annualVals = [m.signed_annual||m.signed_amount||0, m.revenue_annual||0, m.gross_annual||0, m.cost_annual||0, m.paid_annual||0]; @@ -1088,7 +1089,7 @@ function renderFinanceQuarterlyOverview() { tbody += rowHtml + ''; } document.querySelector("#financeQuarterlyOverview").innerHTML = '
' + - card('

业务(项目)财务季度概览

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

' + thead + '' + tbody + '
', 'p-4') + + card('

业务(项目)财务季度概览

合同 → 确收 → 毛利 → 成本 → 项目利润 → 回款 → 支出 → 现金流

' + thead + '' + tbody + '
', 'p-4') + '
'; if (window.lucide) window.lucide.createIcons(); } @@ -1112,7 +1113,7 @@ function renderOverviewCharts(data) { var c3 = document.querySelector("#finChartCash"); if (c3 && window.Chart) { if (state.finChart3) state.finChart3.destroy(); - state.finChart3 = new Chart(c3, Object.assign({}, iconf, { data: { labels: labels, datasets: [{ label: "回款", data: data.map(function(x) { return x.payment || 0; }), borderColor: "#d97706", backgroundColor: "rgba(217,119,6,0.06)", fill: true, tension: 0.3 }, { label: "已付", data: data.map(function(x) { return x.cost || 0; }), borderColor: "#7c3aed", backgroundColor: "rgba(124,58,237,0.06)", fill: true, tension: 0.3 }] } })); + state.finChart3 = new Chart(c3, Object.assign({}, iconf, { data: { labels: labels, datasets: [{ label: "回款", data: data.map(function(x) { return x.payment || 0; }), borderColor: "#d97706", backgroundColor: "rgba(217,119,6,0.06)", fill: true, tension: 0.3 }, { label: "支出", data: data.map(function(x) { return x.cost || 0; }), borderColor: "#7c3aed", backgroundColor: "rgba(124,58,237,0.06)", fill: true, tension: 0.3 }] } })); } var c4 = document.querySelector("#finChartProfit"); if (c4 && window.Chart) { diff --git a/static/modules/plan.js b/static/modules/plan.js index 8c5525e..0ceb755 100644 --- a/static/modules/plan.js +++ b/static/modules/plan.js @@ -111,7 +111,7 @@ function renderPlan() { const finHeaderBase = `
|按季度:${qLabels.map((q,i) => ``).join('')}|按月度:${activeMonths.map(m => { const ms = `2026-${String(m).padStart(2,'0')}`; return ``; }).join('')}
`; const finAddBtn = ``; - const planFilterTabs = `
`; + const planFilterTabs = `
`; document.querySelector("#plan").innerHTML = `
${planFilterTabs} @@ -254,11 +254,9 @@ function renderPlan() { { label: '已确收', hideUnsigned: true, value: ctx => moneyWan(ctx.sumRev), color: 'text-blue-700' }, { label: '毛利', value: ctx => money(ctx.sumGross), color: 'text-green-700' }, { 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: '已回款', hideUnsigned: true, value: ctx => moneyWan(ctx.sumPay), color: 'text-amber-700' }, { label: '已付', hideUnsigned: true, value: ctx => moneyWan(ctx.sumPaid), color: 'text-purple-700' }, { label: '现金流', hideUnsigned: true, value: ctx => { const v = ctx.sumPay - ctx.sumPaid; return { val: money(v), cls: v >= 0 ? 'text-green-600' : 'text-red-600' }; }, color: null }, - { label: '执行率', hideUnsigned: true, value: ctx => ctx.sumRev && ctx.signTotal ? Math.round(ctx.sumRev / ctx.signTotal * 100) + '%' : '—', color: 'text-slate-500' }, { label: '毛利率', hideUnsigned: true, value: ctx => ctx.sumRev && ctx.sumGross ? Math.round(ctx.sumGross / ctx.sumRev * 100) + '%' : '—', color: 'text-slate-500' }, { label: '回款率', hideUnsigned: true, value: ctx => ctx.sumRev && ctx.sumPay ? Math.round(ctx.sumPay / ctx.sumRev * 100) + '%' : '—', color: 'text-slate-500' }, { label: '付款率', hideUnsigned: true, value: ctx => ctx.sumCost && ctx.sumPaid ? Math.round(ctx.sumPaid / ctx.sumCost * 100) + '%' : '—', color: 'text-slate-500' }, @@ -298,11 +296,11 @@ function renderPlan() { { key: 'gross', label: '毛利' }, { key: 'gross_rate', label: '毛利率' }, { key: 'payment', label: '回款' }, - { key: 'cashflow', label: '现金流' }, { key: 'pay_rate', label: '回款率' }, { key: 'cost', label: '成本' }, { key: 'paid', label: '付款' }, { key: 'paid_rate', label: '付款率' }, + { key: 'cashflow', label: '现金流' }, ]; const UNSIGNED_COLS = [ { key: 'client_name', label: '客户' }, @@ -344,7 +342,7 @@ function renderPlan() { const grossR = rev && gross ? Math.round(gross / rev * 100) + '%' : ''; const cf = cashflow ? money(cashflow) : ''; const cfCls = cashflow >= 0 ? 'text-green-600' : 'text-red-600'; - return `${esc(pf.client_name || "")}${esc(pf.customer_name)}${planInlineSelect('weight', pf.id, 'weight', pf.weight || '20%', ['20%','40%','60%','80%','100%'])}${planInlineSelect('ptype', pf.id, 'project_type', pf.project_type || '待签约', ['待签约','已签约'])}${money(pf.sign_amount)}${fmtNoUnit(rev)}${fmtNoUnit(gross)}${grossR}${fmtNoUnit(payment)}${cf}${payR}${fmtNoUnit(cost)}${fmtNoUnit(paid)}${paidR}`; + return `${esc(pf.client_name || "")}${esc(pf.customer_name)}${planInlineSelect('weight', pf.id, 'weight', pf.weight || '20%', ['20%','40%','60%','80%','100%'])}${planInlineSelect('ptype', pf.id, 'project_type', pf.project_type || '待签约', ['待签约','已签约'])}${money(pf.sign_amount)}${fmtNoUnit(rev)}${fmtNoUnit(gross)}${grossR}${fmtNoUnit(payment)}${payR}${fmtNoUnit(cost)}${fmtNoUnit(paid)}${paidR}${cf}`; }; // 待签约简版行 const tdRowUnsigned = (pf, cost, gross) => { @@ -1010,7 +1008,7 @@ function renderPlanOverview() { annualSums.payment += fm[i].payment; annualSums.paid += fm[i].paid; } const mLbl = ['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月']; - const LABELS = ['合同金额','确收金额','确收毛利','成本','已付','回款','项目现金流']; + const LABELS = ['合同金额','确收金额','确收毛利','成本','支出','回款','项目现金流']; // 每行的年度累计 + 12个月值 const annualVals = [ annualSums.sign, annualSums.revenue, annualSums.gross, annualSums.cost, annualSums.paid, @@ -1032,6 +1030,7 @@ function renderPlanOverview() { else { var fk = fmFields[ri]; if (fk) val = fm[mi][fk] || 0; } } var cls = 'text-slate-700'; + if (ri === 2) cls = 'text-green-600'; if (ri === 6) cls = val >= 0 ? 'text-green-600' : 'text-red-600'; rowHtml += '' + moneyIntL(val) + ''; } @@ -1060,11 +1059,12 @@ function renderPlanOverview() { for (var dri = 0; dri < 3; dri++) { var dr = deptRows[dri]; var rowCls = dri === 2 ? 'font-semibold' : ''; - var valCls = dri === 2 ? 'text-green-600' : 'text-slate-700'; var rowHtml = '' + dr.label + ''; - rowHtml += '' + moneyIntL(dr.annual) + ''; + var annualCls = dri === 2 ? (dr.annual >= 0 ? 'text-green-600' : 'text-red-600') : 'text-slate-700'; + rowHtml += '' + moneyIntL(dr.annual) + ''; for (var dmi = 0; dmi < 12; dmi++) { - rowHtml += '' + moneyIntL(dr.vals[dmi]) + ''; + var cellCls = dri === 2 ? (dr.vals[dmi] >= 0 ? 'text-green-600' : 'text-red-600') : 'text-slate-700'; + rowHtml += '' + moneyIntL(dr.vals[dmi]) + ''; } deptTbody += rowHtml + ''; } @@ -1128,7 +1128,7 @@ function renderPlanQuarterlyOverview() { const fm = computePlanMonthly(planFinances); const moneyIntL = (v) => Math.round(Number(v || 0)).toLocaleString("zh-CN"); const card = (body, cls) => '
' + body + '
'; - const LABELS = ['合同金额','确收金额','确收毛利','成本','已付']; + const LABELS = ['合同金额','确收金额','确收毛利','成本','支出']; const qLbl = ['Q1','Q2','Q3','Q4']; const qRanges = [[0,1,2],[3,4,5],[6,7,8],[9,10,11]]; var annualSums2 = { sign: 0, revenue: 0, gross: 0, cost: 0, payment: 0, paid: 0 }; diff --git a/templates/index.html b/templates/index.html index 1cbbd3f..ab50489 100644 --- a/templates/index.html +++ b/templates/index.html @@ -80,7 +80,7 @@
-

OPC Manager v1.0.0.33

+

OPC Manager v1.0.0.34

科普 OPC 工作台