@@ -232,7 +227,7 @@ function renderFinance() {
const fmt = (v) => v ? `
${money(v)}` : '
—';
const fmtDiff = (v) => { if (!v) return '
—'; return `
${money(Math.abs(v))}`; };
const rows = [];
- let sumRev=0, sumPay=0, sumCost=0, sumPaid=0;
+ let sumRev=0, sumPay=0, sumCost=0, sumPaid=0, sumGross=0;
allPfs.forEach(pf => {
let bd = []; try { bd = JSON.parse(pf.budget_data || "[]"); } catch(e) {}
const b = bd.find(x => (x.month||"") === selMonth) || {};
@@ -240,14 +235,15 @@ function renderFinance() {
const payment = Math.round(parseFloat(b.payment||0)||0);
const cost = Math.round(parseFloat(b.cost||0)||0);
const paid = Math.round(parseFloat(b.paid||0)||0);
- if (!rev && !payment && !cost && !paid) return;
+ const gross = Math.round(parseFloat(b.gross||0)||0);
+ if (!rev && !payment && !cost && !paid && !gross) return;
const payDiff = rev - payment;
const costDiff = cost - paid;
const cashflow = payment - paid;
- sumRev+=rev; sumPay+=payment; sumCost+=cost; sumPaid+=paid;
- rows.push(`
| ${esc(pf.customer_name)} | ${pf.status==='已签约'?'已签约':pf.status==='流程中'?'流程中':'待签约'} | ${fmt(rev)} | ${fmt(payment)} | ${fmtDiff(payDiff)} | ${fmt(cost)} | ${fmt(paid)} | ${fmtDiff(costDiff)} | ${cashflow ? money(cashflow) : '—'} |
`);
+ sumRev+=rev; sumPay+=payment; sumCost+=cost; sumPaid+=paid; sumGross+=gross;
+ rows.push(`
| ${esc(pf.customer_name)} | ${pf.status==='已签约'?'已签约':pf.status==='流程中'?'流程中':'待签约'} | ${money(pf.sign_amount)} | ${fmt(rev)} | ${rev&&pf.sign_amount?Math.round(rev/pf.sign_amount*100)+'%':'—'} | ${fmt(payment)} | ${rev&&payment?Math.round(payment/rev*100)+'%':'—'} | ${fmt(cost)} | ${fmt(paid)} | ${cost&&paid?Math.round(paid/cost*100)+'%':'—'} | ${fmt(gross)} | ${rev&&gross?Math.round(gross/rev*100)+'%':'—'} | ${cashflow ? money(cashflow) : '—'} |
`);
});
- return card(`
${finHeaderBase}月份:
${finAddBtn}
| 项目名称 | 状态 | 已确收 | 已回款 | 回款差额 | 应付 | 已付 | 应付差额 | 现金流 |
${rows.length ? rows.join("") : '| 该月份暂无数据 |
'}| 合计 | ${money(sumRev)} | ${money(sumPay)} | ${fmtDiff(sumRev - sumPay)} | ${money(sumCost)} | ${money(sumPaid)} | ${fmtDiff(sumCost - sumPaid)} | ${money(sumPay - sumPaid)} |
`, "p-4");
+ return card(`
月份:
${(()=>{const s=allPfs.reduce((a,p)=>a+(p.sign_amount||0),0);return[["签约金额",money(s),"text-slate-700"],["已确收",money(sumRev),"text-blue-700"],["执行率",sumRev&&s?Math.round(sumRev/s*100)+'%':'—',"text-slate-500"],["已回款",money(sumPay),"text-amber-700"],["回款率",sumRev&&sumPay?Math.round(sumPay/sumRev*100)+'%':'—',"text-slate-500"],["应付",money(sumCost),"text-rose-700"],["已付",money(sumPaid),"text-purple-700"],["付款率",sumCost&&sumPaid?Math.round(sumPaid/sumCost*100)+'%':'—',"text-slate-500"],["毛利",money(sumGross),"text-green-700"],["毛利率",sumRev&&sumGross?Math.round(sumGross/sumRev*100)+'%':'—',"text-slate-500"],["现金流",money(sumPay-sumPaid),sumPay-sumPaid>=0?"text-green-600":"text-red-600"]].map(([l,v,c])=>'
'+l+''+v+'
').join("")})()}
| 项目名称 | 状态 | 签约金额 | 已确收 | 执行率 | 已回款 | 回款率 | 应付 | 已付 | 付款率 | 毛利 | 毛利率 | 现金流 |
${rows.length ? rows.join("") : '| 该月份暂无数据 |
'}
`, "p-4");
})() : state.finView === 'quarterly' ? (() => {
const allPfs = pfs.filter(x => x.status === state.finFilter);
const qRanges = [[1,2,3],[4,5,6],[7,8,9],[10,11,12]];
@@ -270,34 +266,35 @@ function renderFinance() {
};
const fmt = (v) => v ? `
${money(v)}` : '
—';
const fmtDiff = (v) => { if (!v) return '
—'; return `
${money(Math.abs(v))}`; };
- let sumRev = 0, sumPay = 0, sumCost = 0, sumPaid = 0;
+ let sumRev = 0, sumPay = 0, sumCost = 0, sumPaid = 0, sumGross = 0;
const rows = [];
allPfs.forEach(pf => {
const rev = sumBudget(pf, "rev");
const payment = sumBudget(pf, "payment");
const cost = sumBudget(pf, "cost");
const paid = sumBudget(pf, "paid");
+ const gross = sumBudget(pf, "gross");
const payDiff = rev - payment;
const costDiff = cost - paid;
const cashflow = payment - paid;
- sumRev += rev; sumPay += payment; sumCost += cost; sumPaid += paid;
- rows.push(`
| ${esc(pf.customer_name)} | ${pf.status==='已签约'?'已签约':pf.status==='流程中'?'流程中':'待签约'} | ${fmt(rev)} | ${fmt(payment)} | ${fmtDiff(payDiff)} | ${fmt(cost)} | ${fmt(paid)} | ${fmtDiff(costDiff)} | ${cashflow ? money(cashflow) : '—'} |
`);
+ sumRev += rev; sumPay += payment; sumCost += cost; sumPaid += paid; sumGross += gross;
+ rows.push(`
| ${esc(pf.customer_name)} | ${pf.status==='已签约'?'已签约':pf.status==='流程中'?'流程中':'待签约'} | ${money(pf.sign_amount)} | ${fmt(rev)} | ${rev&&pf.sign_amount?Math.round(rev/pf.sign_amount*100)+'%':'—'} | ${fmt(payment)} | ${rev&&payment?Math.round(payment/rev*100)+'%':'—'} | ${fmt(cost)} | ${fmt(paid)} | ${cost&&paid?Math.round(paid/cost*100)+'%':'—'} | ${fmt(gross)} | ${rev&&gross?Math.round(gross/rev*100)+'%':'—'} | ${cashflow ? money(cashflow) : '—'} |
`);
});
- return card(`
${finHeaderBase}季度:
${finAddBtn}
| 项目名称 | 状态 | 已确收 | 已回款 | 回款差额 | 应付 | 已付 | 应付差额 | 现金流 |
${rows.length ? rows.join("") : '| 该季度暂无数据 |
'}| 合计 | ${money(sumRev)} | ${money(sumPay)} | ${fmtDiff(sumRev - sumPay)} | ${money(sumCost)} | ${money(sumPaid)} | ${fmtDiff(sumCost - sumPaid)} | ${money(sumPay - sumPaid)} |
`, "p-4");
+ return card(`
季度:
${(()=>{const s=allPfs.reduce((a,p)=>a+(p.sign_amount||0),0);return[["签约金额",money(s),"text-slate-700"],["已确收",money(sumRev),"text-blue-700"],["执行率",sumRev&&s?Math.round(sumRev/s*100)+'%':'—',"text-slate-500"],["已回款",money(sumPay),"text-amber-700"],["回款率",sumRev&&sumPay?Math.round(sumPay/sumRev*100)+'%':'—',"text-slate-500"],["应付",money(sumCost),"text-rose-700"],["已付",money(sumPaid),"text-purple-700"],["付款率",sumCost&&sumPaid?Math.round(sumPaid/sumCost*100)+'%':'—',"text-slate-500"],["毛利",money(sumGross),"text-green-700"],["毛利率",sumRev&&sumGross?Math.round(sumGross/sumRev*100)+'%':'—',"text-slate-500"],["现金流",money(sumPay-sumPaid),sumPay-sumPaid>=0?"text-green-600":"text-red-600"]].map(([l,v,c])=>'
'+l+''+v+'
').join("")})()}
| 项目名称 | 状态 | 签约金额 | 已确收 | 执行率 | 已回款 | 回款率 | 应付 | 已付 | 付款率 | 毛利 | 毛利率 | 现金流 |
${rows.length ? rows.join("") : '| 该季度暂无数据 |
'}
`, "p-4");
})() : (() => {
const calcTotals = (pf) => {
let budget = []; try { budget = JSON.parse(pf.budget_data || "[]"); } catch (e) {}
- let rev = 0, payment = 0, cost = 0;
- budget.forEach(b => { rev += parseFloat(b.rev||0)||0; payment += parseFloat(b.payment||0)||0; cost += parseFloat(b.cost||0)||0; });
+ 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; });
const paid = parseFloat(pf.total_paid) || 0;
- return { rev: Math.round(rev), payment: Math.round(payment), cost: Math.round(cost), paid: Math.round(paid) };
+ return { rev: Math.round(rev), payment: Math.round(payment), cost: Math.round(cost), paid: Math.round(paid), gross: Math.round(gross) };
};
const allPfs = pfs.filter(x => x.status === state.finFilter);
- let sumRev=0, sumPay=0, sumCost=0, sumPaid=0;
- allPfs.forEach(pf => { const t = calcTotals(pf); sumRev+=t.rev; sumPay+=t.payment; sumCost+=t.cost; sumPaid+=t.paid; });
+ let sumRev=0, sumPay=0, sumCost=0, sumPaid=0, sumGross=0;
+ allPfs.forEach(pf => { const t = calcTotals(pf); sumRev+=t.rev; sumPay+=t.payment; sumCost+=t.cost; sumPaid+=t.paid; sumGross+=t.gross; });
const fmt = (v) => v ? `
${money(v)}` : '
—';
const fmtDiff = (v) => { if (!v) return '
—'; return `
${money(Math.abs(v))}`; };
- return card(`
${finHeaderBase}
${finAddBtn}
| 项目名称 | 状态 | 已确收 | 已回款 | 回款差额 | 应付 | 已付 | 应付差额 | 现金流 |
${allPfs.map(pf => { const t = calcTotals(pf); const payDiff = t.rev - t.payment; const costDiff = t.cost - t.paid; const cashflow = t.payment - t.paid; return `| ${esc(pf.customer_name)} | ${pf.status==='已签约'?'已签约':pf.status==='流程中'?'流程中':'待签约'} | ${fmt(t.rev)} | ${fmt(t.payment)} | ${fmtDiff(payDiff)} | ${fmt(t.cost)} | ${fmt(t.paid)} | ${fmtDiff(costDiff)} | ${cashflow ? money(cashflow) : '—'} |
`; }).join("")}| 合计 | ${money(sumRev)} | ${money(sumPay)} | ${fmtDiff(sumRev - sumPay)} | ${money(sumCost)} | ${money(sumPaid)} | ${fmtDiff(sumCost - sumPaid)} | ${money(sumPay - sumPaid)} |
`, "p-4");
+ return card(`
${(()=>{const s=allPfs.reduce((a,p)=>a+(p.sign_amount||0),0);return[["签约金额",money(s),"text-slate-700"],["已确收",money(sumRev),"text-blue-700"],["执行率",sumRev&&s?Math.round(sumRev/s*100)+'%':'—',"text-slate-500"],["已回款",money(sumPay),"text-amber-700"],["回款率",sumRev&&sumPay?Math.round(sumPay/sumRev*100)+'%':'—',"text-slate-500"],["应付",money(sumCost),"text-rose-700"],["已付",money(sumPaid),"text-purple-700"],["付款率",sumCost&&sumPaid?Math.round(sumPaid/sumCost*100)+'%':'—',"text-slate-500"],["毛利",money(sumGross),"text-green-700"],["毛利率",sumRev&&sumGross?Math.round(sumGross/sumRev*100)+'%':'—',"text-slate-500"],["现金流",money(sumPay-sumPaid),sumPay-sumPaid>=0?"text-green-600":"text-red-600"]].map(([l,v,c])=>'
'+l+''+v+'
').join("")})()}
| 项目名称 | 状态 | 签约金额 | 已确收 | 执行率 | 已回款 | 回款率 | 应付 | 已付 | 付款率 | 毛利 | 毛利率 | 现金流 |
${allPfs.map(pf => { const t = calcTotals(pf); const payDiff = t.rev - t.payment; const costDiff = t.cost - t.paid; const cashflow = t.payment - t.paid; return `| ${esc(pf.customer_name)} | ${pf.status==='已签约'?'已签约':pf.status==='流程中'?'流程中':'待签约'} | ${money(pf.sign_amount)} | ${fmt(t.rev)} | ${t.rev&&pf.sign_amount?Math.round(t.rev/pf.sign_amount*100)+'%':'—'} | ${fmt(t.payment)} | ${t.rev&&t.payment?Math.round(t.payment/t.rev*100)+'%':'—'} | ${fmt(t.cost)} | ${fmt(t.paid)} | ${t.cost&&t.paid?Math.round(t.paid/t.cost*100)+'%':'—'} | ${fmt(t.gross)} | ${t.rev&&t.gross?Math.round(t.gross/t.rev*100)+'%':'—'} | ${cashflow ? money(cashflow) : '—'} |
`; }).join("")}
`, "p-4");
})()}
`;
if (window.lucide) window.lucide.createIcons();
diff --git a/static/modules/purchase.js b/static/modules/purchase.js
new file mode 100644
index 0000000..7af8dfa
--- /dev/null
+++ b/static/modules/purchase.js
@@ -0,0 +1,103 @@
+// purchase.js — 采购管理模块
+
+function renderPurchase() {
+ const records = state.data.purchase || [];
+ const money = (v) => `¥ ${(Number(v || 0)).toLocaleString("zh-CN", { minimumFractionDigits: 0, maximumFractionDigits: 2 })}`;
+ const badge = (s) => {
+ const m = { "已审批": "green", "已采购": "blue", "已完成": "slate", "待审批": "amber" };
+ const c = m[s] || "amber";
+ return `
`;
+ };
+ const esc = (s) => (s || "").replace(/&/g, "&").replace(//g, ">").replace(/"/g, """);
+
+ const totalAmount = records.reduce((s, r) => s + (r.amount || 0), 0);
+
+ document.querySelector("#purchase").innerHTML = `
`;
+ if (window.lucide) window.lucide.createIcons();
+}
+
+window.openPurchaseModal = () => {
+ const form = document.querySelector("#purchaseModal form");
+ form.reset();
+ form.querySelector('[name="purchase_id"]').value = "";
+ document.querySelector("#purchaseModalTitle").textContent = "新增采购";
+ document.querySelector("#purchaseDeleteBtn").classList.add("hidden");
+ document.querySelector("#purchaseModal").classList.remove("hidden");
+};
+
+window.closePurchaseModal = () => {
+ document.querySelector("#purchaseModal").classList.add("hidden");
+};
+
+window.openPurchaseEdit = (id) => {
+ const r = (state.data.purchase || []).find(x => x.id === id);
+ if (!r) return;
+ const form = document.querySelector("#purchaseModal form");
+ form.reset();
+ const setVal = (name, val) => { const el = form.querySelector(`[name="${name}"]`); if (el) el.value = val || ""; };
+ setVal("purchase_id", r.id);
+ setVal("project_name", r.project_name);
+ setVal("purchase_item", r.purchase_item);
+ setVal("supplier", r.supplier);
+ setVal("amount", r.amount);
+ setVal("purchase_date", r.purchase_date);
+ setVal("status", r.status);
+ setVal("category", r.category);
+ setVal("notes", r.notes);
+ document.querySelector("#purchaseModalTitle").textContent = "编辑采购";
+ document.querySelector("#purchaseDeleteBtn").classList.remove("hidden");
+ document.querySelector("#purchaseModal").classList.remove("hidden");
+};
+
+window.savePurchase = async (event) => {
+ event.preventDefault();
+ const form = event.target;
+ const data = Object.fromEntries(new FormData(form).entries());
+ const isEdit = !!data.purchase_id;
+ delete data.purchase_id;
+
+ const url = isEdit
+ ? `/api/purchase/${form.querySelector('[name="purchase_id"]').value}`
+ : "/api/purchase";
+ const method = isEdit ? "PUT" : "POST";
+
+ const resp = await fetch(url, { method, headers: { "Content-Type": "application/json" }, body: JSON.stringify({ data: { ...data, tenant: state.tenant } }) });
+ if (!resp.ok) { alert("保存失败"); return; }
+
+ // Refresh data
+ const bResp = await fetch(`/api/bootstrap?tenant=${encodeURIComponent(state.tenant)}`);
+ if (bResp.ok) {
+ const bd = await bResp.json();
+ state.data = bd;
+ applyUserTenants();
+ renderPurchase();
+ }
+ closePurchaseModal();
+};
+
+window.deletePurchaseItem = async () => {
+ const id = document.querySelector("#purchaseModal form [name='purchase_id']").value;
+ if (!id || !confirm("确定删除?")) return;
+ const resp = await fetch(`/api/purchase/${id}`, { method: "DELETE" });
+ if (!resp.ok) { alert("删除失败"); return; }
+ const bResp = await fetch(`/api/bootstrap?tenant=${encodeURIComponent(state.tenant)}`);
+ if (bResp.ok) {
+ const bd = await bResp.json();
+ state.data = bd;
+ applyUserTenants();
+ renderPurchase();
+ }
+ closePurchaseModal();
+};
diff --git a/static/modules/utils.js b/static/modules/utils.js
index 445b8b8..80abc87 100644
--- a/static/modules/utils.js
+++ b/static/modules/utils.js
@@ -147,6 +147,7 @@ function render() {
renderProposals();
renderProducts();
renderFinance();
+ renderPurchase();
if (window.lucide) window.lucide.createIcons();
}
diff --git a/templates/index.html b/templates/index.html
index fc2f71d..b71b605 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -53,6 +53,10 @@