feat: add option to exclude year-end closing entries in SIE export and related reports (#567)
This commit is contained in:
@@ -1735,6 +1735,7 @@ export default function ImportPage() {
|
||||
const [userId, setUserId] = useState('')
|
||||
const [isSandbox, setIsSandbox] = useState(false)
|
||||
const [exportPeriodId, setExportPeriodId] = useState<string | null>(null)
|
||||
const [exportExcludeClosing, setExportExcludeClosing] = useState(true)
|
||||
const t = useTranslations('import')
|
||||
const router = useRouter()
|
||||
const hasCloudBackup = ENABLED_EXTENSION_IDS.has('cloud-backup')
|
||||
@@ -2042,10 +2043,21 @@ export default function ImportPage() {
|
||||
hideFuturePeriods
|
||||
label={t('export_sie_period_label')}
|
||||
/>
|
||||
<label className="flex items-start gap-2 text-sm text-muted-foreground cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mt-0.5 h-4 w-4 rounded border-border"
|
||||
checked={exportExcludeClosing}
|
||||
onChange={(e) => setExportExcludeClosing(e.target.checked)}
|
||||
/>
|
||||
<span>{t('export_sie_exclude_closing_label')}</span>
|
||||
</label>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (exportPeriodId) {
|
||||
window.open(`/api/reports/sie-export?period_id=${exportPeriodId}`, '_blank')
|
||||
const params = new URLSearchParams({ period_id: exportPeriodId })
|
||||
if (exportExcludeClosing) params.set('exclude_closing', 'true')
|
||||
window.open(`/api/reports/sie-export?${params.toString()}`, '_blank')
|
||||
}
|
||||
}}
|
||||
disabled={!exportPeriodId || isSandbox}
|
||||
|
||||
@@ -10,6 +10,7 @@ export const GET = withRouteContext(
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const periodId = searchParams.get('period_id')
|
||||
const excludeClosing = searchParams.get('exclude_closing') === 'true'
|
||||
|
||||
if (!periodId) {
|
||||
return errorResponseFromCode('REPORT_PERIOD_REQUIRED', log, { requestId })
|
||||
@@ -32,6 +33,7 @@ export const GET = withRouteContext(
|
||||
fiscal_period_id: periodId,
|
||||
company_name: company.company_name || 'Unknown',
|
||||
org_number: company.org_number,
|
||||
exclude_year_end_closing: excludeClosing,
|
||||
})
|
||||
|
||||
return new NextResponse(sieContent, {
|
||||
|
||||
@@ -53,6 +53,8 @@ export const GET = withApiV1<{ params: Promise<{ companyId: string }> }>(
|
||||
})
|
||||
if (!period.ok) return period.response
|
||||
|
||||
const excludeClosing = new URL(request.url).searchParams.get('exclude_closing') === 'true'
|
||||
|
||||
const { data: company, error: companyErr } = await ctx.supabase
|
||||
.from('company_settings')
|
||||
.select('company_name, org_number')
|
||||
@@ -71,6 +73,7 @@ export const GET = withApiV1<{ params: Promise<{ companyId: string }> }>(
|
||||
fiscal_period_id: period.period.id,
|
||||
company_name: (company as { company_name: string | null }).company_name || 'Unknown',
|
||||
org_number: (company as { org_number: string | null }).org_number,
|
||||
exclude_year_end_closing: excludeClosing,
|
||||
}),
|
||||
{ log: ctx.log, requestId: ctx.requestId, reportName: 'sie-export' },
|
||||
)
|
||||
|
||||
@@ -73,6 +73,7 @@ const LABELS = {
|
||||
iban: 'IBAN:',
|
||||
bic: 'BIC/SWIFT:',
|
||||
ocr: 'OCR/Referens:',
|
||||
paymentReference: 'Betalningsreferens:',
|
||||
// Footer
|
||||
orgNoLong: 'Org.nr:',
|
||||
vatRegNo: 'Momsreg.nr:',
|
||||
@@ -126,6 +127,7 @@ const LABELS = {
|
||||
iban: 'IBAN:',
|
||||
bic: 'BIC/SWIFT:',
|
||||
ocr: 'Reference:',
|
||||
paymentReference: 'Payment reference:',
|
||||
orgNoLong: 'Reg. no.:',
|
||||
vatRegNo: 'VAT reg. no.:',
|
||||
// Statutory Swedish phrase — kept verbatim in both locales. Peppol SE-R-005
|
||||
@@ -765,12 +767,18 @@ export function InvoicePDF({ invoice, customer, items, company, originalInvoiceN
|
||||
<Text style={styles.paymentLabel}>{L.dueDate}</Text>
|
||||
<Text style={[styles.paymentValue, { fontWeight: 'bold' }]}>{formatDate(invoice.due_date)}</Text>
|
||||
</View>
|
||||
{(company.invoice_show_ocr ?? true) && (
|
||||
{(company.invoice_show_ocr ?? true) && (company.bankgiro || company.plusgiro) && lang === 'sv' && (
|
||||
<View style={styles.paymentRow}>
|
||||
<Text style={styles.paymentLabel}>{L.ocr}</Text>
|
||||
<Text style={[styles.paymentValue, { fontWeight: 'bold' }]}>{invoice.invoice_number ? generateOcrReference(invoice.invoice_number) : '—'}</Text>
|
||||
</View>
|
||||
)}
|
||||
{lang !== 'sv' && invoice.invoice_number && (
|
||||
<View style={styles.paymentRow}>
|
||||
<Text style={styles.paymentLabel}>{L.paymentReference}</Text>
|
||||
<Text style={[styles.paymentValue, { fontWeight: 'bold' }]}>{invoice.invoice_number}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
|
||||
@@ -16,7 +16,13 @@ export async function generateIncomeStatement(
|
||||
companyId: string,
|
||||
fiscalPeriodId: string
|
||||
): Promise<IncomeStatementReport> {
|
||||
const { rows } = await generateTrialBalance(supabase, companyId, fiscalPeriodId)
|
||||
// Exclude year-end closing entries: after closing, P&L accounts (3-8) are
|
||||
// zeroed by the closing verifikat (8999 → 2099). Including them collapses
|
||||
// the resultaträkning to zero. The income statement must reflect the
|
||||
// pre-closing activity for the year.
|
||||
const { rows } = await generateTrialBalance(supabase, companyId, fiscalPeriodId, {
|
||||
excludeYearEndClosing: true,
|
||||
})
|
||||
|
||||
// Filter to income/expense accounts (class 3-8)
|
||||
const incomeExpenseRows = rows.filter(
|
||||
|
||||
@@ -57,7 +57,7 @@ export async function generateSIEExport(
|
||||
)
|
||||
|
||||
// Fetch all posted journal entries with lines
|
||||
const { data: entries } = await supabase
|
||||
let entriesQuery = supabase
|
||||
.from('journal_entries')
|
||||
.select('*, lines:journal_entry_lines(*)')
|
||||
.eq('company_id', companyId)
|
||||
@@ -65,6 +65,12 @@ export async function generateSIEExport(
|
||||
.in('status', ['posted', 'reversed'])
|
||||
.order('voucher_number')
|
||||
|
||||
if (options.exclude_year_end_closing) {
|
||||
entriesQuery = entriesQuery.neq('source_type', 'year_end')
|
||||
}
|
||||
|
||||
const { data: entries } = await entriesQuery
|
||||
|
||||
// Fetch cost centers and projects for dimension records
|
||||
const { data: costCenters } = await supabase
|
||||
.from('cost_centers')
|
||||
|
||||
@@ -16,7 +16,8 @@ import type { TrialBalanceRow } from '@/types'
|
||||
export async function generateTrialBalance(
|
||||
supabase: SupabaseClient,
|
||||
companyId: string,
|
||||
fiscalPeriodId: string
|
||||
fiscalPeriodId: string,
|
||||
options?: { excludeYearEndClosing?: boolean }
|
||||
): Promise<{
|
||||
rows: TrialBalanceRow[]
|
||||
totalDebit: number
|
||||
@@ -51,7 +52,7 @@ export async function generateTrialBalance(
|
||||
}>(({ from, to }) => {
|
||||
let query = supabase
|
||||
.from('journal_entry_lines')
|
||||
.select('account_number, debit_amount, credit_amount, journal_entries!inner(company_id, fiscal_period_id, status)')
|
||||
.select('account_number, debit_amount, credit_amount, journal_entries!inner(company_id, fiscal_period_id, status, source_type)')
|
||||
.eq('journal_entries.company_id', companyId)
|
||||
.eq('journal_entries.fiscal_period_id', fiscalPeriodId)
|
||||
.in('journal_entries.status', ['posted', 'reversed'])
|
||||
@@ -60,6 +61,10 @@ export async function generateTrialBalance(
|
||||
query = query.neq('journal_entry_id', obEntryId)
|
||||
}
|
||||
|
||||
if (options?.excludeYearEndClosing) {
|
||||
query = query.neq('journal_entries.source_type', 'year_end')
|
||||
}
|
||||
|
||||
return query.range(from, to)
|
||||
})
|
||||
|
||||
|
||||
@@ -3179,6 +3179,7 @@
|
||||
"export_sie_period_label": "Fiscal year",
|
||||
"export_sie_button": "Download SIE",
|
||||
"export_sie_no_period": "Select a fiscal year to export.",
|
||||
"export_sie_exclude_closing_label": "Exclude year-end closing voucher (recommended for eDeklarera and systems that do their own year-end closing)",
|
||||
"export_cloud_title": "Cloud backup to Google Drive"
|
||||
},
|
||||
"empty": {
|
||||
|
||||
@@ -3179,6 +3179,7 @@
|
||||
"export_sie_period_label": "Räkenskapsår",
|
||||
"export_sie_button": "Ladda ner SIE",
|
||||
"export_sie_no_period": "Välj ett räkenskapsår för att exportera.",
|
||||
"export_sie_exclude_closing_label": "Exkludera bokslutsverifikat (rekommenderas för eDeklarera och system som gör eget bokslut)",
|
||||
"export_cloud_title": "Säkerhetskopia till Google Drive"
|
||||
},
|
||||
"empty": {
|
||||
|
||||
@@ -1387,6 +1387,13 @@ export interface SIEExportOptions {
|
||||
company_name: string
|
||||
org_number: string | null
|
||||
program_name?: string
|
||||
/**
|
||||
* When true, omit year-end closing verifikat (source_type = 'year_end')
|
||||
* from #VER and from #RES/#UB calculations. Use when handing the file
|
||||
* to systems (e.g. eDeklarera) that do their own closing — including
|
||||
* our closing entry would zero out the P&L accounts.
|
||||
*/
|
||||
exclude_year_end_closing?: boolean
|
||||
}
|
||||
|
||||
// Input types for creating entries
|
||||
|
||||
Reference in New Issue
Block a user