diff --git a/client-di2/src/pages/calculations.tsx b/client-di2/src/pages/calculations.tsx
index 3e1222e..85d393f 100644
--- a/client-di2/src/pages/calculations.tsx
+++ b/client-di2/src/pages/calculations.tsx
@@ -645,7 +645,8 @@ function VatTab({ data, quarters, balance, sync, markers }: { data: any; quarter
const salesByRate = (data.byRate ?? []).filter((r: any) => r.direction === "sales");
const purchasesByRate = (data.byRate ?? []).filter((r: any) => r.direction === "purchases");
- const totals = data.totals ?? { accrued: 0, deducted: 0, toPay: 0 };
+ const totals = data.totals ?? { accrued: 0, deducted: 0 };
+ const quarter = data.currentQuarter ?? null;
const redRows: any[] = markers?.rows ?? [];
const markerCounts: any[] = markers?.byMarker ?? [];
@@ -661,6 +662,7 @@ function VatTab({ data, quarters, balance, sync, markers }: { data: any; quarter
Начислено
{formatCurrency(totals.accrued)}
+ за период{data.dateFrom && data.dateTo ? ` ${data.dateFrom} — ${data.dateTo}` : ""}
@@ -672,6 +674,7 @@ function VatTab({ data, quarters, balance, sync, markers }: { data: any; quarter
Вычет
{formatCurrency(totals.deducted)}
+ за период
@@ -682,8 +685,11 @@ function VatTab({ data, quarters, balance, sync, markers }: { data: any; quarter
К уплате
- = 0 ? "text-destructive" : "text-success"}`} data-testid="text-vat-to-pay">
- {formatCurrency(totals.toPay)}
+
= 0 ? "text-destructive" : "text-success"}`} data-testid="text-vat-to-pay">
+ {formatCurrency(quarter?.toPay ?? 0)}
+
+
+ {quarter ? `квартал ${formatQuarterLabel(quarter.dateFrom)}` : ""}
diff --git a/server/finance-di2/nds.routes.ts b/server/finance-di2/nds.routes.ts
index f9d9b94..9ed79c5 100644
--- a/server/finance-di2/nds.routes.ts
+++ b/server/finance-di2/nds.routes.ts
@@ -22,37 +22,69 @@ function strParam(v: unknown): string | null {
return typeof v === "string" && v.trim() ? v.trim() : null;
}
-// Границы периода: из query или текущий календарный квартал (НДС — квартальный налог)
-async function resolvePeriod(dateFrom: unknown, dateTo: unknown): Promise<{ from: string; to: string }> {
+// Границы периода: из query или вся история витрины (дефолт).
+// Отдельно возвращает границы квартала для KPI «К уплате»: квартал явного периода,
+// если он внутри одного квартала, иначе квартал последнего месяца в данных.
+async function resolvePeriod(dateFrom: unknown, dateTo: unknown): Promise<{ from: string; to: string; quarter: { from: string; to: string } }> {
const fromParam = strParam(dateFrom);
const toParam = strParam(dateTo);
- if (fromParam && toParam) return { from: fromParam, to: toParam };
- const bounds = await cachedQuery("nds:period-bounds", TTL_NDS, () =>
- query(`SELECT date_trunc('quarter', CURRENT_DATE)::date AS qstart,
- (date_trunc('quarter', CURRENT_DATE) + interval '3 months' - interval '1 day')::date AS qend`)
+ const fmt = (d: unknown): string => (typeof d === "string" ? d : new Date(d as any).toISOString().slice(0, 10));
+ const bounds = await cachedQuery("nds:data-bounds", TTL_NDS, () =>
+ query(
+ `SELECT MIN(month)::date AS dfrom,
+ (date_trunc('quarter', MAX(month)) + interval '3 months' - interval '1 day')::date AS dto,
+ date_trunc('quarter', MAX(month))::date AS qfrom,
+ (date_trunc('quarter', MAX(month)) + interval '3 months' - interval '1 day')::date AS qto
+ FROM nds_monthly`
+ )
);
const row = bounds.rows[0] as any;
- const fmt = (d: any) => (typeof d === "string" ? d : new Date(d).toISOString().slice(0, 10));
- return { from: fromParam || fmt(row.qstart), to: toParam || fmt(row.qend) };
+ const quarter = { from: fmt(row.qfrom), to: fmt(row.qto) };
+ if (fromParam && toParam) {
+ const q = await cachedQuery(stableKey("nds:explicit-quarter", { fromParam, toParam }), TTL_NDS, () =>
+ query(
+ `SELECT date_trunc('quarter', $1::date)::date AS qfrom,
+ (date_trunc('quarter', $2::date) + interval '3 months' - interval '1 day')::date AS qto,
+ date_trunc('quarter', $1::date) = date_trunc('quarter', $2::date) AS same`,
+ [fromParam, toParam]
+ )
+ );
+ const qr = q.rows[0] as any;
+ if (qr.same) {
+ quarter.from = fmt(qr.qfrom);
+ quarter.to = fmt(qr.qto);
+ }
+ }
+ return { from: fromParam || fmt(row.dfrom), to: toParam || fmt(row.dto), quarter };
}
export function registerNdsRoutes(app: Express): void {
- // Сводка за период: KPI, помесячная динамика с накопительным итогом внутри квартала,
- // разрез по ставкам, структура начислений по типам операций, доля авансов
+ // Сводка за период: помесячная динамика с накопительным итогом внутри квартала,
+ // разрез по ставкам, структура начислений по типам операций, доля авансов.
+ // KPI: начислено/вычет — за период; «к уплате» — за квартал (currentQuarter), НДС не копится между кварталами.
app.get("/api/di2/nds/summary", async (req, res) => {
try {
- const { from, to } = await resolvePeriod(req.query.dateFrom, req.query.dateTo);
+ const { from, to, quarter } = await resolvePeriod(req.query.dateFrom, req.query.dateTo);
const params = [from, to];
+ const qParams = [quarter.from, quarter.to];
- const [totals, monthly, byRate, events, advances] = await Promise.all([
+ const [totals, quarterTotals, monthly, byRate, events, advances] = await Promise.all([
cachedQuery(stableKey("nds:summary:totals", req.query), TTL_NDS, () =>
+ query(
+ `SELECT COALESCE(SUM(vat) FILTER (WHERE direction = 'sales'), 0)::float8 AS accrued,
+ COALESCE(SUM(vat) FILTER (WHERE direction = 'purchases'), 0)::float8 AS deducted
+ FROM nds_monthly WHERE month >= $1::date AND month <= $2::date`,
+ params
+ )
+ ),
+ cachedQuery(`nds:summary:quarter:${quarter.from}:${quarter.to}`, TTL_NDS, () =>
query(
`SELECT COALESCE(SUM(vat) FILTER (WHERE direction = 'sales'), 0)::float8 AS accrued,
COALESCE(SUM(vat) FILTER (WHERE direction = 'purchases'), 0)::float8 AS deducted,
(COALESCE(SUM(vat) FILTER (WHERE direction = 'sales'), 0)
- COALESCE(SUM(vat) FILTER (WHERE direction = 'purchases'), 0))::float8 AS "toPay"
FROM nds_monthly WHERE month >= $1::date AND month <= $2::date`,
- params
+ qParams
)
),
cachedQuery(stableKey("nds:summary:monthly", req.query), TTL_NDS, () =>
@@ -110,6 +142,7 @@ export function registerNdsRoutes(app: Express): void {
dateFrom: from,
dateTo: to,
totals: totals.rows[0],
+ currentQuarter: { dateFrom: quarter.from, dateTo: quarter.to, ...quarterTotals.rows[0] },
monthly: monthly.rows,
byRate: byRate.rows,
events: events.rows,