fix(transactions): steer duplicate guard to matching for ledger-only vouchers (#958)
When the booking-time duplicate guard flags a ledger-only voucher candidate (transaction_id null: a verifikat from an SIE import, a paid invoice, a salary payout), the dialog's primary action is now "Matcha mot verifikatet": it links the bank transaction to the existing voucher via /api/reconciliation/bank/link (the same path MatchVoucherDialog uses) instead of double-booking the same affarshandelse. "Bokfor anda" stays available but demoted to an outline button. Sibling-transaction candidates keep today's layout: matching a second bank line onto a voucher that already has one is the N:1 edge case, not the default. Wired through both call sites: the transactions page (runCategorize) reuses handleVoucherLinked's refresh, and TransactionBookingDialog (JournalEntryForm, /api/transactions/[id]/book path) reuses the booked flow so in-dialog attached documents land on the matched verifikat and the row leaves the list, with a "Bankhandelsen kopplad" toast instead of "Bokford". No lib change: the candidate payload already carries the transaction_id discriminator, covered by existing detection tests. Closes #919. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
d7e110b6b2
commit
82c66739d7
@@ -28,6 +28,9 @@ interface TransactionBookingDialogProps {
|
||||
transactionId: string,
|
||||
journalEntryId: string,
|
||||
attachedDocumentId?: string | null,
|
||||
/** True when the transaction was LINKED to an existing voucher via the
|
||||
* duplicate guard's match action: no new verifikat was created. */
|
||||
matched?: boolean,
|
||||
) => void
|
||||
preselectedTemplate?: BookingTemplateLibrary | null
|
||||
}
|
||||
@@ -154,7 +157,7 @@ export default function TransactionBookingDialog({
|
||||
|
||||
const isIncome = transaction.amount > 0
|
||||
|
||||
const handleBooked = async (transactionId: string, journalEntryId: string) => {
|
||||
const handleBooked = async (transactionId: string, journalEntryId: string, matched = false) => {
|
||||
// Link any attached documents to the new journal entry: freshly uploaded
|
||||
// files, and existing inbox documents picked via InboxDocumentPicker. For
|
||||
// picked docs, inbox_item_id stamps the inbox item as consumed so it drops
|
||||
@@ -221,7 +224,7 @@ export default function TransactionBookingDialog({
|
||||
|
||||
setUploadedFiles([])
|
||||
setPickedInboxDocs([])
|
||||
onBooked(transactionId, journalEntryId, pinnedDocId)
|
||||
onBooked(transactionId, journalEntryId, pinnedDocId, matched)
|
||||
}
|
||||
|
||||
// The receipt to show beside the form. A transaction may arrive with a
|
||||
@@ -377,6 +380,15 @@ export default function TransactionBookingDialog({
|
||||
sourceType="bank_transaction"
|
||||
sourceId={transaction.id}
|
||||
onEntryCreated={(entryId) => handleBooked(transaction.id, entryId)}
|
||||
duplicateMatchTransaction={{
|
||||
id: transaction.id,
|
||||
cash_account_id: transaction.cash_account_id ?? null,
|
||||
currency: transaction.currency ?? 'SEK',
|
||||
}}
|
||||
// Duplicate guard match: the bank line was linked to an existing
|
||||
// voucher (no new entry). Reuse the booked flow so attached
|
||||
// documents land on that verifikat and the row leaves the list.
|
||||
onDuplicateMatched={(journalEntryId) => handleBooked(transaction.id, journalEntryId, true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user