Files
accounted/lib/bookkeeping/template-library.ts
T
bf36ebfd88 feat: booking template library with system templates and cross-company sharing (#235)
* feat: implement viewer role permissions for bank transaction imports and connections

* feat: add booking_template_library table with 30 system templates

Three-level scoping (system/team/company), RLS policies for
read/write/delete, and pre-seeded templates for EU reverse charge,
tax account, private transfers, salary, representation, year-end,
VAT netting, and bank/finance scenarios.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add template library types, helpers, and tests

- BookingTemplateLibrary/Line/Category types in types/index.ts
- applyTemplate() converts template lines + amount into form lines
- Category labels, scope helpers for UI display
- 8 unit tests for amount calculation, VAT, rounding, and scope detection

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add booking template CRUD, export, and import API routes

- GET/POST/DELETE /api/settings/booking-templates (list, create, soft-delete)
- PUT /api/settings/booking-templates/[id] (update non-system templates)
- GET /api/settings/booking-templates/export (JSON download)
- POST /api/settings/booking-templates/import (bulk import from JSON)

All routes enforce auth, write permissions, and Zod validation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add template picker UI and settings management panel

- BookingTemplatePicker: dialog with search, category/entity-type filter,
  line preview, and amount input — integrated into JournalEntryForm
- BookingTemplatesPanel: settings page with grouped templates
  (system/team/company), create dialog, export/import, soft-delete
- Settings templates page now shows both booking and counterparty templates

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Update supabase/migrations/20260413160000_booking_template_library.sql

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-04-14 09:42:14 +02:00

79 lines
2.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { BookingTemplateCategory, BookingTemplateLibraryLine } from '@/types'
import type { FormLine } from '@/components/bookkeeping/JournalEntryForm'
/**
* Category labels in Swedish for UI display.
*/
export const TEMPLATE_CATEGORY_LABELS: Record<BookingTemplateCategory, string> = {
eu_trade: 'EU-handel',
tax_account: 'Skattekonto',
private_transfer: 'Egna transaktioner',
salary: 'Lön',
representation: 'Representation',
year_end: 'Bokslut',
vat: 'Moms',
financial: 'Bank & finans',
other: 'Övrigt',
}
/**
* Convert a template's line pattern + total amount into form lines
* ready for the JournalEntryForm.
*
* The algorithm:
* 1. VAT lines: amount = totalAmount × vat_rate / (1 + vat_rate)
* 2. Settlement lines: amount = totalAmount (the full payment)
* 3. Business lines: amount = totalAmount × ratio (cost/revenue net of VAT handled separately)
*
* For simple two-line templates (no VAT), the ratio is typically 1.0
* on both sides and totalAmount is used directly.
*/
export function applyTemplate(
lines: BookingTemplateLibraryLine[],
totalAmount: number,
): FormLine[] {
const result: FormLine[] = []
for (const line of lines) {
let amount = 0
if (line.type === 'vat' && line.vat_rate) {
// VAT calculated on the total inclusive amount
amount = Math.round(totalAmount * line.vat_rate / (1 + line.vat_rate) * 100) / 100
} else if (line.type === 'settlement') {
amount = Math.round(totalAmount * (line.ratio ?? 1) * 100) / 100
} else {
// Business lines — use ratio (default 1.0)
amount = Math.round(totalAmount * (line.ratio ?? 1) * 100) / 100
}
result.push({
account_number: line.account,
debit_amount: line.side === 'debit' ? amount.toFixed(2) : '',
credit_amount: line.side === 'credit' ? amount.toFixed(2) : '',
line_description: line.label,
})
}
return result
}
/**
* Scope label for displaying where a template comes from.
*/
export function getTemplateScope(template: {
is_system: boolean
team_id: string | null
company_id: string | null
}): 'system' | 'team' | 'company' {
if (template.is_system) return 'system'
if (template.team_id) return 'team'
return 'company'
}
export const SCOPE_LABELS: Record<ReturnType<typeof getTemplateScope>, string> = {
system: 'Standard',
team: 'Team',
company: 'Företag',
}