diff --git a/backend/migrations/columns.py b/backend/migrations/columns.py
index ec957b0..17c40e9 100644
--- a/backend/migrations/columns.py
+++ b/backend/migrations/columns.py
@@ -129,6 +129,9 @@ def migrate_add_columns():
cur.execute("ALTER TABLE plan_finances DROP COLUMN status")
conn.commit()
print("[migrate] plan_finances.status 列已删除")
+ # project_finances 新增客户名称字段
+ _add_column_if_missing(conn, "project_finances", "client_name",
+ "ALTER TABLE project_finances ADD COLUMN client_name VARCHAR(200) NOT NULL DEFAULT ''")
# 删除 project_finances.status 列(实际模块不再需要状态字段)
cur.execute("SHOW COLUMNS FROM project_finances LIKE 'status'")
if cur.fetchone():
diff --git a/backend/routes.py b/backend/routes.py
index c530d25..154d5ae 100644
--- a/backend/routes.py
+++ b/backend/routes.py
@@ -23,9 +23,9 @@ TABLES = {
"products": ("product_versions", ["product_name", "version", "version_goal", "priority", "start_date", "plan_date", "dev_done_date", "test_date", "launch_date", "status", "notes", "tenant"]),
"finance": ("finance_records", ["month", "project_name", "record_type", "category", "amount", "occurred_date", "notes", "tenant"]),
"tasks": ("project_tasks", ["project_id", "phase", "milestone", "task", "owner", "due_date", "blockers", "notes", "status", "sort_order", "priority", "tenant"]),
- "projectFinances": ("project_finances", ["project_id", "tenant", "business_type", "customer_name", "sign_amount", "sign_month", "sales_person", "owner", "total_rev", "total_gross", "total_payment", "total_cost", "total_paid", "budget_data", "expense_data", "start_date", "end_date", "task_type", "task_count", "service_fee_standard", "project_manager", "task_data", "project_code", "contact_name", "contact_phone", "other_info"]),
+ "projectFinances": ("project_finances", ["project_id", "tenant", "business_type", "customer_name", "client_name", "sign_amount", "sign_month", "sales_person", "owner", "total_rev", "total_gross", "total_payment", "total_cost", "total_paid", "budget_data", "expense_data", "start_date", "end_date", "task_type", "task_count", "service_fee_standard", "project_manager", "task_data", "project_code", "contact_name", "contact_phone", "other_info"]),
"expense": ("expense_records", ["expense_type", "expense_month", "amount", "incurred_amount", "notes", "status", "tenant"]),
- "planFinances": ("plan_finances", ["project_id", "tenant", "business_type", "customer_name", "sign_amount", "sign_month", "sales_person", "owner", "total_rev", "total_gross", "total_payment", "total_cost", "total_paid", "budget_data", "expense_data", "start_date", "end_date", "task_type", "task_count", "service_fee_standard", "project_manager", "task_data", "project_code", "contact_name", "contact_phone", "other_info", "client_name", "project_type", "weight"]),
+ "planFinances": ("plan_finances", ["project_id", "tenant", "business_type", "customer_name", "client_name", "sign_amount", "sign_month", "sales_person", "owner", "total_rev", "total_gross", "total_payment", "total_cost", "total_paid", "budget_data", "expense_data", "start_date", "end_date", "task_type", "task_count", "service_fee_standard", "project_manager", "task_data", "project_code", "contact_name", "contact_phone", "other_info", "client_name", "project_type", "weight"]),
"planExpense": ("plan_expense_records", ["expense_type", "expense_month", "amount", "incurred_amount", "notes", "status", "tenant"]),
}
diff --git a/static/modules/finance.js b/static/modules/finance.js
index c4e5ce5..f01d9ee 100644
--- a/static/modules/finance.js
+++ b/static/modules/finance.js
@@ -98,7 +98,7 @@ function renderFinance() {
})();
const sm = pf.sign_month || "";
const signMonthCell = `
${sm || '—'} | `;
- return `| ${esc(pf.customer_name)} | ${signMonthCell}${money(pf.sign_amount)} | ${mCols}${totalCol}
`;
+ return `| ${esc(pf.client_name || "")} | ${esc(pf.customer_name)} | ${signMonthCell}${money(pf.sign_amount)} | ${mCols}${totalCol}
`;
};
const now2 = new Date();
@@ -134,7 +134,7 @@ function renderFinance() {
-
+
@@ -326,7 +326,7 @@ function renderFinance() {
const profit = gross;
const pfCls = profit >= 0 ? 'text-green-600' : 'text-red-600';
const pfVal = profit ? money(profit) : '
—';
- return `
| ${esc(pf.customer_name)} | ${money(pf.sign_amount)} | ${fmtNoUnit(rev)} | ${fmtNoUnit(gross)} | ${fmtNoUnit(cost)} | ${pfVal} | ${fmtNoUnit(payment)} | ${fmtNoUnit(paid)} | ${cf} | ${exe} | ${grossR} | ${payR} | ${paidR} |
`;
+ return `
| ${esc(pf.client_name || "")} | ${esc(pf.customer_name)} | ${money(pf.sign_amount)} | ${fmtNoUnit(rev)} | ${fmtNoUnit(gross)} | ${fmtNoUnit(cost)} | ${pfVal} | ${fmtNoUnit(payment)} | ${fmtNoUnit(paid)} | ${cf} | ${exe} | ${grossR} | ${payR} | ${paidR} |
`;
};
// 待签约简版行
const tdRowUnsigned = (pf, cost, gross) => {
@@ -772,6 +772,7 @@ window.openPfEditModal = (pfId) => {
}
});
form.querySelector('[name="customer_name"]').value = pf.customer_name || "";
+ const clientInput = form.querySelector('[name="client_name"]'); if (clientInput) clientInput.value = pf.client_name || "";
const setVal = (name, val) => { const el = form.querySelector(`[name="${name}"]`); if (el) el.value = val || ""; };
setVal("project_code", pf.project_code);
form.querySelector('[name="sign_amount"]').value = pf.sign_amount || "";
diff --git a/static/modules/plan.js b/static/modules/plan.js
index 483b1f3..02437c4 100644
--- a/static/modules/plan.js
+++ b/static/modules/plan.js
@@ -325,7 +325,7 @@ function renderPlan() {
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}
`, "p-4");
};
@@ -966,46 +966,50 @@ function renderPlanOverview() {
const { summary, financeMonthly } = state.data;
if (!summary) return;
const m = summary.metrics;
- const moneyIntL = (v) => `${Math.round(Number(v || 0)).toLocaleString("zh-CN")} 元`;
+ const moneyIntL = (v) => Math.round(Number(v || 0)).toLocaleString("zh-CN");
const moneyWan = (v) => {
const n = Number(v || 0);
return n >= 10000 ? (n / 10000).toFixed(0) + "万" : n.toLocaleString("zh-CN");
};
const card = (body, cls) => '
' + body + '
';
- const rows1 = [["年度累计", moneyIntL(m.signed_annual || m.signed_amount)], ["本季度累计", moneyIntL(m.signed_q2 || 0)], ["本月累计", moneyIntL(m.signed_month || 0)], ["上本季度累计", moneyIntL(m.signed_prev_q || 0)], ["上月累计", moneyIntL(m.signed_prev_month || 0)]];
- const rows2 = [["年度累计", moneyIntL(m.revenue_annual || 0)], ["本季度累计", moneyIntL(m.revenue_q2 || 0)], ["本月累计", moneyIntL(m.monthly_revenue || 0)], ["上本季度累计", moneyIntL(m.revenue_prev_q || 0)], ["上月累计", moneyIntL(m.revenue_prev_month || 0)]];
- const rows3 = [["年度累计", moneyIntL(m.gross_annual || 0)], ["本季度累计", moneyIntL(m.gross_q2 || 0)], ["本月累计", moneyIntL(m.monthly_net_profit || 0)], ["上本季度累计", moneyIntL(m.gross_prev_q || 0)], ["上月累计", moneyIntL(m.gross_prev_month || 0)]];
- const rows4 = [["年度累计", moneyIntL(m.cost_annual || 0)], ["本季度累计", moneyIntL(m.cost_q2 || 0)], ["本月累计", moneyIntL(m.cost_month || 0)], ["上本季度累计", moneyIntL(m.cost_prev_q || 0)], ["上月累计", moneyIntL(m.cost_prev_month || 0)]];
- const rows5 = [["年度累计", moneyIntL(m.profit_annual || 0)], ["本季度累计", moneyIntL(m.profit_q2 || 0)], ["本月累计", moneyIntL(m.profit_month || 0)], ["上本季度累计", moneyIntL(m.profit_prev_q || 0)], ["上月累计", moneyIntL(m.profit_prev_month || 0)]];
- const rows7 = [["年度累计", moneyIntL(m.payment_annual || 0)], ["本季度累计", moneyIntL(m.payment_q2 || 0)], ["本月累计", moneyIntL(m.payment_month || 0)], ["上本季度累计", moneyIntL(m.payment_prev_q || 0)], ["上月累计", moneyIntL(m.payment_prev_month || 0)]];
- const rows8 = [["年度累计", moneyIntL(m.paid_annual || 0)], ["本季度累计", moneyIntL(m.paid_q2 || 0)], ["本月累计", moneyIntL(m.paid_month || 0)], ["上本季度累计", moneyIntL(m.paid_prev_q || 0)], ["上月累计", moneyIntL(m.paid_prev_month || 0)]];
- const rows9 = [["年度累计", moneyIntL(m.cashflow_annual || 0)], ["本季度累计", moneyIntL(m.cashflow_q2 || 0)], ["本月累计", moneyIntL(m.cashflow_month || 0)], ["上本季度累计", moneyIntL(m.cashflow_prev_q || 0)], ["上月累计", moneyIntL(m.cashflow_prev_month || 0)]];
-
- const qoq = (cur, prev) => { if (!prev) return '
—'; const pct = Math.round((cur - prev) / prev * 100); const cls = pct >= 0 ? 'text-green-600' : 'text-red-600'; const sign = pct >= 0 ? '+' : ''; return '
' + sign + pct + '%'; };
+ // 月度数据 (financeMonthly 有12个月)
+ const fm = financeMonthly || [];
+ const mLbl = ['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月'];
const LABELS = ['合同金额','确收金额','确收毛利','成本','项目利润','回款金额','已付','现金流'];
- const Q_FIELDS = [m.signed_q2||0, m.revenue_q2, m.gross_q2, m.cost_q2||0, m.profit_q2||0, m.payment_q2||0, m.paid_q2||0, m.cashflow_q2||0];
- const Q_PREV = [m.signed_prev_q||0, m.revenue_prev_q||0, m.gross_prev_q||0, m.cost_prev_q||0, m.profit_prev_q||0, m.payment_prev_q||0, m.paid_prev_q||0, m.cashflow_prev_q||0];
- const M_FIELDS = [m.signed_month||0, m.monthly_revenue, m.monthly_net_profit, m.cost_month||0, m.profit_month||0, m.payment_month||0, m.paid_month||0, m.cashflow_month||0];
- const M_PREV = [m.signed_prev_month||0, m.revenue_prev_month||0, m.gross_prev_month||0, m.cost_prev_month||0, m.profit_prev_month||0, m.payment_prev_month||0, m.paid_prev_month||0, m.cashflow_prev_month||0];
- const ROWS = [rows1, rows2, rows3, rows4, rows5, rows7, rows8, rows9];
- const CF_RAW = [m.cashflow_annual||0, m.cashflow_q2||0, m.cashflow_month||0, m.cashflow_prev_q||0, m.cashflow_prev_month||0];
- const PF_RAW = [m.profit_annual||0, m.profit_q2||0, m.profit_month||0, m.profit_prev_q||0, m.profit_prev_month||0];
-
- var tdVal = function(ri, col, val) {
- if (ri === 4) return '
' + val + ' | ';
- if (ri === 7) return '
' + val + ' | ';
- return '
' + val + ' | ';
- };
- var thead = '
| 指标 | 年度累计 | 上季度累计 | 上月累计 | 本季度累计 | 季环比 | 本月累计 | 月环比 |
|---|
';
+ // 每行的年度累计 + 12个月值
+ const annualVals = [
+ m.signed_annual || m.signed_amount || 0,
+ m.revenue_annual || 0,
+ m.gross_annual || 0,
+ m.cost_annual || 0,
+ m.profit_annual || 0,
+ m.payment_annual || 0,
+ m.paid_annual || 0,
+ m.cashflow_annual || 0,
+ ];
+ // 字段映射: sign, revenue, gross, cost, profit(=gross-cost), payment, paid, cashflow(=payment-paid)
+ const fmFields = ['sign','revenue','gross','cost',null,'payment','paid',null];
+ var thead = '
| 指标 | 年度累计 | ';
+ for (var mi = 0; mi < 12; mi++) { thead += '' + mLbl[mi] + ' | '; }
+ thead += '
|---|
';
var tbody = '';
for (var ri = 0; ri < 8; ri++) {
- var vals = ROWS[ri];
- tbody += '
| ' + LABELS[ri] + ' | ' +
- tdVal(ri, 0, vals[0][1]) + tdVal(ri, 3, vals[3][1]) + tdVal(ri, 4, vals[4][1]) + tdVal(ri, 1, vals[1][1]) +
- '' + qoq(Q_FIELDS[ri], Q_PREV[ri]) + ' | ' +
- tdVal(ri, 2, vals[2][1]) +
- '' + qoq(M_FIELDS[ri], M_PREV[ri]) + ' |
';
+ var rowHtml = '
| ' + LABELS[ri] + ' | ';
+ rowHtml += '' + moneyIntL(annualVals[ri]) + ' | ';
+ for (var mi = 0; mi < 12; mi++) {
+ var val = 0;
+ if (fm[mi]) {
+ if (ri === 4) { val = (fm[mi].gross || 0) - (fm[mi].cost || 0); }
+ else if (ri === 7) { 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 === 4) cls = val >= 0 ? 'text-green-600' : 'text-red-600';
+ if (ri === 7) cls = val >= 0 ? 'text-green-600' : 'text-red-600';
+ rowHtml += '' + moneyIntL(val) + ' | ';
+ }
+ tbody += rowHtml + '
';
}
document.querySelector("#planOverview").innerHTML = '
' +
diff --git a/templates/index.html b/templates/index.html
index 3e2961e..f3cb77a 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -80,7 +80,7 @@
-
OPC Manager v1.0.0.25
+
OPC Manager v1.0.0.26