From 0a0caa959c0dd8ef0173680f2f4586a16035ffac Mon Sep 17 00:00:00 2001 From: mac Date: Wed, 8 Jul 2026 16:49:11 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9C=88=E5=BA=A6=E6=80=BB=E8=A7=88?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=B8=9A=E5=8A=A1=E7=BA=BF=E7=BB=8F=E8=90=A5?= =?UTF-8?q?+=E7=8E=B0=E9=87=91=E6=B5=81=E5=8D=A1=E7=89=87+=E8=A1=A8?= =?UTF-8?q?=E5=A4=B4=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 计划+实际模块月度总览: 新增业务线经营情况卡片(毛利/平台费用/利润) - 计划+实际模块月度总览: 新增业务线现金流卡片(项目现金流/平台费用已付/业务线现金流) - 实际模块月度总览: 修复deptCard/cfCard未输出到innerHTML - 计划模块项目表头: 列顺序调整(14列), 已确收→确收, 已付→付款 - 实际模块项目表头: 列顺序调整(12列) - 版本号 v1.0.0.31 --- static/modules/finance.js | 73 ++++++++++++++++++++++++++++++- static/modules/plan.js | 91 +++++++++++++++++++++++++++++++++------ templates/index.html | 2 +- 3 files changed, 151 insertions(+), 15 deletions(-) diff --git a/static/modules/finance.js b/static/modules/finance.js index dede43b..64f6de3 100644 --- a/static/modules/finance.js +++ b/static/modules/finance.js @@ -957,8 +957,79 @@ function renderFinanceOverview() { tbody += rowHtml + ''; } + + // 业务线经营情况卡片 + var finExp = state.data.expense || []; + var expMonthly = [0,0,0,0,0,0,0,0,0,0,0,0]; + for (var ei = 0; ei < finExp.length; ei++) { + if ((finExp[ei].status || '') === '暂不计入') continue; + var em = (finExp[ei].expense_month || '').substring(0, 7); + var mi2 = parseInt(em.substring(5)) - 1; + if (mi2 >= 0 && mi2 < 12) expMonthly[mi2] += parseFloat(finExp[ei].amount || 0); + } + var expAnnual = 0; for (var ei = 0; ei < 12; ei++) expAnnual += expMonthly[ei]; + var grossAnnual = 0; for (var ei = 0; ei < 12; ei++) grossAnnual += (fm[ei] ? fm[ei].gross : 0); + var deptThead = '指标年度累计'; + for (var dmi = 0; dmi < 12; dmi++) { deptThead += '' + mLbl[dmi] + ''; } + deptThead += ''; + var deptRows = [ + { label: '毛利', vals: fm.map(function(x) { return x ? x.gross : 0; }), annual: grossAnnual }, + { label: '平台费用', vals: expMonthly, annual: expAnnual }, + { label: '利润', vals: fm.map(function(x, i) { return (x ? x.gross : 0) - expMonthly[i]; }), annual: grossAnnual - expAnnual } + ]; + var deptTbody = ''; + 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) + ''; + for (var dmi = 0; dmi < 12; dmi++) { + rowHtml += '' + moneyIntL(dr.vals[dmi]) + ''; + } + deptTbody += rowHtml + ''; + } + var deptCard = card('

业务线经营情况

毛利 - 平台费用 = 利润

' + deptThead + '' + deptTbody + '
', 'p-4'); + + // 业务线现金流卡片 + var cfMonthly = [0,0,0,0,0,0,0,0,0,0,0,0]; + for (var cfi = 0; cfi < 12; cfi++) { cfMonthly[cfi] = (fm[cfi] ? (fm[cfi].payment - fm[cfi].paid) : 0); } + var cfAnnual = 0; for (var cfi = 0; cfi < 12; cfi++) cfAnnual += cfMonthly[cfi]; + var expPaidMonthly = [0,0,0,0,0,0,0,0,0,0,0,0]; + for (var epi = 0; epi < finExp.length; epi++) { + if ((finExp[epi].status || '') === '暂不计入') continue; + var epm = (finExp[epi].expense_month || '').substring(0, 7); + var epmi = parseInt(epm.substring(5)) - 1; + if (epmi >= 0 && epmi < 12) expPaidMonthly[epmi] += parseFloat(finExp[epi].incurred_amount || 0); + } + var expPaidAnnual = 0; for (var epi = 0; epi < 12; epi++) expPaidAnnual += expPaidMonthly[epi]; + var blCfMonthly = cfMonthly.map(function(v, i) { return v - expPaidMonthly[i]; }); + var blCfAnnual = cfAnnual - expPaidAnnual; + var cfRows = [ + { label: '项目现金流', vals: cfMonthly, annual: cfAnnual }, + { label: '平台费用(已付)', vals: expPaidMonthly, annual: expPaidAnnual }, + { label: '业务线现金流', vals: blCfMonthly, annual: blCfAnnual } + ]; + var cfTbody = ''; + for (var cfri = 0; cfri < 3; cfri++) { + var cfdr = cfRows[cfri]; + var cfRowCls = cfri === 2 ? 'font-semibold' : ''; + var cfValCls = cfri === 2 ? (cfdr.annual >= 0 ? 'text-green-600' : 'text-red-600') : 'text-slate-700'; + var cfRowHtml = '' + cfdr.label + ''; + cfRowHtml += '' + moneyIntL(cfdr.annual) + ''; + for (var cfdmi = 0; cfdmi < 12; cfdmi++) { + var cellCls = cfValCls; + if (cfri === 2) cellCls = blCfMonthly[cfdmi] >= 0 ? 'text-green-600' : 'text-red-600'; + cfRowHtml += '' + moneyIntL(cfdr.vals[cfdmi]) + ''; + } + cfTbody += cfRowHtml + ''; + } + 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') + diff --git a/static/modules/plan.js b/static/modules/plan.js index e93831e..3c29218 100644 --- a/static/modules/plan.js +++ b/static/modules/plan.js @@ -294,15 +294,15 @@ function renderPlan() { { key: 'weight', label: '加权' }, { key: 'project_type', label: '项目类型' }, { key: 'sign_amount', label: '签约金额' }, - { key: 'rev', label: '已确收' }, + { key: 'rev', label: '确收' }, { key: 'gross', label: '毛利' }, - { key: 'cost', label: '成本' }, - { key: 'profit', label: '项目利润' }, - { key: 'payment', label: '已回款' }, - { key: 'paid', label: '已付' }, + { key: 'gross_rate', label: '毛利率' }, + { key: 'payment', label: '回款' }, { key: 'cashflow', label: '现金流' }, - { key: 'exe_rate', label: '执行率' }, { key: 'pay_rate', label: '回款率' }, + { key: 'cost', label: '成本' }, + { key: 'paid', label: '付款' }, + { key: 'paid_rate', label: '付款率' }, ]; const UNSIGNED_COLS = [ { key: 'client_name', label: '客户' }, @@ -337,18 +337,14 @@ function renderPlan() { const fmtNoUnit = (v) => v ? `${Number(v||0).toLocaleString('zh-CN')}` : ''; 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 payRVal = rev && payment ? Math.round(payment / rev * 100) : null; const payRCls = payRVal === null ? '' : payRVal < 30 ? 'text-red-600' : payRVal < 80 ? 'text-amber-600' : 'text-green-600'; const payR = payRVal !== null ? '' + payRVal + '%' : ''; const paidR = cost && paid ? Math.round(paid / cost * 100) + '%' : ''; const grossR = rev && gross ? Math.round(gross / rev * 100) + '%' : ''; - const cf = cashflow ? money(cashflow) : ''; + const cf = cashflow ? money(cashflow) : ''; const cfCls = cashflow >= 0 ? 'text-green-600' : 'text-red-600'; - const profit = gross; - const pfCls = profit >= 0 ? 'text-green-600' : 'text-red-600'; - const pfVal = profit ? money(profit) : ''; - 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)}${fmtNoUnit(cost)}${pfVal}${fmtNoUnit(payment)}${fmtNoUnit(paid)}${cf}${exe}${payR}`; + 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}`; }; // 待签约简版行 const tdRowUnsigned = (pf, cost, gross) => { @@ -1034,8 +1030,77 @@ function renderPlanOverview() { tbody += rowHtml + ''; } + // 部门经营卡片:毛利、平台费用、利润 + var planExp = state.data.planExpense || []; + var expMonthly = [0,0,0,0,0,0,0,0,0,0,0,0]; + for (var ei = 0; ei < planExp.length; ei++) { + var em = (planExp[ei].expense_month || '').substring(0, 7); + var mi = parseInt(em.substring(5)) - 1; + if (mi >= 0 && mi < 12) expMonthly[mi] += parseFloat(planExp[ei].amount || 0); + } + var expAnnual = 0; for (var ei = 0; ei < 12; ei++) expAnnual += expMonthly[ei]; + var grossAnnual = 0; for (var ei = 0; ei < 12; ei++) grossAnnual += (fm[ei] ? fm[ei].gross : 0); + var deptThead = '指标年度累计'; + for (var dmi = 0; dmi < 12; dmi++) { deptThead += '' + mLbl[dmi] + ''; } + deptThead += ''; + var deptRows = [ + { label: '毛利', vals: fm.map(function(x) { return x ? x.gross : 0; }), annual: grossAnnual }, + { label: '平台费用', vals: expMonthly, annual: expAnnual }, + { label: '利润', vals: fm.map(function(x, i) { return (x ? x.gross : 0) - expMonthly[i]; }), annual: grossAnnual - expAnnual } + ]; + var deptTbody = ''; + 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) + ''; + for (var dmi = 0; dmi < 12; dmi++) { + rowHtml += '' + moneyIntL(dr.vals[dmi]) + ''; + } + deptTbody += rowHtml + ''; + } + var deptCard = card('

业务线经营情况

毛利 - 平台费用 = 利润

' + deptThead + '' + deptTbody + '
', 'p-4'); + + + // 业务线现金流卡片 + var cfMonthly = [0,0,0,0,0,0,0,0,0,0,0,0]; + for (var cfi = 0; cfi < 12; cfi++) { cfMonthly[cfi] = (fm[cfi] ? (fm[cfi].payment - fm[cfi].paid) : 0); } + var cfAnnual = 0; for (var cfi = 0; cfi < 12; cfi++) cfAnnual += cfMonthly[cfi]; + var expPaidMonthly = [0,0,0,0,0,0,0,0,0,0,0,0]; + for (var epi = 0; epi < planExp.length; epi++) { + var epm = (planExp[epi].expense_month || '').substring(0, 7); + var epmi = parseInt(epm.substring(5)) - 1; + if (epmi >= 0 && epmi < 12) expPaidMonthly[epmi] += parseFloat(planExp[epi].incurred_amount || 0); + } + var expPaidAnnual = 0; for (var epi = 0; epi < 12; epi++) expPaidAnnual += expPaidMonthly[epi]; + var blCfMonthly = cfMonthly.map(function(v, i) { return v - expPaidMonthly[i]; }); + var blCfAnnual = cfAnnual - expPaidAnnual; + var cfRows = [ + { label: '项目现金流', vals: cfMonthly, annual: cfAnnual }, + { label: '平台费用(已付)', vals: expPaidMonthly, annual: expPaidAnnual }, + { label: '业务线现金流', vals: blCfMonthly, annual: blCfAnnual } + ]; + var cfTbody = ''; + for (var cfri = 0; cfri < 3; cfri++) { + var cfdr = cfRows[cfri]; + var cfRowCls = cfri === 2 ? 'font-semibold' : ''; + var cfValCls = cfri === 2 ? (cfdr.annual >= 0 ? 'text-green-600' : 'text-red-600') : 'text-slate-700'; + var cfRowHtml = '' + cfdr.label + ''; + cfRowHtml += '' + moneyIntL(cfdr.annual) + ''; + for (var cfdmi = 0; cfdmi < 12; cfdmi++) { + var cellCls = cfValCls; + if (cfri === 2) cellCls = blCfMonthly[cfdmi] >= 0 ? 'text-green-600' : 'text-red-600'; + cfRowHtml += '' + moneyIntL(cfdr.vals[cfdmi]) + ''; + } + cfTbody += cfRowHtml + ''; + } + var cfCard = card('

业务线现金流

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

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

业务(项目)财务概览

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

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

业务(项目)财务概览

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

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

月度签约趋势

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

月度确收与毛利

2026
', 'p-4') + diff --git a/templates/index.html b/templates/index.html index 17d9acf..f394156 100644 --- a/templates/index.html +++ b/templates/index.html @@ -80,7 +80,7 @@
-

OPC Manager v1.0.0.30

+

OPC Manager v1.0.0.31

科普 OPC 工作台