НДС в DI2: трекер авансовых УПД, доработка графика и виджета «Контроль квартала»
- Новый GET /api/di2/nds/advance-vat-tracker (nds_advance_vat_tracker): KPI «сколько можем сбавить НДС» (оценка сверху 22/122) + топ-15 поставщиков без авансовой УПД; карточка сразу после «Динамики НДС» с обязательными оговорками из спеки - Новый GET /api/di2/nds/quarter-control: redHanging / redNoAdvance («проверить зачёт») / totals / byMarker / ?all=1 — по правкам виджета из спеки - График «Динамика НДС» (месяц): два столбца «К уплате за месяц» и «Накоплено за квартал», кварталы выделены фоновыми полосами (ReferenceArea) - Виджет «Контроль квартала»: легенда с подписями маркеров, итог «N договоров, сумма авансов X», кнопка «Показать все» (полный список с маркерами), отдельный список «Оплачено, аванс не висячий», полный год в датах договоров, overflow-x для таблиц
This commit is contained in:
@@ -230,6 +230,103 @@ export function registerNdsRoutes(app: Express): void {
|
||||
}
|
||||
});
|
||||
|
||||
// Трекер авансовых УПД поставщиков (spec: nds_advance_vat_tracker — «где сбавить НДС»).
|
||||
// Рабочая очередь = status 'НЕТ АВАНСОВОЙ УПД'; строки 'ПОЛУЧЕНО' не показываем.
|
||||
// potential_vat — оценка сверху по 22/122, реальный вычет = НДС из УПД поставщика.
|
||||
app.get("/api/di2/nds/advance-vat-tracker", async (req, res) => {
|
||||
try {
|
||||
const [kpis, rows] = await Promise.all([
|
||||
cachedQuery("nds:adv-vat:kpis", TTL_NDS, () =>
|
||||
query(
|
||||
`SELECT ROUND(SUM(potential_vat), 2)::float8 AS "vatToSave",
|
||||
COUNT(*)::int AS "suppliersWaiting",
|
||||
ROUND(SUM(advance_out), 2)::float8 AS "advancesTotal",
|
||||
MAX(snapshot_date)::text AS "snapshotDate"
|
||||
FROM nds_advance_vat_tracker
|
||||
WHERE snapshot_date = (SELECT MAX(snapshot_date) FROM nds_advance_vat_tracker)
|
||||
AND status = 'НЕТ АВАНСОВОЙ УПД'`
|
||||
)
|
||||
),
|
||||
cachedQuery("nds:adv-vat:rows", TTL_NDS, () =>
|
||||
query(
|
||||
`SELECT supplier, contract,
|
||||
advance_out::float8 AS "advanceOut",
|
||||
potential_vat::float8 AS "potentialVat"
|
||||
FROM nds_advance_vat_tracker
|
||||
WHERE snapshot_date = (SELECT MAX(snapshot_date) FROM nds_advance_vat_tracker)
|
||||
AND status = 'НЕТ АВАНСОВОЙ УПД'
|
||||
ORDER BY potential_vat DESC
|
||||
LIMIT 15`
|
||||
)
|
||||
),
|
||||
]);
|
||||
res.json({ kpis: kpis.rows[0], rows: rows.rows });
|
||||
} catch (err) {
|
||||
handleApiError(res, err);
|
||||
}
|
||||
});
|
||||
|
||||
// Виджет «Контроль квартала»: главный список RED с висячим авансом,
|
||||
// отдельный список RED с нулевым авансом (проверить зачёт), итоги и полный список (?all=1)
|
||||
app.get("/api/di2/nds/quarter-control", async (req, res) => {
|
||||
try {
|
||||
if (String(req.query.all) === "1") {
|
||||
const result = await cachedQuery("nds:quarter-control:all", TTL_NDS, () =>
|
||||
query(
|
||||
`SELECT counterparty, contract,
|
||||
paid::float8 AS paid, shipped::float8 AS shipped,
|
||||
advance_balance::float8 AS "advanceBalance", diff::float8 AS diff, marker
|
||||
FROM nds_contract_marker
|
||||
ORDER BY advance_balance DESC, diff DESC`
|
||||
)
|
||||
);
|
||||
return res.json({ rows: result.rows });
|
||||
}
|
||||
const [redHanging, redNoAdvance, totals, byMarker] = await Promise.all([
|
||||
cachedQuery("nds:quarter-control:red-hanging", TTL_NDS, () =>
|
||||
query(
|
||||
`SELECT counterparty, contract,
|
||||
paid::float8 AS paid, shipped::float8 AS shipped,
|
||||
advance_balance::float8 AS "advanceBalance", diff::float8 AS diff
|
||||
FROM nds_contract_marker
|
||||
WHERE marker = 'RED_NO_SHIPMENT' AND advance_balance > 0
|
||||
ORDER BY paid DESC`
|
||||
)
|
||||
),
|
||||
cachedQuery("nds:quarter-control:red-no-advance", TTL_NDS, () =>
|
||||
query(
|
||||
`SELECT counterparty, contract, paid::float8 AS paid
|
||||
FROM nds_contract_marker
|
||||
WHERE marker = 'RED_NO_SHIPMENT' AND advance_balance = 0
|
||||
ORDER BY paid DESC`
|
||||
)
|
||||
),
|
||||
cachedQuery("nds:quarter-control:totals", TTL_NDS, () =>
|
||||
query(
|
||||
`SELECT COUNT(*)::int AS "totalContracts",
|
||||
ROUND(COALESCE(SUM(advance_balance), 0)::numeric, 2)::float8 AS "totalAdvance"
|
||||
FROM nds_contract_marker`
|
||||
)
|
||||
),
|
||||
cachedQuery("nds:quarter-control:by-marker", TTL_NDS, () =>
|
||||
query(
|
||||
`SELECT marker, COUNT(*)::int AS count
|
||||
FROM nds_contract_marker
|
||||
GROUP BY marker ORDER BY count DESC`
|
||||
)
|
||||
),
|
||||
]);
|
||||
res.json({
|
||||
redHanging: redHanging.rows,
|
||||
redNoAdvance: redNoAdvance.rows,
|
||||
totals: totals.rows[0],
|
||||
byMarker: byMarker.rows,
|
||||
});
|
||||
} catch (err) {
|
||||
handleApiError(res, err);
|
||||
}
|
||||
});
|
||||
|
||||
// Свежесть витрины: последняя успешная синхронизация; stale = данные не обновлялись >36 часов
|
||||
app.get("/api/di2/nds/sync-status", async (req, res) => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user