Files
accounted/lib/reports/balansrapport.ts
T
MattssonandClaude Opus 4.7 a9b43ebeb7 Bug/vat selection warning (#583)
* refactor: update VAT handling logic for non-registered sellers and improve related comments

* chore: gate automated email flows behind 503 responses

Disables user-facing access to invoice payment reminders and salary
payslip email sending. Underlying lib code (reminder-processor,
PDF templates, notification_settings) is preserved for easy re-enable.

- Invoice reminders cron route returns 503; settings UI section removed.
- Payslip send route returns 503; original implementation kept as
  _sendPayslipsImpl for future re-enable.
- Push notifications were already extension-disabled, no change needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: remove Recapt feedback widget

Strips the third-party Recapt SDK and its floating feedback bubble from
the app. The in-app contact form keeps working via the existing email
channel (/api/support/contact). Drops the Recapt entries from the CSP
and the subprocessor list in the privacy policy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: reject meaningless rättelser in correctEntry

Guard against zero-economic-effect corrections in the storno engine:
- Reject when proposed lines net to zero on every account (e.g. 1930
  debit 100 / 1930 credit 100), which would erase the original posting
  without representing any affärshändelse (BFL 5 kap. 5 §).
- Reject when proposed lines are an exact multiset match of the original
  entry — a rättelse must actually change something.

New MeaninglessCorrectionError wired through bookkeepingErrorResponse
(HTTP 400) and the Swedish error translator.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: add date-range picker to resultat- and balansrapport

Adds optional from/to date filtering to the four operational financial
reports (resultatrapport, balansrapport, income-statement, balance-sheet)
so users can view a month, quarter, or custom range inside a fiscal year
without leaving the report. Defaults to YTD; "Hela året" preserves the
prior full-period behaviour (URL-identical, cache-stable).

- trial-balance engine accepts optional fromDate/toDate, rolling prior
  in-period activity into IB and clamping period activity to the window
- 12 API routes accept and validate from_date/to_date query params
- ReportDateRange chip picker persists preset per company, only renders
  on the four relevant tabs
- FiscalYearSelector now emits the period object so the range picker
  has bounds without an extra fetch
- PDF/XLSX filenames reflect the chosen range
- Resultatrapport drops the prior-year column when narrowed (full-year
  vs partial-year would mislead)
- 11 new tests (engine + parser); all existing report tests pass

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: add support for marking journal entries as "no document required"

- Introduced a new sidecar table `journal_entry_no_doc_required` to track entries that do not require separate documentation (e.g., bank fees, interest).
- Implemented API routes for creating and deleting exemptions, including validation and authorization checks.
- Added a toggle component in the UI to allow users to mark entries as exempt, with an optional reason.
- Updated relevant tests to cover the new functionality, including RLS checks and cascading deletes.
- Enhanced existing schemas and types to accommodate the new `vat_amount` field for supplier invoice items.

* fix: address PR review findings on no-doc-required + VAT changes

- pg-real cascade test wraps DELETE in gnubok.allow_delete='true' txn so the
  immutability trigger bypass fires (mirrors delete_last_voucher RPC).
- Clamp supplier-invoice item vat_amount to <= line_total * vat_rate via Zod
  refinement (with 1-öre rounding tolerance) so the manual override can't
  inflate the 2641 debit beyond the statutory ceiling.
- groupVatByRate falls back to line_total * rate when stored vat_amount is 0
  with a positive rate, so legacy/import paths leaving the column at its
  NOT NULL DEFAULT 0 don't silently understate ruta 48.
- ReportDateRange todayIso() and preset endpoints use local date components
  instead of toISOString() (UTC) — fixes the midnight-to-02:00 off-by-one
  that truncated a day from YTD / this-month / this-quarter for Swedish
  users.
- NoDocRequiredToggle restores the previous reason on failed POST/DELETE so
  the rolled-back toggle state stays consistent with the rendered reason.
- Document the company-scoped (not user-scoped) DELETE authorization policy
  on the no-document-required route.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 01:56:09 +02:00

116 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { SupabaseClient } from '@supabase/supabase-js'
import { generateTrialBalance } from './trial-balance'
import type {
BalansrapportReport,
BalansrapportRow,
BalansrapportGroup,
} from '@/types'
const CLASS_LABELS: Record<number, string> = {
1: '1 Tillgångar',
2: '2 Eget kapital, obeskattade reserver, avsättningar och skulder',
}
/**
* Balansrapport — operational balance report.
*
* Lists every account in classes 1–2 with IB, period change, and UB.
* Unlike Balansräkning (formal, ÅRL Bilaga 1), this keeps account numbers
* and is meant for ongoing reconciliation, not for årsbokslut/årsredovisning.
*
* Sign convention: every row is shown debit-positive (debit - credit). Class 1
* accounts (debit balance) render positive; class 2 accounts (credit balance)
* render negative. This matches Fortnox/Visma/Bokio and lets the user verify
* the balance by adding rows: total_assets_ub + total_equity_liabilities_ub
* = beraknat_resultat (the running-year P&L residual, 0 after year-end close).
*/
export async function generateBalansrapport(
supabase: SupabaseClient,
companyId: string,
fiscalPeriodId: string,
options?: { fromDate?: string; toDate?: string }
): Promise<BalansrapportReport> {
const { data: period } = await supabase
.from('fiscal_periods')
.select('period_start, period_end')
.eq('id', fiscalPeriodId)
.eq('company_id', companyId)
.single()
if (!period) {
throw new Error('Fiscal period not found')
}
const effectiveFromDate = options?.fromDate ?? period.period_start
const effectiveToDate = options?.toDate ?? period.period_end
const trialBalance = await generateTrialBalance(supabase, companyId, fiscalPeriodId, {
fromDate: options?.fromDate,
toDate: options?.toDate,
})
const balanceRows = trialBalance.rows.filter((r) => r.account_class === 1 || r.account_class === 2)
const groups: BalansrapportGroup[] = []
for (const klass of [1, 2] as const) {
const groupRows = balanceRows
.filter((r) => r.account_class === klass)
.sort((a, b) => a.account_number.localeCompare(b.account_number))
const rows: BalansrapportRow[] = []
let subtotalIb = 0
let subtotalUb = 0
for (const r of groupRows) {
const ib = signedAmount(r.opening_debit, r.opening_credit)
const ub = signedAmount(r.closing_debit, r.closing_credit)
const change = round2(ub - ib)
if (Math.abs(ib) < 0.005 && Math.abs(ub) < 0.005) continue
rows.push({
account_number: r.account_number,
account_name: r.account_name,
ib: round2(ib),
ub: round2(ub),
period_change: change,
})
subtotalIb += ib
subtotalUb += ub
}
if (rows.length === 0) continue
groups.push({
class: klass,
class_label: CLASS_LABELS[klass],
rows,
subtotal_ib: round2(subtotalIb),
subtotal_ub: round2(subtotalUb),
})
}
const totalAssetsUb = groups.find((g) => g.class === 1)?.subtotal_ub ?? 0
const totalEquityLiabilitiesUb = groups.find((g) => g.class === 2)?.subtotal_ub ?? 0
// Beräknat resultat: the residual on the balance side. With both classes in
// debit-positive sign, assets are positive and eq_liab is negative; their sum
// equals the running-year P&L residual. Trial balance guarantees
// sum_all(debit - credit) = 0, so sum_balance = -sum_pl = revenues - costs.
// After year-end close posts the result into 2099, the residual is 0.
const beraknatResultat = round2(totalAssetsUb + totalEquityLiabilitiesUb)
return {
groups,
total_assets_ub: totalAssetsUb,
total_equity_liabilities_ub: totalEquityLiabilitiesUb,
beraknat_resultat: beraknatResultat,
is_balanced: trialBalance.isBalanced,
period: { start: effectiveFromDate, end: effectiveToDate },
}
}
function signedAmount(debit: number, credit: number): number {
return debit - credit
}
function round2(n: number): number {
return Math.round(n * 100) / 100
}