feat(dimensions): voucher-level retro-tagging workbench — reversal pairs hidden by default (#874)

* feat(dimensions): voucher-level retro-tagging workbench, reversal pairs hidden by default

UX rework of the BulkTagWorkbench (follow-up to #867): the verifikat is now
the unit of work, matching how users think ('that invoice belongs to project
X') and how every Swedish bookkeeping tool presents entries.

- /api/dimensions/tagging/lines returns voucher-grouped results via a
  two-step query: filters select QUALIFYING vouchers (line-level predicates
  become 'voucher has such a line' through the inner join), then the
  complete line set for each — tagging a voucher always covers the whole
  verifikat, never the filtered subset of one
- reversal pairs are EXCLUDED by default: an annulled entry and its storno
  net to zero in every dimension bucket as long as both sides carry the
  same tag, so retro-tagging them is a no-op with an asymmetry foot-gun
  attached; 'Visa annullerade' opts them back in, and the blocking
  motverifikat confirmation survives only in that view (correction rebooks
  remain taggable — only the original+storno pair is hidden)
- voucher rows: label, description, date, line count, distinct tag chips +
  'Delvis taggad' state, single total amount (no debit/credit columns);
  chevron expands to per-line rows (signed amount, per-line checkboxes)
  for the mixed case (a voucher split across projects)
- selection stays line-id based under the hood (the retag RPC is per
  line); shift-click ranges operate on vouchers; apply groups by resulting
  map and now chunks to the apply route's 500-line cap; failed vouchers
  auto-expand with their Swedish RPC errors inline
- 'endast otaggade' now means 'vouchers with at least one untagged line';
  the cap counts vouchers (default 150, max 300)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: pull counter-vouchers into the annulled view even outside filters (review)

Without this, a pair leg whose counter fell outside the date range would
show no motverifikat warning at all — one-sided tagging would slip
through silently, exactly the Srf U 14 skew the guard exists to prevent.
Also scope the line fetch explicitly through the parent company filter
(defense in depth).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-07-03 10:11:07 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent 45438d4d64
commit 28827c2613
4 changed files with 706 additions and 308 deletions
+10 -2
View File
@@ -2202,9 +2202,17 @@ export const DimensionTaggingLinesQuerySchema = z.object({
account_to: accountNumber.optional(),
/** Free-text ilike filter on journal_entries.description. */
text: z.string().trim().max(200).optional(),
/** '1' → only lines whose dimensions map is empty ({}). */
/** '1' → only vouchers with at least one untagged line ({} dimensions). */
only_untagged: z.enum(['0', '1']).optional(),
limit: z.coerce.number().int().min(1).max(500).default(200),
/**
* '1' → include reversal pairs (annulled entries + their stornos). Excluded
* by default: a pair nets to zero in every dimension bucket when both sides
* carry the same tag, so retro-tagging it is a no-op — and showing it
* invites tagging one side only, which skews project P&L.
*/
include_annulled: z.enum(['0', '1']).optional(),
/** Cap counts VOUCHERS since the voucher-level rework. */
limit: z.coerce.number().int().min(1).max(300).default(150),
})
/**