Fix/m sprint fixes (#613)
* fix(dashboard): exclude ignored and already-triaged transactions from stale count The "Gamla transaktioner" widget counted transactions that had been ignored or already marked as is_business=true but not yet booked, so users saw a nag for a row they had already dealt with — and the /transactions inbox correctly hid it. Align the count with the inbox criterion (is_business IS NULL, is_ignored = false) so the widget clears when the row leaves the inbox. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(transactions): read entity_type from settings response wrapper The transactions page read entityRes.entity_type directly, but /api/settings returns { data: { entity_type, ... } }. The expression was always undefined, so setEntityType never fired and entityType stayed at its initial 'enskild_firma'. The template picker's entity_type filter then dropped every aktiebolag-tagged user template for AB customers — only entity_type='all' templates made it through. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * stale templates bank sync journal entry from transaction * fixed pr comments * fixed pr comment --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
fc7a46c3f2
commit
ea1bf01f1e
@@ -29,6 +29,7 @@ function makeTx(overrides: Partial<Transaction> = {}): Transaction {
|
||||
receipt_id: null,
|
||||
document_id: null,
|
||||
reconciliation_method: null,
|
||||
is_ignored: false,
|
||||
import_source: 'enable_banking',
|
||||
reference: null,
|
||||
counterparty_iban: 'SE9550000000054910000003',
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
import { describe, expect, it, beforeEach, vi } from 'vitest'
|
||||
import { isPaymentSourceType, syncInvoiceStatusFromPaymentEntry } from '@/lib/bookkeeping/payment-sync'
|
||||
import { createQueuedMockSupabase } from '@/tests/helpers'
|
||||
import type { JournalEntry } from '@/types'
|
||||
|
||||
describe('isPaymentSourceType', () => {
|
||||
it.each([
|
||||
'invoice_paid',
|
||||
'invoice_cash_payment',
|
||||
'supplier_invoice_paid',
|
||||
'supplier_invoice_cash_payment',
|
||||
])('recognises %s as payment', (sourceType) => {
|
||||
expect(isPaymentSourceType(sourceType)).toBe(true)
|
||||
})
|
||||
|
||||
it.each(['manual', 'invoice_created', 'supplier_invoice_registered', '', null, undefined])(
|
||||
'rejects %s',
|
||||
(sourceType) => {
|
||||
expect(isPaymentSourceType(sourceType)).toBe(false)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
describe('syncInvoiceStatusFromPaymentEntry', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
function entry(overrides: Partial<JournalEntry> = {}): Pick<JournalEntry, 'id' | 'source_type' | 'source_id'> {
|
||||
return {
|
||||
id: 'entry-1',
|
||||
source_type: 'supplier_invoice_paid',
|
||||
source_id: 'supplier-invoice-1',
|
||||
...overrides,
|
||||
} as Pick<JournalEntry, 'id' | 'source_type' | 'source_id'>
|
||||
}
|
||||
|
||||
it('is a no-op when source_type is not a payment', async () => {
|
||||
const { supabase } = createQueuedMockSupabase()
|
||||
await syncInvoiceStatusFromPaymentEntry(
|
||||
supabase as never,
|
||||
'co-1',
|
||||
entry({ source_type: 'manual' as JournalEntry['source_type'] })
|
||||
)
|
||||
expect(supabase.from).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('is a no-op when source_id is missing', async () => {
|
||||
const { supabase } = createQueuedMockSupabase()
|
||||
await syncInvoiceStatusFromPaymentEntry(
|
||||
supabase as never,
|
||||
'co-1',
|
||||
entry({ source_id: null })
|
||||
)
|
||||
expect(supabase.from).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('reverts a fully-paid supplier invoice back to approved', async () => {
|
||||
const { supabase, enqueueMany } = createQueuedMockSupabase()
|
||||
enqueueMany([
|
||||
{ data: { amount: 1000 } },
|
||||
// Fully paid before deletion: paid_amount === total_amount
|
||||
{ data: { paid_amount: 1000, total_amount: 1000, due_date: '2099-12-31' } },
|
||||
{ data: null }, // UPDATE result
|
||||
])
|
||||
|
||||
await syncInvoiceStatusFromPaymentEntry(supabase as never, 'co-1', entry())
|
||||
|
||||
const fromCalls = (supabase.from as ReturnType<typeof vi.fn>).mock.calls.map((c) => c[0])
|
||||
expect(fromCalls).toEqual([
|
||||
'supplier_invoice_payments',
|
||||
'supplier_invoices',
|
||||
'supplier_invoices',
|
||||
])
|
||||
})
|
||||
|
||||
it('reverts a partially-paid supplier invoice to partially_paid when paid_amount remains', async () => {
|
||||
const { supabase, enqueueMany } = createQueuedMockSupabase()
|
||||
enqueueMany([
|
||||
{ data: { amount: 500 } }, // payment being reversed
|
||||
// Started with 1000 paid (multiple payments), reversing 500
|
||||
{ data: { paid_amount: 1000, total_amount: 1500, due_date: '2099-12-31' } },
|
||||
{ data: null },
|
||||
])
|
||||
|
||||
await syncInvoiceStatusFromPaymentEntry(supabase as never, 'co-1', entry())
|
||||
|
||||
// Test passes if the queries fire in the expected order without error
|
||||
expect((supabase.from as ReturnType<typeof vi.fn>).mock.calls.length).toBe(3)
|
||||
})
|
||||
|
||||
it('routes customer invoice entries through the invoices table', async () => {
|
||||
const { supabase, enqueueMany } = createQueuedMockSupabase()
|
||||
enqueueMany([
|
||||
{ data: { amount: 1000 } },
|
||||
{ data: { paid_amount: 1000, due_date: '2099-12-31' } },
|
||||
{ data: null },
|
||||
])
|
||||
|
||||
await syncInvoiceStatusFromPaymentEntry(
|
||||
supabase as never,
|
||||
'co-1',
|
||||
entry({ source_type: 'invoice_paid', source_id: 'invoice-1' })
|
||||
)
|
||||
|
||||
const fromCalls = (supabase.from as ReturnType<typeof vi.fn>).mock.calls.map((c) => c[0])
|
||||
expect(fromCalls).toEqual(['invoice_payments', 'invoices', 'invoices'])
|
||||
})
|
||||
|
||||
it('handles invoice_cash_payment the same way as invoice_paid', async () => {
|
||||
const { supabase, enqueueMany } = createQueuedMockSupabase()
|
||||
enqueueMany([
|
||||
{ data: { amount: 500 } },
|
||||
{ data: { paid_amount: 500, due_date: '2099-12-31' } },
|
||||
{ data: null },
|
||||
])
|
||||
|
||||
await syncInvoiceStatusFromPaymentEntry(
|
||||
supabase as never,
|
||||
'co-1',
|
||||
entry({ source_type: 'invoice_cash_payment', source_id: 'invoice-1' })
|
||||
)
|
||||
|
||||
const fromCalls = (supabase.from as ReturnType<typeof vi.fn>).mock.calls.map((c) => c[0])
|
||||
expect(fromCalls[0]).toBe('invoice_payments')
|
||||
expect(fromCalls[1]).toBe('invoices')
|
||||
})
|
||||
|
||||
it('handles supplier_invoice_cash_payment the same way as supplier_invoice_paid', async () => {
|
||||
const { supabase, enqueueMany } = createQueuedMockSupabase()
|
||||
enqueueMany([
|
||||
{ data: { amount: 1000 } },
|
||||
{ data: { paid_amount: 1000, total_amount: 1000, due_date: '2099-12-31' } },
|
||||
{ data: null },
|
||||
])
|
||||
|
||||
await syncInvoiceStatusFromPaymentEntry(
|
||||
supabase as never,
|
||||
'co-1',
|
||||
entry({ source_type: 'supplier_invoice_cash_payment' })
|
||||
)
|
||||
|
||||
const fromCalls = (supabase.from as ReturnType<typeof vi.fn>).mock.calls.map((c) => c[0])
|
||||
expect(fromCalls[0]).toBe('supplier_invoice_payments')
|
||||
expect(fromCalls[1]).toBe('supplier_invoices')
|
||||
})
|
||||
|
||||
it('does not error when no payment row exists for the supplier entry', async () => {
|
||||
const { supabase, enqueueMany } = createQueuedMockSupabase()
|
||||
enqueueMany([
|
||||
{ data: null }, // no payment row
|
||||
{ data: { paid_amount: 1000, total_amount: 1000, due_date: '2099-12-31' } },
|
||||
])
|
||||
|
||||
await expect(
|
||||
syncInvoiceStatusFromPaymentEntry(supabase as never, 'co-1', entry())
|
||||
).resolves.toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -877,6 +877,43 @@ describe('createSupplierInvoicePaymentEntry', () => {
|
||||
expect(input.description).toBe('Utbetalning leverantörsfaktura LF-200, Leverantör AB (ankomst 10)')
|
||||
})
|
||||
|
||||
it('credits the provided paymentAccount instead of 1930', async () => {
|
||||
const invoice = makeSupplierInvoice()
|
||||
|
||||
await createSupplierInvoicePaymentEntry(
|
||||
null as never, 'company-1', 'user-1', invoice, 10000, '2024-07-01',
|
||||
undefined, undefined, '1940'
|
||||
)
|
||||
|
||||
const input = mockedCreateEntry.mock.calls[0][3]
|
||||
expect(findByAccount(input.lines, '1930')).toHaveLength(0)
|
||||
expect(findByAccount(input.lines, '1940')[0].credit_amount).toBe(10000)
|
||||
})
|
||||
|
||||
it('falls back to 1930 when paymentAccount is undefined', async () => {
|
||||
const invoice = makeSupplierInvoice()
|
||||
|
||||
await createSupplierInvoicePaymentEntry(
|
||||
null as never, 'company-1', 'user-1', invoice, 10000, '2024-07-01'
|
||||
)
|
||||
|
||||
const input = mockedCreateEntry.mock.calls[0][3]
|
||||
expect(findByAccount(input.lines, '1930')[0].credit_amount).toBe(10000)
|
||||
})
|
||||
|
||||
it('uses paymentAccount on the FX-difference branch too', async () => {
|
||||
const invoice = makeSupplierInvoice({ total: 11500, currency: 'EUR' })
|
||||
|
||||
await createSupplierInvoicePaymentEntry(
|
||||
null as never, 'company-1', 'user-1', invoice, 11500, '2024-07-15',
|
||||
500, undefined, '2018'
|
||||
)
|
||||
|
||||
const input = mockedCreateEntry.mock.calls[0][3]
|
||||
expect(findByAccount(input.lines, '1930')).toHaveLength(0)
|
||||
expect(findByAccount(input.lines, '2018')[0].credit_amount).toBe(11000)
|
||||
})
|
||||
|
||||
it('uses paymentDate not invoice_date as entry_date', async () => {
|
||||
const invoice = makeSupplierInvoice({ invoice_date: '2024-06-01' })
|
||||
|
||||
@@ -936,6 +973,23 @@ describe('createSupplierInvoiceCashEntry', () => {
|
||||
assertBalanced(input)
|
||||
})
|
||||
|
||||
it('credits the provided paymentAccount instead of 1930', async () => {
|
||||
const invoice = makeSupplierInvoice({
|
||||
subtotal: 8000, vat_amount: 2000, total: 10000,
|
||||
})
|
||||
const items = [makeItem({ line_total: 8000, account_number: '6200', vat_rate: 0.25 })]
|
||||
|
||||
await createSupplierInvoiceCashEntry(
|
||||
null as never, 'company-1', 'user-1', invoice, items, '2024-07-01', 'swedish_business',
|
||||
undefined, '2018'
|
||||
)
|
||||
|
||||
const input = mockedCreateEntry.mock.calls[0][3]
|
||||
expect(findByAccount(input.lines, '1930')).toHaveLength(0)
|
||||
expect(findByAccount(input.lines, '2018')[0].credit_amount).toBe(10000)
|
||||
assertBalanced(input)
|
||||
})
|
||||
|
||||
it('domestic zero VAT', async () => {
|
||||
const invoice = makeSupplierInvoice({
|
||||
subtotal: 5000,
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
JournalEntryNotFoundError,
|
||||
} from '@/lib/bookkeeping/errors'
|
||||
import { resolveDefaultSeriesForSource } from '@/lib/bookkeeping/voucher-series-resolver'
|
||||
import { syncInvoiceStatusFromPaymentEntry, isPaymentSourceType } from '@/lib/bookkeeping/payment-sync'
|
||||
import type {
|
||||
CreateJournalEntryInput,
|
||||
CreateJournalEntryLineInput,
|
||||
@@ -589,92 +590,12 @@ export async function reverseEntry(
|
||||
throw new EntryAlreadyReversedError()
|
||||
}
|
||||
|
||||
// If this was a payment entry, sync the linked invoice/supplier-invoice status
|
||||
const paymentSourceTypes = [
|
||||
'invoice_paid', 'invoice_cash_payment',
|
||||
'supplier_invoice_paid', 'supplier_invoice_cash_payment',
|
||||
]
|
||||
|
||||
if (paymentSourceTypes.includes(original.source_type) && original.source_id) {
|
||||
// The GL reversal is already handled above (line-by-line mirror of the original
|
||||
// verifikation per BFL 5 kap 5§). Here we sync the business-level invoice state.
|
||||
// Payment amounts come from the payments table, not from GL line inspection —
|
||||
// this works identically for kontantmetod and faktureringsmetod.
|
||||
const entryId = original.id
|
||||
|
||||
if (original.source_type.startsWith('supplier_invoice')) {
|
||||
const { data: payment } = await supabase
|
||||
.from('supplier_invoice_payments')
|
||||
.select('amount')
|
||||
.eq('journal_entry_id', entryId)
|
||||
.single()
|
||||
|
||||
const { data: supplierInvoice } = await supabase
|
||||
.from('supplier_invoices')
|
||||
.select('paid_amount, total_amount, due_date')
|
||||
.eq('id', original.source_id)
|
||||
.eq('company_id', companyId)
|
||||
.single()
|
||||
|
||||
if (supplierInvoice && payment) {
|
||||
const newPaidAmount = Math.round((supplierInvoice.paid_amount - payment.amount) * 100) / 100
|
||||
const newRemaining = Math.round((supplierInvoice.total_amount - Math.max(0, newPaidAmount)) * 100) / 100
|
||||
let newStatus: string
|
||||
if (newPaidAmount > 0) {
|
||||
newStatus = 'partially_paid'
|
||||
} else if (supplierInvoice.due_date && new Date(supplierInvoice.due_date) < new Date()) {
|
||||
newStatus = 'overdue'
|
||||
} else {
|
||||
newStatus = 'approved'
|
||||
}
|
||||
|
||||
await supabase
|
||||
.from('supplier_invoices')
|
||||
.update({
|
||||
status: newStatus,
|
||||
paid_amount: Math.max(0, newPaidAmount),
|
||||
remaining_amount: newRemaining,
|
||||
paid_at: null,
|
||||
payment_journal_entry_id: null,
|
||||
})
|
||||
.eq('id', original.source_id)
|
||||
.eq('company_id', companyId)
|
||||
}
|
||||
} else {
|
||||
const { data: payment } = await supabase
|
||||
.from('invoice_payments')
|
||||
.select('amount')
|
||||
.eq('journal_entry_id', entryId)
|
||||
.single()
|
||||
|
||||
const { data: customerInvoice } = await supabase
|
||||
.from('invoices')
|
||||
.select('paid_amount, due_date')
|
||||
.eq('id', original.source_id)
|
||||
.eq('company_id', companyId)
|
||||
.single()
|
||||
|
||||
if (customerInvoice) {
|
||||
const paymentAmount = payment?.amount ?? customerInvoice.paid_amount
|
||||
const newPaidAmount = Math.round((customerInvoice.paid_amount - paymentAmount) * 100) / 100
|
||||
const revertStatus = newPaidAmount > 0
|
||||
? 'partially_paid'
|
||||
: customerInvoice.due_date && new Date(customerInvoice.due_date) < new Date()
|
||||
? 'overdue'
|
||||
: 'sent'
|
||||
|
||||
await supabase
|
||||
.from('invoices')
|
||||
.update({
|
||||
status: revertStatus,
|
||||
paid_at: null,
|
||||
paid_amount: Math.max(0, newPaidAmount),
|
||||
})
|
||||
.eq('id', original.source_id)
|
||||
.eq('company_id', companyId)
|
||||
.in('status', ['paid', 'partially_paid'])
|
||||
}
|
||||
}
|
||||
// If this was a payment entry, sync the linked invoice/supplier-invoice status.
|
||||
// Helper is shared with the DELETE journal entry route so both code paths leave
|
||||
// the invoice in a consistent state (BFL 5 kap 5§ requires GL reversal; this
|
||||
// covers the business-level state that lives outside the GL).
|
||||
if (isPaymentSourceType(original.source_type)) {
|
||||
await syncInvoiceStatusFromPaymentEntry(supabase, companyId, original as JournalEntry)
|
||||
}
|
||||
|
||||
// Fetch complete reversal entry with lines
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import type { SupabaseClient } from '@supabase/supabase-js'
|
||||
import type { JournalEntry } from '@/types'
|
||||
|
||||
export const PAYMENT_SOURCE_TYPES = [
|
||||
'invoice_paid',
|
||||
'invoice_cash_payment',
|
||||
'supplier_invoice_paid',
|
||||
'supplier_invoice_cash_payment',
|
||||
] as const
|
||||
|
||||
export function isPaymentSourceType(sourceType: string | null | undefined): boolean {
|
||||
if (!sourceType) return false
|
||||
return (PAYMENT_SOURCE_TYPES as readonly string[]).includes(sourceType)
|
||||
}
|
||||
|
||||
/**
|
||||
* Revert the business-level paid status on the invoice or supplier invoice
|
||||
* that a payment journal entry was attached to. Used by both reverseEntry()
|
||||
* (storno) and the DELETE journal entry route — both paths leave the GL in a
|
||||
* consistent state but the invoice's status/paid_amount/paid_at would otherwise
|
||||
* stay stuck on "paid".
|
||||
*
|
||||
* Safe to call with any entry — returns early if source_type is not a payment.
|
||||
*/
|
||||
export async function syncInvoiceStatusFromPaymentEntry(
|
||||
supabase: SupabaseClient,
|
||||
companyId: string,
|
||||
entry: Pick<JournalEntry, 'id' | 'source_type' | 'source_id'>
|
||||
): Promise<void> {
|
||||
if (!isPaymentSourceType(entry.source_type) || !entry.source_id) return
|
||||
|
||||
const entryId = entry.id
|
||||
|
||||
if (entry.source_type.startsWith('supplier_invoice')) {
|
||||
const { data: payment } = await supabase
|
||||
.from('supplier_invoice_payments')
|
||||
.select('amount')
|
||||
.eq('journal_entry_id', entryId)
|
||||
.single()
|
||||
|
||||
const { data: supplierInvoice } = await supabase
|
||||
.from('supplier_invoices')
|
||||
.select('paid_amount, total_amount, due_date')
|
||||
.eq('id', entry.source_id)
|
||||
.eq('company_id', companyId)
|
||||
.single()
|
||||
|
||||
if (supplierInvoice && payment) {
|
||||
const newPaidAmount = Math.round((supplierInvoice.paid_amount - payment.amount) * 100) / 100
|
||||
const newRemaining = Math.round((supplierInvoice.total_amount - Math.max(0, newPaidAmount)) * 100) / 100
|
||||
let newStatus: string
|
||||
if (newPaidAmount > 0) {
|
||||
newStatus = 'partially_paid'
|
||||
} else if (supplierInvoice.due_date && new Date(supplierInvoice.due_date) < new Date()) {
|
||||
newStatus = 'overdue'
|
||||
} else {
|
||||
newStatus = 'approved'
|
||||
}
|
||||
|
||||
await supabase
|
||||
.from('supplier_invoices')
|
||||
.update({
|
||||
status: newStatus,
|
||||
paid_amount: Math.max(0, newPaidAmount),
|
||||
remaining_amount: newRemaining,
|
||||
paid_at: null,
|
||||
payment_journal_entry_id: null,
|
||||
})
|
||||
.eq('id', entry.source_id)
|
||||
.eq('company_id', companyId)
|
||||
}
|
||||
} else {
|
||||
const { data: payment } = await supabase
|
||||
.from('invoice_payments')
|
||||
.select('amount')
|
||||
.eq('journal_entry_id', entryId)
|
||||
.single()
|
||||
|
||||
const { data: customerInvoice } = await supabase
|
||||
.from('invoices')
|
||||
.select('paid_amount, due_date')
|
||||
.eq('id', entry.source_id)
|
||||
.eq('company_id', companyId)
|
||||
.single()
|
||||
|
||||
if (customerInvoice) {
|
||||
const paymentAmount = payment?.amount ?? customerInvoice.paid_amount
|
||||
const newPaidAmount = Math.round((customerInvoice.paid_amount - paymentAmount) * 100) / 100
|
||||
const revertStatus = newPaidAmount > 0
|
||||
? 'partially_paid'
|
||||
: customerInvoice.due_date && new Date(customerInvoice.due_date) < new Date()
|
||||
? 'overdue'
|
||||
: 'sent'
|
||||
|
||||
await supabase
|
||||
.from('invoices')
|
||||
.update({
|
||||
status: revertStatus,
|
||||
paid_at: null,
|
||||
paid_amount: Math.max(0, newPaidAmount),
|
||||
})
|
||||
.eq('id', entry.source_id)
|
||||
.eq('company_id', companyId)
|
||||
.in('status', ['paid', 'partially_paid'])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -202,8 +202,10 @@ export async function createSupplierInvoicePaymentEntry(
|
||||
paymentAmount: number,
|
||||
paymentDate: string,
|
||||
exchangeRateDifference?: number,
|
||||
supplierName?: string
|
||||
supplierName?: string,
|
||||
paymentAccount?: string
|
||||
): Promise<JournalEntry | null> {
|
||||
const creditAccount = paymentAccount || '1930'
|
||||
const fiscalPeriodId = await findFiscalPeriod(supabase, companyId, paymentDate)
|
||||
if (!fiscalPeriodId) {
|
||||
log.warn('No open fiscal period found for payment date:', paymentDate)
|
||||
@@ -228,7 +230,7 @@ export async function createSupplierInvoicePaymentEntry(
|
||||
|
||||
// Credit: Bank at actual SEK paid
|
||||
lines.push({
|
||||
account_number: '1930',
|
||||
account_number: creditAccount,
|
||||
debit_amount: 0,
|
||||
credit_amount: Math.round(actualSekPaid * 100) / 100,
|
||||
line_description: desc,
|
||||
@@ -262,7 +264,7 @@ export async function createSupplierInvoicePaymentEntry(
|
||||
})
|
||||
|
||||
lines.push({
|
||||
account_number: '1930',
|
||||
account_number: creditAccount,
|
||||
debit_amount: 0,
|
||||
credit_amount: Math.round(paymentAmount * 100) / 100,
|
||||
line_description: desc,
|
||||
@@ -297,8 +299,10 @@ export async function createSupplierInvoiceCashEntry(
|
||||
items: SupplierInvoiceItem[],
|
||||
paymentDate: string,
|
||||
supplierType: string,
|
||||
supplierName?: string
|
||||
supplierName?: string,
|
||||
paymentAccount?: string
|
||||
): Promise<JournalEntry | null> {
|
||||
const creditAccount = paymentAccount || '1930'
|
||||
const fiscalPeriodId = await findFiscalPeriod(supabase, companyId, paymentDate)
|
||||
if (!fiscalPeriodId) {
|
||||
log.warn('No open fiscal period found for payment date:', paymentDate)
|
||||
@@ -370,12 +374,12 @@ export async function createSupplierInvoiceCashEntry(
|
||||
}
|
||||
}
|
||||
|
||||
// Credit: Företagskonto — balance guarantee: ensures sum(debits) === sum(credits)
|
||||
// Credit: payment account — balance guarantee: ensures sum(debits) === sum(credits)
|
||||
// For reverse charge, intermediate credits (2614/2624/2634) already exist, so we subtract them
|
||||
const totalDebits = lines.reduce((sum, l) => sum + l.debit_amount, 0)
|
||||
const totalCredits = lines.reduce((sum, l) => sum + l.credit_amount, 0)
|
||||
lines.push({
|
||||
account_number: '1930',
|
||||
account_number: creditAccount,
|
||||
debit_amount: 0,
|
||||
credit_amount: Math.round((totalDebits - totalCredits) * 100) / 100,
|
||||
line_description: desc,
|
||||
|
||||
Reference in New Issue
Block a user