From b9ef0b21526de230afeb9d4c816ceba88be857a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=BB=D1=8C=D1=8F=D1=81=20=D0=A1=D1=83=D0=BB=D1=82?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2?= Date: Tue, 29 Sep 2026 09:15:43 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9D=D0=94=D0=A1=20=D0=B2=20DI2:=20=D0=B4?= =?UTF-8?q?=D0=B5=D1=84=D0=BE=D0=BB=D1=82=D0=BD=D1=8B=D0=B9=20=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D0=B8=D0=BE=D0=B4=20=E2=80=94=20=D0=B2=D1=81=D1=8F=20?= =?UTF-8?q?=D0=B8=D1=81=D1=82=D0=BE=D1=80=D0=B8=D1=8F=20=D0=B2=D0=B8=D1=82?= =?UTF-8?q?=D1=80=D0=B8=D0=BD=D1=8B,=20KPI=20=C2=AB=D0=9A=20=D1=83=D0=BF?= =?UTF-8?q?=D0=BB=D0=B0=D1=82=D0=B5=C2=BB=20=E2=80=94=20=D0=B7=D0=B0=20?= =?UTF-8?q?=D0=BA=D0=B2=D0=B0=D1=80=D1=82=D0=B0=D0=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - summary: без фильтров дат период = весь доступный диапазон nds_monthly (с 2025-01) - «К уплате» теперь всегда квартальный: квартал выбранного периода, если он внутри одного квартала, иначе квартал последнего месяца в данных (НДС не копится между кварталами) - карточки KPI: «Начислено»/«Вычет» за период с подписью, «К уплате» с меткой квартала --- client-di2/src/pages/calculations.tsx | 12 ++++-- server/finance-di2/nds.routes.ts | 59 +++++++++++++++++++++------ 2 files changed, 55 insertions(+), 16 deletions(-) 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,