diff --git a/backend/helpers.py b/backend/helpers.py index efec6f7..1aa56c5 100644 --- a/backend/helpers.py +++ b/backend/helpers.py @@ -61,8 +61,8 @@ def attach_common(conn, resource, items): return items -def monthly_finance(conn, tenant="科普·无界"): - _year = date.today().year +def monthly_finance(conn, tenant="科普·无界", year=None): + _year = year or date.today().year months = [f"{_year}-{m:02d}" for m in range(1, 13)] pfs = rows(conn, "SELECT sign_amount, sign_month, budget_data, expense_data FROM project_finances WHERE tenant=?", diff --git a/backend/routes.py b/backend/routes.py index 606e429..895990c 100644 --- a/backend/routes.py +++ b/backend/routes.py @@ -489,6 +489,42 @@ def bootstrap(): conn.close() +@bp.route("/api/finance/yearly") +@login_required +def finance_yearly(): + """按年份返回经营月报/季报所需的 12 月财务数据,支持历史年份切换。year=0 表示所有年份""" + _year = request.args.get("year", type=int) + if _year is None: + _year = date.today().year + tenant = request.args.get("tenant", session.get("tenants", ["科普·无界"])[0]) + allowed = session.get("tenants", []) + allowed = [t if t != "总工作台" else "科普·无界" for t in allowed] + allowed = [t if t != "学会·无界" else "学术·无界" for t in allowed] + if tenant not in allowed: + tenant = allowed[0] if allowed else "" + conn = db() + try: + if _year == 0: + # 所有年份:查出所有有数据的年份,合并 12 个月 + yr_rows = rows(conn, + "SELECT DISTINCT SUBSTRING(sign_month,1,4) AS y FROM project_finances WHERE tenant=? AND sign_month IS NOT NULL AND sign_month!='' ORDER BY y", + [tenant]) + all_years = [int(r["y"]) for r in yr_rows if r["y"] and r["y"].isdigit()] + if not all_years: + all_years = [date.today().year] + merged = [{"month": f"所有-{m+1:02d}", "revenue": 0, "gross": 0, "payment": 0, "cost": 0, "paid": 0, "sign": 0} + for m in range(12)] + for y in all_years: + fm = monthly_finance(conn, tenant, y) + for i, m in enumerate(fm): + for k in ("revenue", "gross", "payment", "cost", "paid", "sign"): + merged[i][k] = round(merged[i][k] + (m[k] or 0), 2) + return jsonify({"year": 0, "financeMonthly": merged}) + return jsonify({"year": _year, "financeMonthly": monthly_finance(conn, tenant, _year)}) + finally: + conn.close() + + # ---------- 通用 CRUD ---------- @bp.route("/api//list", methods=["GET"]) diff --git a/static/modules/finance.js b/static/modules/finance.js index eae9f68..13d2722 100644 --- a/static/modules/finance.js +++ b/static/modules/finance.js @@ -105,15 +105,21 @@ function renderFinance() { const defaultMonth2 = now2.getFullYear() + "-" + String(now2.getMonth()+1).padStart(2,"0"); if (!state.finMonth) state.finMonth = defaultMonth2; if (state.finQuarter === undefined) state.finQuarter = Math.floor(now2.getMonth() / 3); - if (!state.finYear) state.finYear = now2.getFullYear(); - if (!state.finView) state.finView = 'annual'; + if (state.finYear == null) state.finYear = now2.getFullYear(); + if (state.finMonthlyYear === undefined) state.finMonthlyYear = state.finYear; + if (!['annual','quarterly','monthly'].includes(state.finView)) state.finView = 'annual'; + if (!['overview','quarterlyOverview','projects','expense'].includes(state.finFilter)) state.finFilter = 'projects'; + if (state.finView === 'monthly' && state.finMonth) { + const monthSuffix = state.finMonth.slice(-2); + state.finMonth = state.finYear + '-' + monthSuffix; + } const qLabels = ['Q1','Q2','Q3','Q4']; const qMonths = [[1,2,3],[4,5,6],[7,8,9],[10,11,12]]; const nowD = new Date(); const activeQ = state.finQuarter !== undefined ? state.finQuarter : Math.floor(nowD.getMonth() / 3); const activeMonths = qMonths[activeQ]; - const yearOpts = [2024,2025,2026,2027]; - const finHeaderBase = `
年份:||季度:${qLabels.map((q,i) => ``).join('')}|月度:${activeMonths.map(m => { const ms = `${state.finYear}-${String(m).padStart(2,'0')}`; return ``; }).join('')}
`; + const yearOpts = [2020,2021,2022,2023,2024,2025,2026,2027]; + const finHeaderBase = `
年份:||季度:${qLabels.map((q,i) => ``).join('')}|月度:${activeMonths.map(m => { const ms = `${state.finYear}-${String(m).padStart(2,'0')}`; return ``; }).join('')}
`; const finAddBtn = ``; const isOverviewMode = false; @@ -242,13 +248,21 @@ function renderFinance() {
${(() => { - if (state.finFilter === 'overview') { - setTimeout(() => renderFinanceOverview(), 10); - return `
`; - } - if (state.finFilter === 'quarterlyOverview') { - setTimeout(() => renderFinanceQuarterlyOverview(), 10); - return `
`; + if (state.finFilter === 'overview' || state.finFilter === 'quarterlyOverview') { + const isQ = state.finFilter === 'quarterlyOverview'; + const renderFn = isQ ? renderFinanceQuarterlyOverview : renderFinanceOverview; + const containerId = isQ ? 'financeQuarterlyOverview' : 'financeOverview'; + if (state.finMonthlyYear !== state.finYear) { + const y = state.finYear; + fetch(`/api/finance/yearly?year=${y}&tenant=${encodeURIComponent(state.tenant)}`).then(r => r.json()).then(d => { + if (d && d.financeMonthly) { state.data.financeMonthly = d.financeMonthly; state.finMonthlyYear = d.year; } + else { state.data.financeMonthly = []; state.finMonthlyYear = y; } + renderFn(); + }).catch(() => { state.finMonthlyYear = y; renderFn(); }); + } else { + setTimeout(renderFn, 10); + } + return `
`; } if (state.finFilter === 'expense') { setTimeout(() => renderExpense(), 10); @@ -378,17 +392,26 @@ function renderFinance() { const defaultMonth = now.getFullYear() + "-" + String(now.getMonth() + 1).padStart(2, "0"); if (!state.finMonth) state.finMonth = defaultMonth; const selMonth = state.finMonth; + const isAllMonth = selMonth.startsWith('0-'); + const monthSuffix = selMonth.slice(-2); var dataItems = []; allPfs.forEach(pf => { let bd = []; try { bd = JSON.parse(pf.budget_data || "[]"); } catch (e) {} let ed = []; try { ed = JSON.parse(pf.expense_data || "[]"); } catch (e) {} - const b = bd.find(x => (x.month || "") === selMonth) || {}; - const e = ed.find(x => (x.month || "") === selMonth) || {}; - const rev = Math.round(parseFloat(b.rev || 0) || 0); - const payment = Math.round(parseFloat(b.payment || 0) || 0); - const gross = Math.round(parseFloat(b.gross || 0) || 0); - const cost = Math.round(parseFloat(e.cost || 0) || 0); - const paid = Math.round(parseFloat(e.paid || 0) || 0); + let rev = 0, payment = 0, gross = 0, cost = 0, paid = 0; + if (isAllMonth) { + bd.forEach(x => { if ((x.month||"").substring(5) === monthSuffix) { rev += parseFloat(x.rev||0)||0; gross += parseFloat(x.gross||0)||0; payment += parseFloat(x.payment||0)||0; } }); + ed.forEach(x => { if ((x.month||"").substring(5) === monthSuffix) { cost += parseFloat(x.cost||0)||0; paid += parseFloat(x.paid||0)||0; } }); + rev = Math.round(rev); payment = Math.round(payment); gross = Math.round(gross); cost = Math.round(cost); paid = Math.round(paid); + } else { + const b = bd.find(x => (x.month || "") === selMonth) || {}; + const e = ed.find(x => (x.month || "") === selMonth) || {}; + rev = Math.round(parseFloat(b.rev || 0) || 0); + payment = Math.round(parseFloat(b.payment || 0) || 0); + gross = Math.round(parseFloat(b.gross || 0) || 0); + cost = Math.round(parseFloat(e.cost || 0) || 0); + paid = Math.round(parseFloat(e.paid || 0) || 0); + } if (!rev && !payment && !cost && !paid && !gross) return; dataItems.push({ pf: pf, customer_name: pf.customer_name || '', sign_amount: pf.sign_amount || 0, sign_month: pf.sign_month || '', @@ -408,8 +431,12 @@ function renderFinance() { sumRev += d.rev; sumPay += d.payment; sumCost += d.cost; sumPaid += d.paid; sumGross += d.gross; rows.push(isUnsigned ? tdRowUnsigned(d.pf, d.cost, d.gross) : tdRow(d.pf, d.rev, d.payment, d.cost, d.paid, d.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.sign_month || "").substring(0, 7) === selMonth).length; + const signTotal = isAllMonth + ? Math.round(allPfs.filter(x => { const sm = x.sign_month || ""; return sm.length >= 7 && sm.substring(5) === monthSuffix; }).reduce((a, p) => a + (p.sign_amount || 0), 0)) + : Math.round(allPfs.filter(x => (x.sign_month || "").substring(0, 7) === selMonth).reduce((a, p) => a + (p.sign_amount || 0), 0)); + const signCnt = isAllMonth + ? allPfs.filter(x => { const sm = x.sign_month || ""; return sm.length >= 7 && sm.substring(5) === monthSuffix; }).length + : allPfs.filter(x => (x.sign_month || "").substring(0, 7) === selMonth).length; return renderView(rows, sumRev, sumPay, sumCost, sumPaid, sumGross, signTotal, signCnt, '该月份暂无数据'); } @@ -425,12 +452,12 @@ function renderFinance() { const qRange = qRanges[selQ]; const sumBudget = (pf, field) => { let total = 0; - try { JSON.parse(pf.budget_data || "[]").forEach(b => { const m = parseInt((b.month || "").substring(5)) || 0; const y = parseInt((b.month || "").substring(0, 4)) || 0; if (qRange.includes(m) && y === state.finYear) total += parseFloat(b[field] || 0); }); } catch (e) {} + try { JSON.parse(pf.budget_data || "[]").forEach(b => { const m = parseInt((b.month || "").substring(5)) || 0; const y = parseInt((b.month || "").substring(0, 4)) || 0; if (qRange.includes(m) && (y === state.finYear || state.finYear === 0)) total += parseFloat(b[field] || 0); }); } catch (e) {} return total; }; const sumExpense = (pf, field) => { let total = 0; - try { JSON.parse(pf.expense_data || "[]").forEach(e => { const m = parseInt((e.month || "").substring(5)) || 0; const y = parseInt((e.month || "").substring(0, 4)) || 0; if (qRange.includes(m) && y === state.finYear) total += parseFloat(e[field] || 0); }); } catch (e) {} + try { JSON.parse(pf.expense_data || "[]").forEach(e => { const m = parseInt((e.month || "").substring(5)) || 0; const y = parseInt((e.month || "").substring(0, 4)) || 0; if (qRange.includes(m) && (y === state.finYear || state.finYear === 0)) total += parseFloat(e[field] || 0); }); } catch (e) {} return total; }; var dataItems = []; @@ -459,25 +486,52 @@ function renderFinance() { sumRev += d.rev; sumPay += d.payment; sumCost += d.cost; sumPaid += d.paid; sumGross += d.gross; rows.push(isUnsigned ? tdRowUnsigned(d.pf, d.cost, d.gross) : tdRow(d.pf, d.rev, d.payment, d.cost, d.paid, d.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 => qRange.includes(parseInt((x.sign_month || "0").substring(5)) || 0)).length; + const signTotal = Math.round(allPfs.filter(x => { + const m = parseInt((x.sign_month || "0").substring(5)) || 0; + const qy = parseInt((x.sign_month || "0").substring(0, 4)) || 0; + return qRange.includes(m) && (qy === state.finYear || state.finYear === 0); + }).reduce((a, p) => a + (p.sign_amount || 0), 0)); + const signCnt = allPfs.filter(x => { + const m = parseInt((x.sign_month || "0").substring(5)) || 0; + const qy = parseInt((x.sign_month || "0").substring(0, 4)) || 0; + return qRange.includes(m) && (qy === state.finYear || state.finYear === 0); + }).length; return renderView(rows, sumRev, sumPay, sumCost, sumPaid, sumGross, signTotal, signCnt, '该季度暂无数据'); } // 总视图 - const calcTotals = (pf) => { + const calcTotals = (pf, year) => { let budget = []; try { budget = JSON.parse(pf.budget_data || "[]"); } catch (e) {} let expense = []; try { expense = JSON.parse(pf.expense_data || "[]"); } catch (e) {} let rev = 0, payment = 0, gross = 0; - budget.forEach(b => { rev += parseFloat(b.rev || 0) || 0; gross += parseFloat(b.gross || 0) || 0; payment += parseFloat(b.payment || 0) || 0; }); + budget.forEach(b => { + if (year !== 0) { + const by = parseInt((b.month || "").substring(0, 4)) || 0; + if (by && by !== year) return; + } + rev += parseFloat(b.rev || 0) || 0; gross += parseFloat(b.gross || 0) || 0; payment += parseFloat(b.payment || 0) || 0; + }); let cost = 0, paid = 0; - expense.forEach(e => { cost += parseFloat(e.cost || 0) || 0; paid += parseFloat(e.paid || 0) || 0; }); + expense.forEach(e => { + if (year !== 0) { + const ey = parseInt((e.month || "").substring(0, 4)) || 0; + if (ey && ey !== year) return; + } + cost += parseFloat(e.cost || 0) || 0; paid += parseFloat(e.paid || 0) || 0; + }); return { rev: Math.round(rev), payment: Math.round(payment), cost: Math.round(cost), paid: Math.round(paid), gross: Math.round(gross) }; }; const allPfs = pfs; - var dataItems = allPfs.map(pf => { - var t = calcTotals(pf); - return { + var dataItems = []; + allPfs.forEach(pf => { + var t = calcTotals(pf, state.finYear); + if (state.finYear !== 0) { + const sy = parseInt((pf.sign_month || "").substring(0, 4)) || 0; + const hasFinance = t.rev || t.payment || t.cost || t.paid || t.gross; + const hasSign = sy === state.finYear; + if (!hasFinance && !hasSign) return; + } + dataItems.push({ pf: pf, customer_name: pf.customer_name || '', sign_amount: pf.sign_amount || 0, sign_month: pf.sign_month || '', rev: t.rev, gross: t.gross, cost: t.cost, payment: t.payment, paid: t.paid, cashflow: t.payment - t.paid, @@ -485,7 +539,7 @@ function renderFinance() { gross_rate: t.rev && t.gross ? Math.round(t.gross / t.rev * 100) : 0, pay_rate: t.rev && t.payment ? Math.round(t.payment / t.rev * 100) : 0, paid_rate: t.cost && t.paid ? Math.round(t.paid / t.cost * 100) : 0, - }; + }); }); var sortK = state.finSort && state.finSort.key ? state.finSort.key.split('|')[1] : null; dataItems = sortData(dataItems, sortK); @@ -495,8 +549,16 @@ function renderFinance() { sumRev += d.rev; sumPay += d.payment; sumCost += d.cost; sumPaid += d.paid; sumGross += d.gross; rows.push(isUnsigned ? tdRowUnsigned(d.pf, d.cost, d.gross) : tdRow(d.pf, d.rev, d.payment, d.cost, d.paid, d.gross)); }); - const signTotal = Math.round(allPfs.reduce((a, p) => a + (p.sign_amount || 0), 0)); - const signCnt = allPfs.length; + const signTotal = Math.round(allPfs.filter(p => { + if (state.finYear === 0) return true; + const sy = parseInt((p.sign_month || "").substring(0, 4)) || 0; + return sy === state.finYear; + }).reduce((a, p) => a + (p.sign_amount || 0), 0)); + const signCnt = allPfs.filter(p => { + if (state.finYear === 0) return true; + const sy = parseInt((p.sign_month || "").substring(0, 4)) || 0; + return sy === state.finYear; + }).length; return renderView(rows, sumRev, sumPay, sumCost, sumPaid, sumGross, signTotal, signCnt, '暂无数据'); })()} `; @@ -992,7 +1054,9 @@ function renderFinanceOverview() { 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, m.payment_annual||0, (m.payment_annual||0) - (m.paid_annual||0)]; + const annualSums = { sign:0, revenue:0, gross:0, cost:0, paid:0, payment:0 }; + fm.forEach(x => { annualSums.sign += x.sign||0; annualSums.revenue += x.revenue||0; annualSums.gross += x.gross||0; annualSums.cost += x.cost||0; annualSums.paid += x.paid||0; annualSums.payment += x.payment||0; }); + const annualVals = [annualSums.sign, annualSums.revenue, annualSums.gross, annualSums.cost, annualSums.paid, annualSums.payment, annualSums.payment - annualSums.paid]; const fmFields = ['sign','revenue','gross','cost','paid','payment',null]; var thead = '指标年度累计'; for (var mi = 0; mi < 12; mi++) { thead += '' + mLbl[mi] + ''; } @@ -1022,6 +1086,8 @@ function renderFinanceOverview() { if ((finExp[ei].status || '') === '暂不计入') continue; var em = (finExp[ei].expense_month || '').substring(0, 7); var mi2 = parseInt(em.substring(5)) - 1; + var emYear = parseInt(em.substring(0, 4)) || 0; + if (state.finYear !== 0 && emYear && emYear !== state.finYear) continue; 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]; @@ -1057,6 +1123,8 @@ function renderFinanceOverview() { if ((finExp[epi].status || '') === '暂不计入') continue; var epm = (finExp[epi].expense_month || '').substring(0, 7); var epmi = parseInt(epm.substring(5)) - 1; + var epmYear = parseInt(epm.substring(0, 4)) || 0; + if (state.finYear !== 0 && epmYear && epmYear !== state.finYear) continue; 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]; @@ -1083,14 +1151,15 @@ function renderFinanceOverview() { } var cfCard = card('

部门现金流 部门现金流=所有项目产生的现金流 - 部门一级因费用产生的现金流出或流入

' + deptThead + '' + cfTbody + '
', 'p-4'); - document.querySelector("#financeOverview").innerHTML = '
' + + const yearSelectOv = `
📅 选择年份:
`; + document.querySelector("#financeOverview").innerHTML = yearSelectOv + '
' + deptCard + cfCard + card('

项目经营详情 所有项目产生的签单,确收,毛利,回款,当期成本,当期流出,现金流

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

月度签约趋势

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

月度确收与毛利

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

月度回款与支出

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

月度签约趋势

' + (state.finYear===0 ? '所有年份' : state.finYear) + '
', 'p-4') + + card('

月度确收与毛利

' + (state.finYear===0 ? '所有年份' : state.finYear) + '
', 'p-4') + + card('

月度回款与支出

' + (state.finYear===0 ? '所有年份' : state.finYear) + '
', 'p-4') + '
'; if (window.lucide) window.lucide.createIcons(); @@ -1111,7 +1180,9 @@ function renderFinanceQuarterlyOverview() { // 财务概览: 7 rows var LABELS = ['合同金额','确收金额','确收毛利','成本','支出','回款','项目现金流']; - var 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)]; + var annualSums = { sign:0, revenue:0, gross:0, cost:0, paid:0, payment:0 }; + fm.forEach(x => { annualSums.sign += x.sign||0; annualSums.revenue += x.revenue||0; annualSums.gross += x.gross||0; annualSums.cost += x.cost||0; annualSums.paid += x.paid||0; annualSums.payment += x.payment||0; }); + var annualVals = [annualSums.sign, annualSums.revenue, annualSums.gross, annualSums.cost, annualSums.paid, annualSums.payment, annualSums.payment - annualSums.paid]; var fmFields = ['sign','revenue','gross','cost','paid','payment',null]; var qData = [[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0]]; for (var qi = 0; qi < 4; qi++) { @@ -1156,11 +1227,13 @@ function renderFinanceQuarterlyOverview() { if ((finExp[ei].status || '') === '暂不计入') continue; var em = (finExp[ei].expense_month || '').substring(0, 7); var emi = parseInt(em.substring(5)) - 1; + var emYear = parseInt(em.substring(0, 4)) || 0; + if (state.finYear !== 0 && emYear && emYear !== state.finYear) continue; if (emi >= 0 && emi < 12) { var eqi = Math.floor(emi / 3); expQ[eqi] += parseFloat(finExp[ei].amount || 0); } } var expAnnual = 0; for (var ei = 0; ei < 4; ei++) expAnnual += expQ[ei]; var grossQ = [qData[2][0], qData[2][1], qData[2][2], qData[2][3]]; - var grossAnnual = m.gross_annual || 0; + var grossAnnual = 0; for (var ei = 0; ei < fm.length; ei++) grossAnnual += (fm[ei] ? fm[ei].gross : 0); var deptThead = '指标年度累计'; for (var qi = 0; qi < 4; qi++) { deptThead += 'Q' + (qi+1) + ''; if (qi > 0) deptThead += '环比'; } deptThead += ''; @@ -1186,12 +1259,14 @@ function renderFinanceQuarterlyOverview() { // 业务线现金流: quarterly var cfQ = [qData[5][0], qData[5][1], qData[5][2], qData[5][3]]; - var cfAnnual = m.payment_annual || 0; + var cfAnnual = 0; for (var ei = 0; ei < fm.length; ei++) cfAnnual += (fm[ei] ? fm[ei].payment : 0); var expPaidQ = [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 emi = parseInt(em.substring(5)) - 1; + var emYear = parseInt(em.substring(0, 4)) || 0; + if (state.finYear !== 0 && emYear && emYear !== state.finYear) continue; if (emi >= 0 && emi < 12) { var eqi = Math.floor(emi / 3); expPaidQ[eqi] += parseFloat(finExp[ei].incurred_amount || 0); } } var expPaidAnnual = 0; for (var ei = 0; ei < 4; ei++) expPaidAnnual += expPaidQ[ei]; @@ -1217,7 +1292,8 @@ function renderFinanceQuarterlyOverview() { cfTbody += rowHtml + ''; } - document.querySelector("#financeQuarterlyOverview").innerHTML = '
' + + const yearSelectQ = `
📅 选择年份:
`; + document.querySelector("#financeQuarterlyOverview").innerHTML = yearSelectQ + '
' + card('

部门损益情况 部门利润 = 所有项目产生的毛利 - 部门 1 级产生的人力成本,研发成本,差旅和其他支出

' + deptThead + '' + deptTbody + '
', 'p-4') + card('

部门现金流 部门现金流=所有项目产生的现金流 - 部门一级因费用产生的现金流出或流入

' + deptThead + '' + cfTbody + '
', 'p-4') + card('

项目经营详情 所有项目产生的签单,确收,毛利,回款,当期成本,当期流出,现金流

' + thead + '' + tbody + '
', 'p-4') + diff --git a/static/modules/plan.js b/static/modules/plan.js index e530739..96b1ec0 100644 --- a/static/modules/plan.js +++ b/static/modules/plan.js @@ -1036,10 +1036,10 @@ window.deletePlanItem = async () => { // 从 planFinances 计算月度数据 -function computePlanMonthly(planFinances) { +function computePlanMonthly(planFinances, year) { var pfs = planFinances || []; var months = []; - var year = new Date().getFullYear(); + year = year || new Date().getFullYear(); for (var mi = 1; mi <= 12; mi++) { months.push(year + '-' + String(mi).padStart(2, '0')); } var data = []; for (var i = 0; i < 12; i++) { @@ -1062,7 +1062,7 @@ function computePlanMonthly(planFinances) { // 总览渲染 function renderPlanOverview() { const planFinances = state.data.planFinances || []; - const fm = computePlanMonthly(planFinances); + const fm = computePlanMonthly(planFinances, state.planYear); const moneyIntL = (v) => Math.round(Number(v || 0)).toLocaleString("zh-CN"); const moneyWan = (v) => { const n = Number(v || 0); @@ -1180,9 +1180,9 @@ function renderPlanOverview() { cfCard + card('

项目经营详情 所有项目产生的签单,确收,毛利,回款,成本,支出,现金流

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

月度签约趋势

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

月度确收与毛利

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

月度回款与已付

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

月度签约趋势

' + state.planYear + '
', 'p-4') + + card('

月度确收与毛利

' + state.planYear + '
', 'p-4') + + card('

月度回款与已付

' + state.planYear + '
', 'p-4') + '
'; if (window.lucide) window.lucide.createIcons(); @@ -1195,7 +1195,7 @@ function renderPlanOverview() { function renderPlanQuarterlyOverview() { const planFinances = state.data.planFinances || []; - const fm = computePlanMonthly(planFinances); + const fm = computePlanMonthly(planFinances, state.planYear); const moneyIntL = (v) => Math.round(Number(v || 0)).toLocaleString("zh-CN"); const card = (body, cls) => '
' + body + '
'; var annualSums2 = { sign: 0, revenue: 0, gross: 0, cost: 0, payment: 0, paid: 0 };