Totalt
diff --git a/app/api/bookkeeping/journal-entries/__tests__/route.test.ts b/app/api/bookkeeping/journal-entries/__tests__/route.test.ts
index 84982d03..fc47189d 100644
--- a/app/api/bookkeeping/journal-entries/__tests__/route.test.ts
+++ b/app/api/bookkeeping/journal-entries/__tests__/route.test.ts
@@ -76,6 +76,8 @@ describe('GET /api/bookkeeping/journal-entries', () => {
date_to: '2024-12-31',
limit: '10',
offset: '5',
+ // Strict period filtering — exercises the PostgREST path, not the RPC.
+ include_related: 'false',
},
})
const response = await GET(request)
@@ -85,6 +87,42 @@ describe('GET /api/bookkeeping/journal-entries', () => {
expect(mockSupabase.from).toHaveBeenCalledWith('journal_entries')
})
+ it('uses RPC with include_related when period_id is set', async () => {
+ const rpcRows = [
+ {
+ entry: { ...makeJournalEntry({ id: 'je-1' }), out_of_period: false },
+ total_count: 2,
+ },
+ {
+ entry: { ...makeJournalEntry({ id: 'je-2' }), out_of_period: true },
+ total_count: 2,
+ },
+ ]
+ enqueue({ data: rpcRows, error: null })
+
+ const request = createMockRequest('/api/bookkeeping/journal-entries', {
+ searchParams: { period_id: 'period-1' },
+ })
+ const response = await GET(request)
+ const { status, body } = await parseJsonResponse<{
+ data: Array<{ id: string; out_of_period?: boolean }>
+ count: number
+ }>(response)
+
+ expect(status).toBe(200)
+ expect(mockSupabase.rpc).toHaveBeenCalledWith(
+ 'list_fiscal_period_entries_with_related',
+ expect.objectContaining({
+ p_company_id: 'company-1',
+ p_period_id: 'period-1',
+ p_include_related: true,
+ })
+ )
+ expect(body.data).toHaveLength(2)
+ expect(body.data[1].out_of_period).toBe(true)
+ expect(body.count).toBe(2)
+ })
+
it('returns 500 on database error', async () => {
enqueue({ data: null, error: { message: 'DB error' } })
diff --git a/app/api/bookkeeping/journal-entries/route.ts b/app/api/bookkeeping/journal-entries/route.ts
index 652400f1..0ef844ac 100644
--- a/app/api/bookkeeping/journal-entries/route.ts
+++ b/app/api/bookkeeping/journal-entries/route.ts
@@ -27,8 +27,38 @@ export async function GET(request: Request) {
const dateFrom = searchParams.get('date_from')
const dateTo = searchParams.get('date_to')
const sortDate = searchParams.get('sort_date') // 'asc' | 'desc'
+ // Default on: when a fiscal period is selected, include follow-up entries
+ // booked in later periods whose source aggregate (invoice, supplier invoice)
+ // is dated inside the selected period. Pass include_related=false to
+ // restore strict fiscal_period_id filtering.
+ const includeRelated = searchParams.get('include_related') !== 'false'
const dateAscending = sortDate === 'asc'
+ const sortDateParam = sortDate === 'asc' || sortDate === 'desc' ? sortDate : 'desc'
+
+ if (periodId && includeRelated) {
+ const { data, error } = await supabase.rpc('list_fiscal_period_entries_with_related', {
+ p_company_id: companyId,
+ p_period_id: periodId,
+ p_include_related: true,
+ p_status: status,
+ p_date_from: dateFrom,
+ p_date_to: dateTo,
+ p_sort_date: sortDateParam,
+ p_limit: limit,
+ p_offset: offset,
+ })
+
+ if (error) {
+ return NextResponse.json({ error: error.message }, { status: 500 })
+ }
+
+ const rows = data ?? []
+ const entries = rows.map((r: { entry: unknown }) => r.entry)
+ const count = rows.length > 0 ? Number((rows[0] as { total_count: number | string }).total_count) : 0
+
+ return NextResponse.json({ data: entries, count })
+ }
let query = supabase
.from('journal_entries')
diff --git a/components/bookkeeping/JournalEntryForm.tsx b/components/bookkeeping/JournalEntryForm.tsx
index 37fb28d1..a04df896 100644
--- a/components/bookkeeping/JournalEntryForm.tsx
+++ b/components/bookkeeping/JournalEntryForm.tsx
@@ -221,9 +221,29 @@ export default function JournalEntryForm({
setLines(updated)
}
- const totalDebit = lines.reduce((sum, l) => sum + (parseFloat(l.debit_amount) || 0), 0)
- const totalCredit = lines.reduce((sum, l) => sum + (parseFloat(l.credit_amount) || 0), 0)
- const isBalanced = Math.round((totalDebit - totalCredit) * 100) === 0 && totalDebit > 0
+ // Only lines with both an account and a non-zero amount end up in the submit
+ // payload (see the filter in handleConfirm). Compute totals and balance from
+ // those same lines so the enable-gate matches what the API will actually see.
+ const submittableLines = lines.filter((l) => {
+ const d = parseFloat(l.debit_amount) || 0
+ const c = parseFloat(l.credit_amount) || 0
+ return !!l.account_number && (d > 0 || c > 0)
+ })
+ const incompleteLineCount = lines.filter((l) => {
+ const d = parseFloat(l.debit_amount) || 0
+ const c = parseFloat(l.credit_amount) || 0
+ const hasAmount = d > 0 || c > 0
+ const hasAccount = !!l.account_number
+ // Row counts as incomplete if exactly one of (account, amount) is present.
+ return hasAccount !== hasAmount
+ }).length
+ const totalDebit = submittableLines.reduce((sum, l) => sum + (parseFloat(l.debit_amount) || 0), 0)
+ const totalCredit = submittableLines.reduce((sum, l) => sum + (parseFloat(l.credit_amount) || 0), 0)
+ const isBalanced =
+ Math.round((totalDebit - totalCredit) * 100) === 0
+ && totalDebit > 0
+ && submittableLines.length >= 2
+ && incompleteLineCount === 0
const rate = parseFloat(exchangeRate) || 0
// If user has manually entered a foreign amount, use that; otherwise derive from SEK total
@@ -734,12 +754,18 @@ export default function JournalEntryForm({
{!canWrite &&
}
Granska & skapa
- {(!description || !selectedPeriod || isUploading || periodMismatch) && (
+ {(!description || !selectedPeriod || isUploading || periodMismatch || incompleteLineCount > 0 || (!isBalanced && submittableLines.length < 2)) && (
{!description &&
Ange en beskrivning
}
{!selectedPeriod &&
Välj en räkenskapsperiod
}
{periodMismatch === 'no_period' &&
Skapa ett räkenskapsår som matchar datumet
}
{isUploading &&
Vänta tills filerna laddats upp
}
+ {incompleteLineCount > 0 && (
+
Alla rader med belopp måste ha ett konto (och tvärtom)
+ )}
+ {submittableLines.length < 2 && incompleteLineCount === 0 && (
+
Minst två rader med konto och belopp krävs
+ )}
)}
diff --git a/components/bookkeeping/JournalEntryList.tsx b/components/bookkeeping/JournalEntryList.tsx
index d98bbcc5..b37691f2 100644
--- a/components/bookkeeping/JournalEntryList.tsx
+++ b/components/bookkeeping/JournalEntryList.tsx
@@ -309,6 +309,15 @@ export default function JournalEntryList({ periodId }: Props) {
+ )}
{(entry.status === 'reversed' || entry.source_type === 'storno' || entry.source_type === 'correction') && (
)}
@@ -344,6 +353,15 @@ export default function JournalEntryList({ periodId }: Props) {