fix(display): 证据金额去尾零与界面显示修复 #5
+14
-3
@@ -297,6 +297,14 @@ def query(request, dataset_id):
|
|||||||
return JsonResponse(analyze(ds, request.member, body(request), pref))
|
return JsonResponse(analyze(ds, request.member, body(request), pref))
|
||||||
|
|
||||||
|
|
||||||
|
def amount_text(value):
|
||||||
|
"""金额展示文本:去掉定点数存储尺度产生的多余尾零(如 10.000000000000000000000000 → 10)。"""
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
text = format(value.normalize(), "f")
|
||||||
|
return "0" if text == "-0" else text
|
||||||
|
|
||||||
|
|
||||||
@endpoint(("POST",))
|
@endpoint(("POST",))
|
||||||
def evidence(request, dataset_id):
|
def evidence(request, dataset_id):
|
||||||
ds = dataset(request, dataset_id, True)
|
ds = dataset(request, dataset_id, True)
|
||||||
@@ -314,6 +322,9 @@ def evidence(request, dataset_id):
|
|||||||
"source_row", "period", "dimensions", "revenue", "cost", "gross", "raw"
|
"source_row", "period", "dimensions", "revenue", "cost", "gross", "raw"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
for row in rows:
|
||||||
|
for field in ("revenue", "cost", "gross"):
|
||||||
|
row[field] = amount_text(row[field])
|
||||||
# Raw patient/customer identifiers require an explicit administrator role.
|
# Raw patient/customer identifiers require an explicit administrator role.
|
||||||
if request.member.role != "admin":
|
if request.member.role != "admin":
|
||||||
for row in rows:
|
for row in rows:
|
||||||
@@ -412,9 +423,9 @@ def export(request, dataset_id):
|
|||||||
row.source_row,
|
row.source_row,
|
||||||
row.period,
|
row.period,
|
||||||
*[row.dimensions.get(d["key"], "") for d in dims],
|
*[row.dimensions.get(d["key"], "") for d in dims],
|
||||||
row.revenue,
|
amount_text(row.revenue),
|
||||||
row.cost,
|
amount_text(row.cost),
|
||||||
row.gross,
|
amount_text(row.gross),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
if workbook:
|
if workbook:
|
||||||
|
|||||||
+100
-80
@@ -45,6 +45,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
format,
|
format,
|
||||||
labels,
|
labels,
|
||||||
|
roleLabels,
|
||||||
type Analysis,
|
type Analysis,
|
||||||
type Context,
|
type Context,
|
||||||
type Dataset,
|
type Dataset,
|
||||||
@@ -308,6 +309,23 @@ export function App() {
|
|||||||
setPath([]);
|
setPath([]);
|
||||||
setModal(null);
|
setModal(null);
|
||||||
}
|
}
|
||||||
|
// 全部分组都为空的指标列(如数据集没有成本/ROI)不展示,避免占位挤压可用列。
|
||||||
|
const metricKeys = useMemo(() => {
|
||||||
|
const candidates = [
|
||||||
|
...new Set<Metric>([
|
||||||
|
"revenue",
|
||||||
|
"gross",
|
||||||
|
"cost",
|
||||||
|
"roi",
|
||||||
|
"customers",
|
||||||
|
ctx?.metric || "revenue",
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
const available = candidates.filter(
|
||||||
|
(m) => m === "revenue" || (result?.rows || []).some((r) => r.values[m]?.current != null),
|
||||||
|
);
|
||||||
|
return available.length ? available : (["revenue"] as Metric[]);
|
||||||
|
}, [result, ctx]);
|
||||||
const columns = useMemo<ColDef<Row>[]>(
|
const columns = useMemo<ColDef<Row>[]>(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
@@ -327,23 +345,14 @@ export function App() {
|
|||||||
</button>
|
</button>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
...[
|
...metricKeys.map((metric) => ({
|
||||||
...new Set<Metric>([
|
|
||||||
"revenue",
|
|
||||||
"gross",
|
|
||||||
"cost",
|
|
||||||
"roi",
|
|
||||||
"customers",
|
|
||||||
ctx?.metric || "revenue",
|
|
||||||
]),
|
|
||||||
].map((metric) => ({
|
|
||||||
colId: metric,
|
colId: metric,
|
||||||
headerName: labels[metric],
|
headerName: labels[metric],
|
||||||
minWidth: 130,
|
minWidth: 130,
|
||||||
valueGetter: (p: { data?: Row }) =>
|
valueGetter: (p: { data?: Row }) =>
|
||||||
p.data?.values[metric].current == null
|
p.data?.values[metric]?.current == null
|
||||||
? null
|
? null
|
||||||
: Number(p.data.values[metric].current),
|
: Number(p.data.values[metric]?.current),
|
||||||
valueFormatter: (p: { value: unknown }) =>
|
valueFormatter: (p: { value: unknown }) =>
|
||||||
format(p.value as number, metric),
|
format(p.value as number, metric),
|
||||||
cellClass: metric === ctx?.metric ? "metric-column" : "",
|
cellClass: metric === ctx?.metric ? "metric-column" : "",
|
||||||
@@ -396,24 +405,34 @@ export function App() {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[ds, ctx, path],
|
[ds, ctx, path, metricKeys],
|
||||||
);
|
);
|
||||||
const rows = (result?.rows || []).filter(
|
const rows = (result?.rows || []).filter(
|
||||||
(r) => (screen !== "anomalies" || r.anomaly) && r.label.includes(search),
|
(r) => (screen !== "anomalies" || r.anomaly) && r.label.includes(search),
|
||||||
);
|
);
|
||||||
const metric = ctx?.metric || "revenue";
|
const metric = ctx?.metric || "revenue";
|
||||||
|
const compactAxis = (value: number) => {
|
||||||
|
const abs = Math.abs(value);
|
||||||
|
if (abs >= 1e8) return `${value / 1e8}亿`;
|
||||||
|
if (abs >= 1e4) return `${value / 1e4}万`;
|
||||||
|
return String(value);
|
||||||
|
};
|
||||||
const trend = useMemo(
|
const trend = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
color: ["#197568", "#5794c9"],
|
color: ["#197568", "#5794c9"],
|
||||||
tooltip: { trigger: "axis" },
|
tooltip: { trigger: "axis" },
|
||||||
legend: { bottom: 0 },
|
legend: { bottom: 0 },
|
||||||
grid: { top: 25, left: 65, right: 25, bottom: 65 },
|
grid: { top: 25, left: 85, right: 25, bottom: 65 },
|
||||||
xAxis: {
|
xAxis: {
|
||||||
type: "category",
|
type: "category",
|
||||||
data: result?.trend.map((r) => r.period),
|
data: result?.trend.map((r) => r.period),
|
||||||
axisLabel: { color: "#73827b" },
|
axisLabel: { color: "#73827b", hideOverlap: true },
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: "value",
|
||||||
|
splitLine: { lineStyle: { color: "#ecf0ee" } },
|
||||||
|
axisLabel: { hideOverlap: true },
|
||||||
},
|
},
|
||||||
yAxis: { type: "value", splitLine: { lineStyle: { color: "#ecf0ee" } } },
|
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
name: "实际收入",
|
name: "实际收入",
|
||||||
@@ -443,7 +462,11 @@ export function App() {
|
|||||||
() => ({
|
() => ({
|
||||||
tooltip: { trigger: "axis" },
|
tooltip: { trigger: "axis" },
|
||||||
grid: { top: 15, left: 95, right: 30, bottom: 30 },
|
grid: { top: 15, left: 95, right: 30, bottom: 30 },
|
||||||
xAxis: { type: "value", splitLine: { lineStyle: { color: "#ecf0ee" } } },
|
xAxis: {
|
||||||
|
type: "value",
|
||||||
|
splitLine: { lineStyle: { color: "#ecf0ee" } },
|
||||||
|
axisLabel: { hideOverlap: true, formatter: compactAxis },
|
||||||
|
},
|
||||||
yAxis: {
|
yAxis: {
|
||||||
type: "category",
|
type: "category",
|
||||||
inverse: true,
|
inverse: true,
|
||||||
@@ -878,6 +901,8 @@ export function App() {
|
|||||||
missing_gross: "毛利缺失",
|
missing_gross: "毛利缺失",
|
||||||
missing_customer: "客户标识缺失",
|
missing_customer: "客户标识缺失",
|
||||||
blank_rows: "空白行",
|
blank_rows: "空白行",
|
||||||
|
g_l_mismatch: "应收金额两列不一致",
|
||||||
|
i_n_mismatch: "实收与实际收入不一致",
|
||||||
} as Record<string, string>
|
} as Record<string, string>
|
||||||
)[k] || k}
|
)[k] || k}
|
||||||
<strong>{v.toLocaleString()}</strong>
|
<strong>{v.toLocaleString()}</strong>
|
||||||
@@ -1030,16 +1055,11 @@ export function App() {
|
|||||||
)?.name
|
)?.name
|
||||||
}
|
}
|
||||||
</th>
|
</th>
|
||||||
{(
|
{metricKeys
|
||||||
[
|
.filter((m) => m !== "customers")
|
||||||
"revenue",
|
.map((m) => (
|
||||||
"gross",
|
<th key={m}>{labels[m]}</th>
|
||||||
"cost",
|
))}
|
||||||
"roi",
|
|
||||||
] as Metric[]
|
|
||||||
).map((m) => (
|
|
||||||
<th key={m}>{labels[m]}</th>
|
|
||||||
))}
|
|
||||||
<th>状态</th>
|
<th>状态</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -1047,15 +1067,10 @@ export function App() {
|
|||||||
{result.rows.slice(0, 12).map((r) => (
|
{result.rows.slice(0, 12).map((r) => (
|
||||||
<tr key={r.label}>
|
<tr key={r.label}>
|
||||||
<th>{r.label}</th>
|
<th>{r.label}</th>
|
||||||
{(
|
{metricKeys
|
||||||
[
|
.filter((m) => m !== "customers")
|
||||||
"revenue",
|
.map((m) => {
|
||||||
"gross",
|
const v = r.values[m]?.change;
|
||||||
"cost",
|
|
||||||
"roi",
|
|
||||||
] as Metric[]
|
|
||||||
).map((m) => {
|
|
||||||
const v = r.values[m].change;
|
|
||||||
return (
|
return (
|
||||||
<td key={m}>
|
<td key={m}>
|
||||||
<button
|
<button
|
||||||
@@ -1069,7 +1084,7 @@ export function App() {
|
|||||||
: "heat-positive"
|
: "heat-positive"
|
||||||
}
|
}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCtx({ ...ctx, metric: m });
|
setCtx({ ...ctx, metric: m as Metric });
|
||||||
setSelected(r);
|
setSelected(r);
|
||||||
setScreen("anomalies");
|
setScreen("anomalies");
|
||||||
}}
|
}}
|
||||||
@@ -1227,48 +1242,53 @@ export function App() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid-layout">
|
<div className="grid-layout">
|
||||||
<div className="grid">
|
{rows.length ? (
|
||||||
<AgGridReact<Row>
|
<div className="grid">
|
||||||
theme={gridTheme}
|
<AgGridReact<Row>
|
||||||
rowData={rows}
|
theme={gridTheme}
|
||||||
columnDefs={columns}
|
rowData={rows}
|
||||||
defaultColDef={{
|
columnDefs={columns}
|
||||||
sortable: true,
|
defaultColDef={{
|
||||||
filter: true,
|
sortable: true,
|
||||||
resizable: true,
|
filter: true,
|
||||||
}}
|
resizable: true,
|
||||||
pagination
|
}}
|
||||||
paginationPageSize={20}
|
pagination
|
||||||
paginationPageSizeSelector={[20, 50, 100]}
|
paginationPageSize={20}
|
||||||
getRowId={(p) => p.data.label}
|
paginationPageSizeSelector={[20, 50, 100]}
|
||||||
onRowClicked={(e) => setSelected(e.data)}
|
getRowId={(p) => p.data.label}
|
||||||
localeText={{
|
onRowClicked={(e) => setSelected(e.data)}
|
||||||
noRowsToShow: "没有符合条件的分组",
|
localeText={{
|
||||||
page: "页",
|
page: "页",
|
||||||
of: "/",
|
of: "/",
|
||||||
to: "至",
|
to: "至",
|
||||||
more: "更多",
|
more: "更多",
|
||||||
nextPage: "下一页",
|
nextPage: "下一页",
|
||||||
previousPage: "上一页",
|
previousPage: "上一页",
|
||||||
firstPage: "首页",
|
firstPage: "首页",
|
||||||
lastPage: "末页",
|
lastPage: "末页",
|
||||||
pageSizeSelectorLabel: "每页",
|
pageSizeSelectorLabel: "每页",
|
||||||
}}
|
}}
|
||||||
{...(enterprise
|
{...(enterprise
|
||||||
? {
|
? {
|
||||||
cellSelection: true,
|
cellSelection: true,
|
||||||
sideBar: "columns",
|
sideBar: "columns",
|
||||||
statusBar: {
|
statusBar: {
|
||||||
statusPanels: [
|
statusPanels: [
|
||||||
{
|
{
|
||||||
statusPanel: "agTotalRowCountComponent",
|
statusPanel: "agTotalRowCountComponent",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {})}
|
: {})}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<p role="status" className="grid-empty">
|
||||||
|
没有符合条件的分组
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
{selected && (
|
{selected && (
|
||||||
<aside className="explanation">
|
<aside className="explanation">
|
||||||
<div className="section-heading">
|
<div className="section-heading">
|
||||||
@@ -1472,7 +1492,7 @@ export function App() {
|
|||||||
<tr key={c.index}>
|
<tr key={c.index}>
|
||||||
<td>{c.name}</td>
|
<td>{c.name}</td>
|
||||||
<td>{c.description}</td>
|
<td>{c.description}</td>
|
||||||
<td>{c.role}</td>
|
<td>{roleLabels[c.role] || c.role}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ChevronLeft, ChevronRight, Download, Save, Trash2, X } from "lucide-rea
|
|||||||
import { api } from "./api";
|
import { api } from "./api";
|
||||||
import {
|
import {
|
||||||
labels,
|
labels,
|
||||||
|
roleLabels,
|
||||||
type AIConfig,
|
type AIConfig,
|
||||||
type Context,
|
type Context,
|
||||||
type Dataset,
|
type Dataset,
|
||||||
@@ -165,7 +166,9 @@ export function ImportDialog({
|
|||||||
"dimension",
|
"dimension",
|
||||||
"ignore",
|
"ignore",
|
||||||
].map((r) => (
|
].map((r) => (
|
||||||
<option key={r}>{r}</option>
|
<option key={r} value={r}>
|
||||||
|
{roleLabels[r] || r}
|
||||||
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
@@ -367,6 +370,7 @@ export function EvidenceDialog({
|
|||||||
};
|
};
|
||||||
}, [ds.id, ctx, side, page]);
|
}, [ds.id, ctx, side, page]);
|
||||||
function selectSide(value: "current" | "baseline") {
|
function selectSide(value: "current" | "baseline") {
|
||||||
|
if (value === side && page === 0) return;
|
||||||
setResult(undefined);
|
setResult(undefined);
|
||||||
setPage(0);
|
setPage(0);
|
||||||
setSide(value);
|
setSide(value);
|
||||||
@@ -406,7 +410,7 @@ export function EvidenceDialog({
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<div className="table-scroll">
|
<div className="table-scroll">
|
||||||
<table>
|
<table className="evidence-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>来源行</th>
|
<th>来源行</th>
|
||||||
|
|||||||
+22
-7
@@ -484,7 +484,7 @@ a {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
padding: 20px 20px 16px;
|
padding: 18px 16px 14px;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
background: white;
|
background: white;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -510,12 +510,13 @@ a {
|
|||||||
}
|
}
|
||||||
.kpi strong {
|
.kpi strong {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
font-size: 24px;
|
font-size: 21px;
|
||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
font-weight: 620;
|
font-weight: 620;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
white-space: normal;
|
white-space: nowrap;
|
||||||
overflow-wrap: anywhere;
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
.kpi-bottom {
|
.kpi-bottom {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1271,9 +1272,6 @@ details {
|
|||||||
.kpi {
|
.kpi {
|
||||||
padding: 17px 13px;
|
padding: 17px 13px;
|
||||||
}
|
}
|
||||||
.kpi strong {
|
|
||||||
font-size: 21px;
|
|
||||||
}
|
|
||||||
.chart-band {
|
.chart-band {
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
}
|
}
|
||||||
@@ -1562,3 +1560,20 @@ details {
|
|||||||
scroll-behavior: auto !important;
|
scroll-behavior: auto !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.grid-empty {
|
||||||
|
margin: 0;
|
||||||
|
padding: 40px 0;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--muted, #5b6675);
|
||||||
|
border: 1px dashed var(--line);
|
||||||
|
border-radius: 6px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evidence-table {
|
||||||
|
min-width: 960px;
|
||||||
|
}
|
||||||
|
.evidence-table th {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,6 +32,20 @@ export interface Column {
|
|||||||
key: string;
|
key: string;
|
||||||
confidence: number;
|
confidence: number;
|
||||||
}
|
}
|
||||||
|
export const roleLabels: Record<string, string> = {
|
||||||
|
period: "期间",
|
||||||
|
year: "年份",
|
||||||
|
month: "月份",
|
||||||
|
dimension: "维度",
|
||||||
|
customer: "客户标识",
|
||||||
|
quantity: "数量",
|
||||||
|
revenue: "实际收入",
|
||||||
|
cost: "成本",
|
||||||
|
gross: "毛利贡献",
|
||||||
|
list_amount: "应收金额",
|
||||||
|
discount: "折扣金额",
|
||||||
|
ignore: "未使用",
|
||||||
|
};
|
||||||
export interface Mapping {
|
export interface Mapping {
|
||||||
sheet: string;
|
sheet: string;
|
||||||
header: number;
|
header: number;
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ test("桌面:真实导入、异常、下钻、证据、设置、Copilot、导
|
|||||||
});
|
});
|
||||||
await uploaded;
|
await uploaded;
|
||||||
const dialog = page.getByRole("dialog");
|
const dialog = page.getByRole("dialog");
|
||||||
await expect(dialog.getByRole("heading", { name: "确认导入字段" })).toBeVisible();
|
await expect(
|
||||||
|
dialog.getByRole("heading", { name: "确认导入字段" }),
|
||||||
|
).toBeVisible();
|
||||||
await dialog.getByRole("button", { name: "确认并导入" }).click();
|
await dialog.getByRole("button", { name: "确认并导入" }).click();
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole("heading", { name: "集团经营总览", exact: true }),
|
page.getByRole("heading", { name: "集团经营总览", exact: true }),
|
||||||
@@ -85,7 +87,14 @@ test("桌面:真实导入、异常、下钻、证据、设置、Copilot、导
|
|||||||
await expect(evidenceDialog).toContainText("1 条");
|
await expect(evidenceDialog).toContainText("1 条");
|
||||||
await expect(evidenceDialog).toContainText("2026-08");
|
await expect(evidenceDialog).toContainText("2026-08");
|
||||||
await expect(evidenceDialog).toContainText("CSV");
|
await expect(evidenceDialog).toContainText("CSV");
|
||||||
await evidenceDialog.getByRole("button", { name: "基期", exact: true }).click();
|
await evidenceDialog
|
||||||
|
.getByRole("button", { name: "本期", exact: true })
|
||||||
|
.click();
|
||||||
|
await expect(evidenceDialog).toContainText("2026-08");
|
||||||
|
await expect(evidenceDialog).not.toContainText("加载中");
|
||||||
|
await evidenceDialog
|
||||||
|
.getByRole("button", { name: "基期", exact: true })
|
||||||
|
.click();
|
||||||
await expect(evidenceDialog).toContainText("2025-08");
|
await expect(evidenceDialog).toContainText("2025-08");
|
||||||
await evidenceDialog.getByLabel("关闭", { exact: true }).click();
|
await evidenceDialog.getByLabel("关闭", { exact: true }).click();
|
||||||
const evidenceOffsets: number[] = [];
|
const evidenceOffsets: number[] = [];
|
||||||
@@ -111,10 +120,17 @@ test("桌面:真实导入、异常、下钻、证据、设置、Copilot、导
|
|||||||
});
|
});
|
||||||
await page.getByLabel("查看医生A证据", { exact: true }).click();
|
await page.getByLabel("查看医生A证据", { exact: true }).click();
|
||||||
await expect(evidenceDialog).toContainText("第 1-50 条,共 51 条");
|
await expect(evidenceDialog).toContainText("第 1-50 条,共 51 条");
|
||||||
await evidenceDialog.getByRole("button", { name: "下一页", exact: true }).click();
|
await evidenceDialog
|
||||||
|
.getByRole("button", { name: "下一页", exact: true })
|
||||||
|
.click();
|
||||||
await expect(evidenceDialog).toContainText("第 51-51 条,共 51 条");
|
await expect(evidenceDialog).toContainText("第 51-51 条,共 51 条");
|
||||||
await expect(evidenceDialog).toContainText("2026-09");
|
await expect(evidenceDialog).toContainText("2026-09");
|
||||||
expect(evidenceOffsets).toEqual([0, 50]);
|
expect(evidenceOffsets).toEqual([0, 50]);
|
||||||
|
await evidenceDialog
|
||||||
|
.getByRole("button", { name: "基期", exact: true })
|
||||||
|
.click();
|
||||||
|
await expect(evidenceDialog).toContainText("第 1-50 条,共 51 条");
|
||||||
|
await expect(evidenceDialog).not.toContainText("2026-09");
|
||||||
await evidenceDialog.getByLabel("关闭", { exact: true }).click();
|
await evidenceDialog.getByLabel("关闭", { exact: true }).click();
|
||||||
await page
|
await page
|
||||||
.getByRole("navigation", { name: "分析路径" })
|
.getByRole("navigation", { name: "分析路径" })
|
||||||
|
|||||||
+31
-2
@@ -133,6 +133,35 @@ def test_evidence_pagination_returns_stable_pages_and_rejects_invalid_bounds(wor
|
|||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
def test_evidence_amounts_trim_storage_scale(workspace):
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from finance.views import amount_text
|
||||||
|
|
||||||
|
assert amount_text(Decimal("10.000000000000000000000000")) == "10"
|
||||||
|
assert amount_text(Decimal("10.500000000000000000000000")) == "10.5"
|
||||||
|
assert amount_text(Decimal("0E-24")) == "0"
|
||||||
|
assert amount_text(None) is None
|
||||||
|
|
||||||
|
ds, _, client, *_ = workspace
|
||||||
|
response = client.post(
|
||||||
|
f"/api/datasets/{ds.pk}/evidence",
|
||||||
|
data={
|
||||||
|
"start": "2026-08",
|
||||||
|
"end": "2026-08",
|
||||||
|
"comparison": "yoy",
|
||||||
|
"group": "department",
|
||||||
|
"side": "current",
|
||||||
|
"limit": 1,
|
||||||
|
},
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
row = response.json()["rows"][0]
|
||||||
|
assert row["revenue"] == "80"
|
||||||
|
assert row["cost"] == "30"
|
||||||
|
assert row["gross"] == "50"
|
||||||
|
|
||||||
|
|
||||||
def test_settings_revision_and_invalid_metric_are_rejected(workspace):
|
def test_settings_revision_and_invalid_metric_are_rejected(workspace):
|
||||||
ds, _, client, *_ = workspace
|
ds, _, client, *_ = workspace
|
||||||
url = f"/api/datasets/{ds.pk}/settings"
|
url = f"/api/datasets/{ds.pk}/settings"
|
||||||
@@ -169,8 +198,8 @@ def test_csv_export_neutralizes_formula_injection_and_preserves_exact_values(wor
|
|||||||
assert response.streaming
|
assert response.streaming
|
||||||
content = b"".join(response.streaming_content).decode("utf-8-sig")
|
content = b"".join(response.streaming_content).decode("utf-8-sig")
|
||||||
assert "'=1+1" in content
|
assert "'=1+1" in content
|
||||||
assert ",-10.000000000000000000000000," in content
|
assert ",'-10,5,'-15" in content
|
||||||
assert "'-10.000000000000000000000000" not in content
|
assert "00000000000000" not in content
|
||||||
assert "traceiq-evidence.csv" in response["Content-Disposition"]
|
assert "traceiq-evidence.csv" in response["Content-Disposition"]
|
||||||
|
|
||||||
xlsx = client.post(
|
xlsx = client.post(
|
||||||
|
|||||||
Reference in New Issue
Block a user