+
总确收
¥0
@@ -185,9 +185,13 @@ function renderFinance() {
总已付
¥0
+
- | 月份 | 确收 | 毛利 | 回款 | 应付 | 已付 | |
+ | 月份 | 确收 | 毛利 | 回款 | 应付 | 已付 | 费用 | |
@@ -231,16 +235,18 @@ function renderFinance() {
{ 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 => 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 },
];
- const renderView = (rows, sumRev, sumPay, sumCost, sumPaid, sumGross, signTotal, signCnt, emptyText) => {
- const ctx = { sumRev, sumPay, sumCost, sumPaid, sumGross, signTotal, signCnt };
+ const renderView = (rows, sumRev, sumPay, sumCost, sumPaid, sumGross, sumExpense, signTotal, signCnt, emptyText) => {
+ const ctx = { sumRev, sumPay, sumCost, sumPaid, sumGross, sumExpense, 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];
@@ -258,16 +264,18 @@ function renderFinance() {
'
应付 | ' +
'
已付 | ' +
'
付款率 | ' +
+ '
费用 | ' +
'
现金流 | ' +
+ '
利润 | ' +
'';
- const tbody = rows.length ? `
${rows.join("")}` : `
| ${emptyText} |
`;
+ const tbody = rows.length ? `
${rows.join("")}` : `
| ${emptyText} |
`;
- return card(`
${cards.map(([l, v, c]) => '
' + l + '' + v + '
').join("")}
`, "p-4") +
+ return card(`
${cards.map(([l, v, c]) => '
' + l + '' + v + '
').join("")}
`, "p-4") +
card(`
`, "p-4");
};
const fmtNoUnit = (v) => v ? `
${Number(v||0).toLocaleString('zh-CN')}` : '
—';
- const tdRow = (pf, rev, payment, cost, paid, gross) => {
+ const tdRow = (pf, rev, payment, cost, paid, gross, expense) => {
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) + '%' : '
—';
@@ -276,7 +284,10 @@ function renderFinance() {
const st = pf.status === '已签约' ? '
已签约' : '
待签约';
const cf = cashflow ? money(cashflow) : '
—';
const cfCls = cashflow >= 0 ? 'text-green-600' : 'text-red-600';
- return `
| ${esc(pf.customer_name)} | ${st} | ${money(pf.sign_amount)} | ${fmtNoUnit(rev)} | ${exe} | ${fmtNoUnit(gross)} | ${grossR} | ${fmtNoUnit(payment)} | ${payR} | ${fmtNoUnit(cost)} | ${fmtNoUnit(paid)} | ${paidR} | ${cf} |
`;
+ const profit = gross - expense;
+ 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} |
`;
};
if (state.finView === 'monthly') {
@@ -286,7 +297,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;
+ let sumRev = 0, sumPay = 0, sumCost = 0, sumPaid = 0, sumGross = 0, sumExpense = 0;
allPfs.forEach(pf => {
let bd = []; try { bd = JSON.parse(pf.budget_data || "[]"); } catch (e) {}
const b = bd.find(x => (x.month || "") === selMonth) || {};
@@ -295,13 +306,14 @@ 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);
- 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 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));
});
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, signTotal, signCnt, '该月份暂无数据');
+ return renderView(rows, sumRev, sumPay, sumCost, sumPaid, sumGross, sumExpense, signTotal, signCnt, '该月份暂无数据');
}
if (state.finView === 'quarterly') {
@@ -319,7 +331,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;
+ let sumRev = 0, sumPay = 0, sumCost = 0, sumPaid = 0, sumGross = 0, sumExpense = 0;
const rows = [];
allPfs.forEach(pf => {
const rev = sumBudget(pf, "rev");
@@ -327,33 +339,34 @@ function renderFinance() {
const cost = sumBudget(pf, "cost");
const paid = sumBudget(pf, "paid");
const gross = sumBudget(pf, "gross");
- sumRev += rev; sumPay += payment; sumCost += cost; sumPaid += paid; sumGross += gross;
- rows.push(tdRow(pf, rev, payment, cost, paid, 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));
});
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, signTotal, signCnt, '该季度暂无数据');
+ return renderView(rows, sumRev, sumPay, sumCost, sumPaid, sumGross, sumExpense, 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;
- 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; });
+ 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; });
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) };
+ 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) };
};
const allPfs = pfs.filter(x => x.status === state.finFilter);
- let sumRev = 0, sumPay = 0, sumCost = 0, sumPaid = 0, sumGross = 0;
+ let sumRev = 0, sumPay = 0, sumCost = 0, sumPaid = 0, sumGross = 0, sumExpense = 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;
- rows.push(tdRow(pf, t.rev, t.payment, t.cost, t.paid, t.gross));
+ 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));
});
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, signTotal, signCnt, '暂无数据');
+ return renderView(rows, sumRev, sumPay, sumCost, sumPaid, sumGross, sumExpense, signTotal, signCnt, '暂无数据');
})()}
`;
if (window.lucide) window.lucide.createIcons();
@@ -374,7 +387,7 @@ window.openFinanceModal = () => {
modal.classList.remove("hidden");
};
-window.addBudgetRow = (month = '', rev = '', gross = '', payment = '', cost = '', paid = '') => {
+window.addBudgetRow = (month = '', rev = '', gross = '', payment = '', cost = '', paid = '', expense = '') => {
const tbody = document.querySelector("#budgetTbody");
if (!tbody) return;
const row = document.createElement("tr");
@@ -384,6 +397,7 @@ window.addBudgetRow = (month = '', rev = '', gross = '', payment = '', cost = ''
`;
tbody.appendChild(row);
if (window.lucide) window.lucide.createIcons();
@@ -394,23 +408,27 @@ 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, totalPaid = 0;
+ let totalRev = 0, totalGross = 0, totalPayment = 0, totalCost = 0, totalExpense = 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);
};
@@ -419,7 +437,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 || ''));
+ rows.forEach(r => addBudgetRow(r.month || '', r.rev || '', r.gross || '', r.payment || '', r.cost || '', r.paid || '', r.expense || ''));
setTimeout(() => updateBudgetSummary(), 50);
};
@@ -687,6 +705,7 @@ 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;
@@ -697,8 +716,9 @@ 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, paid });
+ budgetRows.push({ month: m, rev, gross, payment, cost, expense, paid });
totalRev += rev;
totalGross += gross;
totalPaidFromBudget += paid;
@@ -707,10 +727,12 @@ window.createFinance = async (event) => {
data.total_rev = totalRev;
data.total_gross = totalGross;
data.total_paid = totalPaidFromBudget;
- let totalPayment = 0, totalCost = 0;
- for (const r of budgetRows) { totalPayment += r.payment; totalCost += r.cost; }
+ let totalPayment = 0, totalCost = 0, totalExpense = 0;
+ for (const r of budgetRows) { totalPayment += r.payment; totalCost += r.cost; totalExpense += r.expense; }
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/utils.js b/static/modules/utils.js
index ca68a08..9aeed51 100644
--- a/static/modules/utils.js
+++ b/static/modules/utils.js
@@ -16,6 +16,10 @@ const state = {
chart3: null,
productPlatform: "all",
uploadTasks: [],
+ expenseFilter: "全部",
+ expenseView: "total",
+ expenseMonth: "",
+ expenseQuarter: "",
};
const money = (value) => `${Number(value || 0).toLocaleString("zh-CN")} 元`;