Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7bf679255 | ||
|
|
c5be3ce134 | ||
|
|
14368e83d0 | ||
|
|
63ddffb12a |
@@ -1093,6 +1093,7 @@ def _aggregate_monthly_from_json(conn, table, tenants):
|
||||
_year = date.today().year
|
||||
months = [f"{_year}-{m:02d}" for m in range(1, 13)]
|
||||
rev, gross, payment, cost, paid = [0]*12, [0]*12, [0]*12, [0]*12, [0]*12
|
||||
sign_monthly = [0]*12
|
||||
sign_total = 0
|
||||
sign_cnt = 0
|
||||
|
||||
@@ -1103,6 +1104,10 @@ def _aggregate_monthly_from_json(conn, table, tenants):
|
||||
if sm.startswith(str(_year)):
|
||||
sign_total += float(pf.get("sign_amount") or 0)
|
||||
sign_cnt += 1
|
||||
# 签单按月拆分
|
||||
sm7 = sm[:7]
|
||||
if sm7 in months:
|
||||
sign_monthly[months.index(sm7)] += float(pf.get("sign_amount") or 0)
|
||||
try:
|
||||
bd = json.loads(pf.get("budget_data") or "[]")
|
||||
except:
|
||||
@@ -1130,6 +1135,7 @@ def _aggregate_monthly_from_json(conn, table, tenants):
|
||||
"rev": rev, "gross": gross, "payment": payment, "cost": cost, "paid": paid,
|
||||
"cashflow": [payment[i] - paid[i] for i in range(12)],
|
||||
"sign_total": sign_total, "sign_cnt": sign_cnt,
|
||||
"sign_monthly": sign_monthly,
|
||||
}
|
||||
|
||||
|
||||
@@ -1181,6 +1187,7 @@ def _build_overview_result(agg, exp_amount, exp_incurred):
|
||||
# 项目经营详情
|
||||
project = {
|
||||
"sign": agg["sign_total"],
|
||||
"sign_monthly": agg["sign_monthly"],
|
||||
"revenue": rev,
|
||||
"gross": gross,
|
||||
"cost": cost,
|
||||
@@ -1218,7 +1225,7 @@ def overview_plan_quarterly():
|
||||
conn.close()
|
||||
# Convert monthly to quarterly
|
||||
result = _build_overview_result(agg, exp_amount, exp_incurred)
|
||||
for key in ["revenue","gross","payment","cost","paid","cashflow"]:
|
||||
for key in ["sign_monthly","revenue","gross","payment","cost","paid","cashflow"]:
|
||||
result["project"][key] = _quarterly_from_monthly(result["project"][key])
|
||||
result["dept"]["project_gross"] = _quarterly_from_monthly(result["dept"]["project_gross"])
|
||||
result["dept"]["dept_expense"] = _quarterly_from_monthly(result["dept"]["dept_expense"])
|
||||
@@ -1247,7 +1254,7 @@ def overview_finance_quarterly():
|
||||
exp_amount, exp_incurred = _aggregate_expenses(conn, "expense_records", OVERVIEW_TENANTS)
|
||||
conn.close()
|
||||
result = _build_overview_result(agg, exp_amount, exp_incurred)
|
||||
for key in ["revenue","gross","payment","cost","paid","cashflow"]:
|
||||
for key in ["sign_monthly","revenue","gross","payment","cost","paid","cashflow"]:
|
||||
result["project"][key] = _quarterly_from_monthly(result["project"][key])
|
||||
result["dept"]["project_gross"] = _quarterly_from_monthly(result["dept"]["project_gross"])
|
||||
result["dept"]["dept_expense"] = _quarterly_from_monthly(result["dept"]["dept_expense"])
|
||||
|
||||
@@ -253,18 +253,6 @@ function renderFinance() {
|
||||
return `<div id="financeExpense"></div>`;
|
||||
}
|
||||
const isUnsigned = false;
|
||||
const METRIC_CARDS = [
|
||||
{ label: '签约金额', value: ctx => moneyWan(ctx.signTotal), color: 'text-slate-700' },
|
||||
{ label: '已确收', hideUnsigned: true, value: ctx => moneyWan(ctx.sumRev), color: 'text-blue-700' },
|
||||
{ label: '待回款', hideUnsigned: true, value: ctx => { const v = ctx.sumRev - ctx.sumPay; return { val: v < 0 ? '超额回款 ' + moneyWan(Math.abs(v)) : moneyWan(v), cls: v > 0 ? 'text-red-600' : v < 0 ? 'text-green-600' : 'text-slate-400' }; }, color: null },
|
||||
{ label: '毛利', value: ctx => moneyWan(ctx.sumGross), color: 'text-green-700' },
|
||||
{ label: '成本(不含平台费用)', value: ctx => moneyWan(ctx.sumCost), color: 'text-rose-700' },
|
||||
{ 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: moneyWan(v), cls: v >= 0 ? 'text-green-600' : 'text-red-600' }; }, color: null },
|
||||
{ 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' },
|
||||
].filter(c => true);
|
||||
|
||||
// 列排序(默认按确收金额从高到低)
|
||||
if (!state.finSort) state.finSort = { key: 'sig|rev', asc: false };
|
||||
@@ -308,22 +296,31 @@ function renderFinance() {
|
||||
];
|
||||
|
||||
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];
|
||||
});
|
||||
const receivable = sumRev - sumPay;
|
||||
const payable = sumCost - sumPaid;
|
||||
const cashflow = sumPay - sumPaid;
|
||||
const netAsset = receivable - payable;
|
||||
const m = moneyWan;
|
||||
const rows_data = [
|
||||
['签约金额', m(signTotal), '合同签约总额'],
|
||||
['确收金额', m(sumRev), '已确认收入'],
|
||||
['毛利', m(sumGross), '确收毛利'],
|
||||
['应收', m(receivable), '确收−回款'],
|
||||
['应付', m(payable), '成本−支出'],
|
||||
['现金流', m(cashflow), '回款−支出'],
|
||||
['项目净资产', m(netAsset), '应收−应付'],
|
||||
];
|
||||
const fs = '<div class="grid grid-cols-4 gap-1.5 py-2">' + rows_data.map(r =>
|
||||
'<div class="card px-2 py-1.5 text-center"><div class="text-xs text-slate-400">' + r[0] + '</div><div class="text-sm font-semibold text-slate-800">' + r[1] + '</div></div>').join('') + '</div>';
|
||||
const thead = isUnsigned ? buildThead(UNSIGNED_COLS, 'uns') : buildThead(SIGNED_COLS, 'sig');
|
||||
const theadCols = isUnsigned ? 6 : 12;
|
||||
const theadCols = isUnsigned ? 6 : 14;
|
||||
const tbody = rows.length ? `<tbody style="line-height:1.37">${rows.join("")}</tbody>` : `<tr><td colspan="${theadCols}" class="p-6 text-center text-slate-400">${emptyText}</td></tr>`;
|
||||
|
||||
const cardGrid = isUnsigned ? 'grid-cols-4' : 'grid-cols-5';
|
||||
const subtitle = isUnsigned
|
||||
? '合同 → 毛利 → 成本 → 项目利润'
|
||||
: '合同 → 确收 → 毛利 → 成本 → 项目利润 → 回款 → 支出 → 现金流 → 执行率 → 毛利率 → 回款率 → 付款率';
|
||||
return `<div class="grid ${cardGrid} gap-1.5 mb-1.5">${cards.map(([l, v, c]) => '<div class="metric-card px-3 py-1.5"><span class="text-xs text-slate-500">' + l + '</span> <strong class="text-base ' + c + '">' + v + '</strong></div>').join("")}</div>` +
|
||||
`${isUnsigned ? '' : `<div class="mb-1.5">${card(`<div class="flex justify-between items-center"><div class="flex items-center gap-2">${finHeaderBase}</div>${finAddBtn}</div>`, "py-2 px-4")}</div>`}` +
|
||||
card(`<h3 class="text-sm font-bold text-slate-700">项目财务明细</h3><p class="text-xs text-slate-400 mb-3">${subtitle}</p><div class="overflow-x-auto"><table class="w-full text-sm">${thead}${tbody}</table></div>`, "p-4");
|
||||
: '合同 → 确收 → 毛利 → 成本 → 项目利润 → 回款 → 支出 → 现金流';
|
||||
return `${isUnsigned ? '' : `<div class="mb-1.5">${card(`<div class="flex justify-between items-center"><div class="flex items-center gap-2">${finHeaderBase}</div>${finAddBtn}</div>`, "py-2 px-4")}</div>`}` +
|
||||
card(fs +
|
||||
`<h3 class="text-sm font-bold text-slate-700 mt-2">项目财务明细</h3><p class="text-xs text-slate-400 mb-3">${subtitle}</p><div class="overflow-x-auto"><table class="w-full text-sm">${thead}${tbody}</table></div>`, "p-4");
|
||||
};
|
||||
|
||||
const fmtNoUnit = (v) => v ? `<span class="font-medium">${Number(v||0).toLocaleString('zh-CN')}</span>` : '<span class="text-slate-300">—</span>';
|
||||
@@ -1248,3 +1245,10 @@ function renderOverviewCharts(data) {
|
||||
state.finChart4 = new Chart(c4, Object.assign({}, iconf, { data: { labels: labels, datasets: [{ label: "利润", data: data.map(function(x) { return x.gross || 0; }), borderColor: "#6366f1", backgroundColor: "rgba(99,102,241,0.06)", fill: true, tension: 0.3 }] } }));
|
||||
}
|
||||
};
|
||||
|
||||
// ====== 筛选器控制函数 ======
|
||||
window.switchFinFilter = (f) => { state.finFilter = f; renderFinance(); };
|
||||
window.setFinView = (v) => { state.finView = v; renderFinance(); };
|
||||
window.setFinYear = (y) => { state.finYear = parseInt(y); renderFinance(); };
|
||||
window.setFinQuarter = (q) => { state.finView = 'quarterly'; state.finQuarter = q; renderFinance(); };
|
||||
window.setFinMonth = (m) => { state.finView = 'monthly'; state.finMonth = m; renderFinance(); };
|
||||
|
||||
@@ -57,11 +57,11 @@ function renderFinanceProjectMatrixData(data, container) {
|
||||
var vBtns = financeBtnBar('project_matrix');
|
||||
var cards = '';
|
||||
for (var mi = 11; mi >= 0; mi--) {
|
||||
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">月度合计</th>';
|
||||
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap" style="min-width:100px">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">月度合计</th>';
|
||||
for (var ti = 0; ti < tenants.length; ti++) { th += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + sn[ti] + '</th>'; }
|
||||
th += '</tr>';
|
||||
if (isAllZero(data, tenants, keys.filter(function(k) { return k !== 'sign'; }), mi)) continue;
|
||||
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">月度合计</th>';
|
||||
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap" style="min-width:100px">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">月度合计</th>';
|
||||
for (var ti = 0; ti < tenants.length; ti++) { th += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + sn[ti] + '</th>'; }
|
||||
th += '</tr>';
|
||||
|
||||
@@ -87,7 +87,7 @@ function financeProjectMatrixTbody(data, tenants, mi, keys, labels, mf) {
|
||||
var tCls = 'text-slate-700';
|
||||
if (ri === 2) tCls = 'text-green-600';
|
||||
if (ri === keys.length - 1) tCls = ts >= 0 ? 'text-green-600' : 'text-red-600';
|
||||
var rh = '<tr class="border-b border-slate-100 last:border-0"><td class="py-1.5 text-left ' + rowCls + ' text-slate-700 whitespace-nowrap">' + labels[ri] + '</td><td class="py-1.5 text-right font-semibold text-sm whitespace-nowrap ' + tCls + '">' + mf(ts) + '</td>';
|
||||
var rh = '<tr class="border-b border-slate-100 last:border-0"><td class="py-1.5 text-left ' + rowCls + ' text-slate-700 whitespace-nowrap" style="min-width:100px">' + labels[ri] + '</td><td class="py-1.5 text-right font-semibold text-sm whitespace-nowrap ' + tCls + '">' + mf(ts) + '</td>';
|
||||
for (var ti = 0; ti < tenants.length; ti++) {
|
||||
var cls = 'text-slate-700';
|
||||
if (ri === 2) cls = 'text-green-600';
|
||||
@@ -118,7 +118,7 @@ function renderOverviewFinanceData(data, view) {
|
||||
btns.push('<button onclick="setOverviewFinanceView(\'project_matrix\')" class="finance-tab' + (view === 'project_matrix' ? ' active' : '') + '" style="font-size:14px;font-weight:600">项目经营详细报表</button>');
|
||||
var viewBtns = '<div class="finance-tabs" style="padding:0;border-bottom:1px solid #e2e8f0;margin-bottom:12px">' + btns.join('') + '</div>';
|
||||
|
||||
var deptThead = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">年度累计</th>';
|
||||
var deptThead = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap" style="min-width:100px">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">年度累计</th>';
|
||||
for (var hi = 0; hi < cols; hi++) { deptThead += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + headers[hi] + '</th>'; }
|
||||
deptThead += '</tr>';
|
||||
|
||||
@@ -130,7 +130,7 @@ function renderOverviewFinanceData(data, view) {
|
||||
var deptTbody = '';
|
||||
for (var dri = 0; dri < 3; dri++) {
|
||||
var dr = deptRows[dri], rowCls = dri === 2 ? 'font-semibold' : '';
|
||||
var rowHtml = '<tr class="border-b border-slate-100 last:border-0"><td class="py-1.5 text-left ' + rowCls + ' text-slate-700 whitespace-nowrap">' + dr.label + '</td>';
|
||||
var rowHtml = '<tr class="border-b border-slate-100 last:border-0"><td class="py-1.5 text-left ' + rowCls + ' text-slate-700 whitespace-nowrap" style="min-width:100px">' + dr.label + '</td>';
|
||||
var annualCls = dri === 2 ? (dr.annual >= 0 ? 'text-green-600' : 'text-red-600') : 'text-slate-700';
|
||||
rowHtml += '<td class="py-1.5 text-right ' + rowCls + ' ' + annualCls + ' whitespace-nowrap">' + moneyIntL(dr.annual) + '</td>';
|
||||
for (var i = 0; i < cols; i++) {
|
||||
@@ -149,7 +149,7 @@ function renderOverviewFinanceData(data, view) {
|
||||
var cfTbody = '';
|
||||
for (var cfri = 0; cfri < 3; cfri++) {
|
||||
var cfr = cfRows[cfri], cfRowCls = cfri === 2 ? 'font-semibold' : '';
|
||||
var rowHtml = '<tr class="border-b border-slate-100 last:border-0"><td class="py-1.5 text-left ' + cfRowCls + ' text-slate-700 whitespace-nowrap">' + cfr.label + '</td>';
|
||||
var rowHtml = '<tr class="border-b border-slate-100 last:border-0"><td class="py-1.5 text-left ' + cfRowCls + ' text-slate-700 whitespace-nowrap" style="min-width:100px">' + cfr.label + '</td>';
|
||||
var cAnnualCls = cfri === 2 ? (cfr.annual >= 0 ? 'text-green-600' : 'text-red-600') : 'text-slate-700';
|
||||
rowHtml += '<td class="py-1.5 text-right ' + cfRowCls + ' ' + cAnnualCls + ' whitespace-nowrap">' + moneyIntL(cfr.annual) + '</td>';
|
||||
for (var i = 0; i < cols; i++) {
|
||||
@@ -161,17 +161,17 @@ function renderOverviewFinanceData(data, view) {
|
||||
var cfCard = card('<h3 class="text-base font-bold text-slate-700 mb-3">部门现金流 <span class="text-sm text-slate-500 font-normal ml-3">部门现金流=所有项目产生的现金流 - 部门一级因费用产生的现金流出或流入</span></h3><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + deptThead + '</thead><tbody>' + cfTbody + '</tbody></table></div>', 'p-4');
|
||||
|
||||
var LABELS = ['合同金额','确收金额','确收毛利','成本','支出','回款','项目现金流'];
|
||||
var projVals = [p.revenue, p.gross, p.cost, p.paid, p.payment, p.cashflow];
|
||||
var projVals = [p.sign_monthly || [], p.revenue, p.gross, p.cost, p.paid, p.payment, p.cashflow];
|
||||
var projAnnual = p.annual;
|
||||
var projThead = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">年度累计</th>';
|
||||
var projThead = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap" style="min-width:100px">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">年度累计</th>';
|
||||
for (var phi = 0; phi < cols; phi++) { projThead += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + headers[phi] + '</th>'; }
|
||||
projThead += '</tr>';
|
||||
var projTbody = '';
|
||||
for (var ri = 0; ri < 7; ri++) {
|
||||
var rowHtml = '<tr class="border-b border-slate-100 last:border-0"><td class="py-1.5 text-left font-semibold text-slate-700 whitespace-nowrap">' + LABELS[ri] + '</td>';
|
||||
rowHtml += '<td class="py-1.5 text-right font-semibold text-slate-800 whitespace-nowrap">' + (ri === 0 ? moneyIntL(p.sign) : moneyIntL(projAnnual[ri])) + '</td>';
|
||||
var rowHtml = '<tr class="border-b border-slate-100 last:border-0"><td class="py-1.5 text-left font-semibold text-slate-700 whitespace-nowrap" style="min-width:100px">' + LABELS[ri] + '</td>';
|
||||
rowHtml += '<td class="py-1.5 text-right font-semibold text-slate-800 whitespace-nowrap">' + moneyIntL(projAnnual[ri]) + '</td>';
|
||||
for (var i = 0; i < cols; i++) {
|
||||
var val = ri === 0 ? 0 : (projVals[ri - 1] ? projVals[ri - 1][i] : 0);
|
||||
var val = projVals[ri] ? (projVals[ri][i] || 0) : 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';
|
||||
@@ -206,7 +206,7 @@ function renderFinanceDeptMatrixData(data, container) {
|
||||
var cards = '';
|
||||
for (var mi = 11; mi >= 0; mi--) {
|
||||
if (isAllZero(data, tenants, ['project_gross','dept_expense','profit'], mi)) continue;
|
||||
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">月度合计</th>';
|
||||
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap" style="min-width:100px">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">月度合计</th>';
|
||||
for (var ti = 0; ti < tenants.length; ti++) { th += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + sn[ti] + '</th>'; }
|
||||
th += '</tr>';
|
||||
var tb = financeMatrixTbody(data, tenants, mi, ['project_gross','dept_expense','profit'], ['项目毛利','部门费用','利润'], mf);
|
||||
@@ -235,7 +235,7 @@ function renderFinanceCfMatrixData(data, container) {
|
||||
var cards = '';
|
||||
for (var mi = 11; mi >= 0; mi--) {
|
||||
if (isAllZero(data, tenants, ['project_cf','dept_cf','bl_cf'], mi)) continue;
|
||||
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">月度合计</th>';
|
||||
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap" style="min-width:100px">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">月度合计</th>';
|
||||
for (var ti = 0; ti < tenants.length; ti++) { th += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + sn[ti] + '</th>'; }
|
||||
th += '</tr>';
|
||||
var tb = financeMatrixTbody(data, tenants, mi, ['project_cf','dept_cf','bl_cf'], ['项目现金流','部门现金流','经营现金流'], mf);
|
||||
@@ -269,7 +269,7 @@ function financeMatrixTbody(data, tenants, mi, keys, labels, mf) {
|
||||
vals.push(v);
|
||||
}
|
||||
var tCls = ts >= 0 ? 'text-green-600' : 'text-red-600';
|
||||
var rh = '<tr class="border-b border-slate-100 last:border-0"><td class="py-1.5 text-left ' + rowCls + ' text-slate-700 whitespace-nowrap">' + labels[ri] + '</td><td class="py-1.5 text-right font-semibold text-sm whitespace-nowrap ' + tCls + '">' + mf(ts) + '</td>';
|
||||
var rh = '<tr class="border-b border-slate-100 last:border-0"><td class="py-1.5 text-left ' + rowCls + ' text-slate-700 whitespace-nowrap" style="min-width:100px">' + labels[ri] + '</td><td class="py-1.5 text-right font-semibold text-sm whitespace-nowrap ' + tCls + '">' + mf(ts) + '</td>';
|
||||
for (var ti = 0; ti < tenants.length; ti++) {
|
||||
var cls = vals[ti] >= 0 ? 'text-green-600' : 'text-red-600';
|
||||
rh += '<td class="py-1.5 text-right text-sm whitespace-nowrap ' + cls + '">' + mf(vals[ti]) + '</td>';
|
||||
|
||||
@@ -45,12 +45,12 @@ function renderProjectMatrixData(data, container) {
|
||||
var vBtns = btnBar('project_matrix');
|
||||
var cards = '';
|
||||
for (var mi = 11; mi >= 0; mi--) {
|
||||
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">月度合计</th>';
|
||||
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap" style="min-width:100px">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">月度合计</th>';
|
||||
for (var ti = 0; ti < tenants.length; ti++) { th += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + sn[ti] + '</th>'; }
|
||||
th += '</tr>';
|
||||
|
||||
if (isAllZero(data, tenants, keys.filter(function(k) { return k !== 'sign'; }), mi)) continue;
|
||||
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">月度合计</th>';
|
||||
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap" style="min-width:100px">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">月度合计</th>';
|
||||
for (var ti = 0; ti < tenants.length; ti++) { th += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + sn[ti] + '</th>'; }
|
||||
th += '</tr>';
|
||||
|
||||
@@ -76,7 +76,7 @@ function projectMatrixTbody(data, tenants, mi, keys, labels, mf) {
|
||||
var tCls = 'text-slate-700';
|
||||
if (ri === 2) tCls = 'text-green-600';
|
||||
if (ri === keys.length - 1) tCls = ts >= 0 ? 'text-green-600' : 'text-red-600';
|
||||
var rh = '<tr class="border-b border-slate-100 last:border-0"><td class="py-1.5 text-left ' + rowCls + ' text-slate-700 whitespace-nowrap">' + labels[ri] + '</td><td class="py-1.5 text-right font-semibold text-sm whitespace-nowrap ' + tCls + '">' + mf(ts) + '</td>';
|
||||
var rh = '<tr class="border-b border-slate-100 last:border-0"><td class="py-1.5 text-left ' + rowCls + ' text-slate-700 whitespace-nowrap" style="min-width:100px">' + labels[ri] + '</td><td class="py-1.5 text-right font-semibold text-sm whitespace-nowrap ' + tCls + '">' + mf(ts) + '</td>';
|
||||
for (var ti = 0; ti < tenants.length; ti++) {
|
||||
var cls = 'text-slate-700';
|
||||
if (ri === 2) cls = 'text-green-600';
|
||||
@@ -113,7 +113,7 @@ function renderOverviewPlanData(data, view) {
|
||||
var viewBtns = '<div class="finance-tabs" style="padding:0;border-bottom:1px solid #e2e8f0;margin-bottom:12px">' + btns.join('') + '</div>';
|
||||
|
||||
// ====== 部门损益情况 ======
|
||||
var deptThead = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">年度累计</th>';
|
||||
var deptThead = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap" style="min-width:100px">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">年度累计</th>';
|
||||
for (var hi = 0; hi < cols; hi++) { deptThead += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + headers[hi] + '</th>'; }
|
||||
deptThead += '</tr>';
|
||||
|
||||
@@ -126,7 +126,7 @@ function renderOverviewPlanData(data, view) {
|
||||
for (var dri = 0; dri < 3; dri++) {
|
||||
var dr = deptRows[dri];
|
||||
var rowCls = dri === 2 ? 'font-semibold' : '';
|
||||
var rowHtml = '<tr class="border-b border-slate-100 last:border-0"><td class="py-1.5 text-left ' + rowCls + ' text-slate-700 whitespace-nowrap">' + dr.label + '</td>';
|
||||
var rowHtml = '<tr class="border-b border-slate-100 last:border-0"><td class="py-1.5 text-left ' + rowCls + ' text-slate-700 whitespace-nowrap" style="min-width:100px">' + dr.label + '</td>';
|
||||
var annualCls = dri === 2 ? (dr.annual >= 0 ? 'text-green-600' : 'text-red-600') : 'text-slate-700';
|
||||
rowHtml += '<td class="py-1.5 text-right ' + rowCls + ' ' + annualCls + ' whitespace-nowrap">' + moneyIntL(dr.annual) + '</td>';
|
||||
for (var i = 0; i < cols; i++) {
|
||||
@@ -147,7 +147,7 @@ function renderOverviewPlanData(data, view) {
|
||||
for (var cfri = 0; cfri < 3; cfri++) {
|
||||
var cfr = cfRows[cfri];
|
||||
var cfRowCls = cfri === 2 ? 'font-semibold' : '';
|
||||
var rowHtml = '<tr class="border-b border-slate-100 last:border-0"><td class="py-1.5 text-left ' + cfRowCls + ' text-slate-700 whitespace-nowrap">' + cfr.label + '</td>';
|
||||
var rowHtml = '<tr class="border-b border-slate-100 last:border-0"><td class="py-1.5 text-left ' + cfRowCls + ' text-slate-700 whitespace-nowrap" style="min-width:100px">' + cfr.label + '</td>';
|
||||
var cAnnualCls = cfri === 2 ? (cfr.annual >= 0 ? 'text-green-600' : 'text-red-600') : 'text-slate-700';
|
||||
rowHtml += '<td class="py-1.5 text-right ' + cfRowCls + ' ' + cAnnualCls + ' whitespace-nowrap">' + moneyIntL(cfr.annual) + '</td>';
|
||||
for (var i = 0; i < cols; i++) {
|
||||
@@ -160,19 +160,19 @@ function renderOverviewPlanData(data, view) {
|
||||
|
||||
// ====== 项目经营详情 ======
|
||||
var LABELS = ['合同金额','确收金额','确收毛利','成本','支出','回款','项目现金流'];
|
||||
var projVals = [p.revenue, p.gross, p.cost, p.paid, p.payment, p.cashflow];
|
||||
var projVals = [p.sign_monthly || [], p.revenue, p.gross, p.cost, p.paid, p.payment, p.cashflow];
|
||||
var projAnnual = p.annual;
|
||||
|
||||
var projThead = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">年度累计</th>';
|
||||
var projThead = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap" style="min-width:100px">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">年度累计</th>';
|
||||
for (var phi = 0; phi < cols; phi++) { projThead += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + headers[phi] + '</th>'; }
|
||||
projThead += '</tr>';
|
||||
|
||||
var projTbody = '';
|
||||
for (var ri = 0; ri < 7; ri++) {
|
||||
var rowHtml = '<tr class="border-b border-slate-100 last:border-0"><td class="py-1.5 text-left font-semibold text-slate-700 whitespace-nowrap">' + LABELS[ri] + '</td>';
|
||||
rowHtml += '<td class="py-1.5 text-right font-semibold text-slate-800 whitespace-nowrap">' + (ri === 0 ? moneyIntL(p.sign) : moneyIntL(projAnnual[ri])) + '</td>';
|
||||
var rowHtml = '<tr class="border-b border-slate-100 last:border-0"><td class="py-1.5 text-left font-semibold text-slate-700 whitespace-nowrap" style="min-width:100px">' + LABELS[ri] + '</td>';
|
||||
rowHtml += '<td class="py-1.5 text-right font-semibold text-slate-800 whitespace-nowrap">' + moneyIntL(projAnnual[ri]) + '</td>';
|
||||
for (var i = 0; i < cols; i++) {
|
||||
var val = ri === 0 ? 0 : (projVals[ri - 1] ? projVals[ri - 1][i] : 0);
|
||||
var val = projVals[ri] ? (projVals[ri][i] || 0) : 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';
|
||||
@@ -208,7 +208,7 @@ function renderDeptMatrixData(data, container) {
|
||||
var cards = '';
|
||||
for (var mi = 11; mi >= 0; mi--) {
|
||||
if (isAllZero(data, tenants, ['project_gross','dept_expense','profit'], mi)) continue;
|
||||
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">月度合计</th>';
|
||||
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap" style="min-width:100px">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">月度合计</th>';
|
||||
for (var ti = 0; ti < tenants.length; ti++) { th += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + sn[ti] + '</th>'; }
|
||||
th += '</tr>';
|
||||
|
||||
@@ -238,12 +238,12 @@ function renderCfMatrixData(data, container) {
|
||||
var vBtns = btnBar('cf_matrix');
|
||||
var cards = '';
|
||||
for (var mi = 11; mi >= 0; mi--) {
|
||||
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">月度合计</th>';
|
||||
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap" style="min-width:100px">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">月度合计</th>';
|
||||
for (var ti = 0; ti < tenants.length; ti++) { th += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + sn[ti] + '</th>'; }
|
||||
th += '</tr>';
|
||||
|
||||
if (isAllZero(data, tenants, ['project_cf','dept_cf','bl_cf'], mi)) continue;
|
||||
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">月度合计</th>';
|
||||
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap" style="min-width:100px">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">月度合计</th>';
|
||||
for (var ti = 0; ti < tenants.length; ti++) { th += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + sn[ti] + '</th>'; }
|
||||
th += '</tr>';
|
||||
|
||||
@@ -277,7 +277,7 @@ function matrixTbody(data, tenants, mi, keys, labels, mf) {
|
||||
vals.push(v);
|
||||
}
|
||||
var tCls = ts >= 0 ? 'text-green-600' : 'text-red-600';
|
||||
var rh = '<tr class="border-b border-slate-100 last:border-0"><td class="py-1.5 text-left ' + rowCls + ' text-slate-700 whitespace-nowrap">' + labels[ri] + '</td><td class="py-1.5 text-right font-semibold text-sm whitespace-nowrap ' + tCls + '">' + mf(ts) + '</td>';
|
||||
var rh = '<tr class="border-b border-slate-100 last:border-0"><td class="py-1.5 text-left ' + rowCls + ' text-slate-700 whitespace-nowrap" style="min-width:100px">' + labels[ri] + '</td><td class="py-1.5 text-right font-semibold text-sm whitespace-nowrap ' + tCls + '">' + mf(ts) + '</td>';
|
||||
for (var ti = 0; ti < tenants.length; ti++) {
|
||||
var cls = vals[ti] >= 0 ? 'text-green-600' : 'text-red-600';
|
||||
rh += '<td class="py-1.5 text-right text-sm whitespace-nowrap ' + cls + '">' + mf(vals[ti]) + '</td>';
|
||||
|
||||
@@ -252,18 +252,7 @@ function renderPlan() {
|
||||
return `<div id="planExpenseModule"></div>`;
|
||||
}
|
||||
const isUnsigned = false;
|
||||
const METRIC_CARDS = [
|
||||
{ label: '签约金额', value: ctx => moneyWan(ctx.signTotal), color: 'text-slate-700' },
|
||||
{ label: '已确收', hideUnsigned: true, value: ctx => moneyWan(ctx.sumRev), color: 'text-blue-700' },
|
||||
{ label: '待回款', hideUnsigned: true, value: ctx => { const v = ctx.sumRev - ctx.sumPay; return { val: v < 0 ? '超额回款 ' + moneyWan(Math.abs(v)) : moneyWan(v), cls: v > 0 ? 'text-red-600' : v < 0 ? 'text-green-600' : 'text-slate-400' }; }, color: null },
|
||||
{ label: '毛利', value: ctx => moneyWan(ctx.sumGross), color: 'text-green-700' },
|
||||
{ label: '当期成本(不含平台费用)', value: ctx => moneyWan(ctx.sumCost), color: 'text-rose-700' },
|
||||
{ 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: moneyWan(v), cls: v >= 0 ? 'text-green-600' : 'text-red-600' }; }, color: null },
|
||||
{ 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' },
|
||||
].filter(c => true);
|
||||
|
||||
|
||||
// 列排序(默认按确收金额从高到低)
|
||||
if (!state.planSort) state.planSort = { key: 'sig|rev', asc: false };
|
||||
@@ -325,22 +314,31 @@ function renderPlan() {
|
||||
];
|
||||
|
||||
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];
|
||||
});
|
||||
const receivable = sumRev - sumPay;
|
||||
const payable = sumCost - sumPaid;
|
||||
const cashflow = sumPay - sumPaid;
|
||||
const netAsset = receivable - payable;
|
||||
const m = moneyWan;
|
||||
const rows_data = [
|
||||
['签约金额', m(signTotal), '合同签约总额'],
|
||||
['确收金额', m(sumRev), '已确认收入'],
|
||||
['毛利', m(sumGross), '确收毛利'],
|
||||
['应收', m(receivable), '确收−回款'],
|
||||
['应付', m(payable), '成本−支出'],
|
||||
['现金流', m(cashflow), '回款−支出'],
|
||||
['项目净资产', m(netAsset), '应收−应付'],
|
||||
];
|
||||
const fs = '<div class="grid grid-cols-4 gap-1.5 py-2">' + rows_data.map(r =>
|
||||
'<div class="card px-2 py-1.5 text-center"><div class="text-xs text-slate-400">' + r[0] + '</div><div class="text-sm font-semibold text-slate-800">' + r[1] + '</div></div>').join('') + '</div>';
|
||||
const thead = isUnsigned ? buildThead(UNSIGNED_COLS, 'uns') : buildThead(SIGNED_COLS, 'sig');
|
||||
const theadCols = isUnsigned ? 9 : 13;
|
||||
const theadCols = isUnsigned ? 9 : 14;
|
||||
const tbody = rows.length ? `<tbody style="line-height:1.37">${rows.join("")}</tbody>` : `<tr><td colspan="${theadCols}" class="p-6 text-center text-slate-400">${emptyText}</td></tr>`;
|
||||
|
||||
const cardGrid = isUnsigned ? 'grid-cols-4' : 'grid-cols-5';
|
||||
const subtitle = isUnsigned
|
||||
? '合同 → 毛利 → 成本 → 项目利润'
|
||||
: '合同 → 确收 → 毛利 → 成本 → 项目利润 → 回款 → 已付 → 现金流';
|
||||
return `<div class="grid ${cardGrid} gap-1.5 mb-1.5">${cards.map(([l, v, c]) => '<div class="metric-card px-3 py-1.5"><span class="text-xs text-slate-500">' + l + '</span> <strong class="text-base ' + c + '">' + v + '</strong></div>').join("")}</div>` +
|
||||
`${isUnsigned ? '' : `<div class="mb-1.5">${card(`<div class="flex justify-between items-center"><div class="flex items-center gap-2">${finHeaderBase}</div>${finAddBtn}</div>`, "py-2 px-4")}</div>`}` +
|
||||
card(`<h3 class="text-sm font-bold text-slate-700">项目财务明细</h3><p class="text-xs text-slate-400 mb-3">${subtitle}</p><div class="overflow-x-auto"><table class="w-full text-sm">${thead}${tbody}</table></div>`, "p-4");
|
||||
: '合同 → 确收 → 毛利 → 成本 → 项目利润 → 回款 → 支出 → 现金流';
|
||||
return `${isUnsigned ? '' : `<div class="mb-1.5">${card(`<div class="flex justify-between items-center"><div class="flex items-center gap-2">${finHeaderBase}</div>${finAddBtn}</div>`, "py-2 px-4")}</div>`}` +
|
||||
card(fs +
|
||||
`<h3 class="text-sm font-bold text-slate-700 mt-2">项目财务明细</h3><p class="text-xs text-slate-400 mb-3">${subtitle}</p><div class="overflow-x-auto"><table class="w-full text-sm">${thead}${tbody}</table></div>`, "p-4");
|
||||
};
|
||||
|
||||
const fmtNoUnit = (v) => v ? `<span class="font-medium">${Number(v||0).toLocaleString('zh-CN')}</span>` : '<span class="text-slate-300">—</span>';
|
||||
@@ -1356,3 +1354,11 @@ window.planInlineUpdate = async (pfId, field, value) => {
|
||||
toast("更新失败:" + error.message, "error");
|
||||
}
|
||||
};
|
||||
|
||||
// ====== 筛选器控制函数 ======
|
||||
window.switchPlanFilter = (f) => { state.planFilter = f; renderPlan(); };
|
||||
window.setPlanView = (v) => { state.planView = v; renderPlan(); };
|
||||
window.setPlanYear = (y) => { state.planYear = parseInt(y); renderPlan(); };
|
||||
window.setPlanQuarter = (q) => { state.planView = 'quarterly'; state.planQuarter = q; renderPlan(); };
|
||||
window.setPlanMonth = (m) => { state.planView = 'monthly'; state.planMonth = m; renderPlan(); };
|
||||
window.setPlanStatusFilter = (f) => { state.planStatusFilter = f; renderPlan(); };
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
<header class="topbar border-b border-slate-200 bg-white px-8 py-5">
|
||||
<div class="flex items-center gap-3 w-full">
|
||||
<div class="flex-1">
|
||||
<p class="eyebrow text-xs font-semibold uppercase tracking-[0.18em] text-blue-700">OPC Manager <span class="ml-2 text-xs font-normal text-slate-400 tracking-normal">v1.2.0.30</span></p>
|
||||
<p class="eyebrow text-xs font-semibold uppercase tracking-[0.18em] text-blue-700">OPC Manager <span class="ml-2 text-xs font-normal text-slate-400 tracking-normal">v1.2.0.34</span></p>
|
||||
<div class="flex items-center gap-4 mt-1">
|
||||
<h1 class="text-2xl font-semibold" id="workspaceTitle">科普 OPC 工作台</h1>
|
||||
<div class="hidden sm:flex items-center gap-1.5 bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-100 rounded-full px-4 py-1.5">
|
||||
|
||||
Reference in New Issue
Block a user