feat: enhance supplier invoices, entity-aware categorization, and new tests
Expand supplier invoice module with overdue cron job, credit note journal entries, and event emissions on approve/mark-paid/create flows. Add entity type (EF/AB) awareness to transaction categorization UI and category mapping logic. Add comprehensive tests for supplier-invoice-entries, transaction-entries, and expanded API route coverage. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
f6a9889f0a
commit
a9bf24ce0b
@@ -96,11 +96,14 @@ export default function CategoryExpandedDialog({
|
||||
key={cat.value}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="justify-start text-xs"
|
||||
className="justify-start text-xs h-auto py-1.5"
|
||||
onClick={() => handleSelectCategory(cat.value)}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
{cat.label}
|
||||
<div className="flex flex-col items-start">
|
||||
<span>{cat.label}</span>
|
||||
{cat.account && <span className="text-[10px] text-muted-foreground">{cat.account}</span>}
|
||||
</div>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
@@ -113,11 +116,14 @@ export default function CategoryExpandedDialog({
|
||||
key={cat.value}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="justify-start text-xs"
|
||||
className="justify-start text-xs h-auto py-1.5"
|
||||
onClick={() => handleSelectCategory(cat.value)}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
{cat.label}
|
||||
<div className="flex flex-col items-start">
|
||||
<span>{cat.label}</span>
|
||||
{cat.account && <span className="text-[10px] text-muted-foreground">{cat.account}</span>}
|
||||
</div>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { formatCurrency } from '@/lib/utils'
|
||||
import { formatAccountWithName } from '@/lib/bookkeeping/client-account-names'
|
||||
import { getVatRate, extractVatAmount, extractNetAmount } from '@/lib/bookkeeping/vat-entries'
|
||||
import { getCategoryAccountMapping } from '@/lib/bookkeeping/category-mapping'
|
||||
import type { TransactionCategory, VatTreatment } from '@/types'
|
||||
import type { TransactionCategory, VatTreatment, EntityType } from '@/types'
|
||||
|
||||
interface PreviewLine {
|
||||
side: 'debet' | 'kredit'
|
||||
@@ -19,6 +19,7 @@ interface JournalEntryPreviewProps {
|
||||
category?: TransactionCategory
|
||||
vatTreatment?: VatTreatment | 'none'
|
||||
accountOverride?: string
|
||||
entityType?: EntityType
|
||||
/** For template-based bookings — overrides category mapping */
|
||||
templateDebitAccount?: string
|
||||
templateCreditAccount?: string
|
||||
@@ -31,6 +32,7 @@ export default function JournalEntryPreview({
|
||||
category,
|
||||
vatTreatment,
|
||||
accountOverride,
|
||||
entityType = 'enskild_firma',
|
||||
templateDebitAccount,
|
||||
templateCreditAccount,
|
||||
templateVatRate,
|
||||
@@ -57,7 +59,7 @@ export default function JournalEntryPreview({
|
||||
if (!category) return result
|
||||
|
||||
const resolvedVat = vatTreatment === 'none' ? undefined : vatTreatment
|
||||
const mapping = getCategoryAccountMapping(category, amount, category !== 'private', 'enskild_firma', resolvedVat)
|
||||
const mapping = getCategoryAccountMapping(category, amount, category !== 'private', entityType, resolvedVat)
|
||||
|
||||
const debitAccount = accountOverride && amount < 0 ? accountOverride : mapping.debitAccount
|
||||
const creditAccount = accountOverride && amount > 0 ? accountOverride : mapping.creditAccount
|
||||
@@ -91,7 +93,7 @@ export default function JournalEntryPreview({
|
||||
}
|
||||
|
||||
return result
|
||||
}, [amount, category, vatTreatment, accountOverride, templateDebitAccount, templateCreditAccount, templateVatRate])
|
||||
}, [amount, category, vatTreatment, accountOverride, entityType, templateDebitAccount, templateCreditAccount, templateVatRate])
|
||||
|
||||
if (lines.length === 0) return null
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import type { UploadedFile } from '@/components/bookkeeping/DocumentUploadZone'
|
||||
import VatTreatmentSelect from './VatTreatmentSelect'
|
||||
import { VAT_TREATMENT_OPTIONS } from './transaction-types'
|
||||
import type { TransactionWithInvoice } from './transaction-types'
|
||||
import type { TransactionCategory, VatTreatment, BASAccount } from '@/types'
|
||||
import type { TransactionCategory, VatTreatment, BASAccount, EntityType } from '@/types'
|
||||
|
||||
interface QuickReviewDialogProps {
|
||||
open: boolean
|
||||
@@ -25,6 +25,7 @@ interface QuickReviewDialogProps {
|
||||
categoryLabel: string
|
||||
defaultAccount: string
|
||||
defaultVat: VatTreatment | 'none'
|
||||
entityType?: EntityType
|
||||
onConfirm: (
|
||||
id: string,
|
||||
category: TransactionCategory,
|
||||
@@ -41,6 +42,7 @@ export default function QuickReviewDialog({
|
||||
categoryLabel,
|
||||
defaultAccount,
|
||||
defaultVat,
|
||||
entityType,
|
||||
onConfirm,
|
||||
}: QuickReviewDialogProps) {
|
||||
const { toast } = useToast()
|
||||
@@ -184,6 +186,7 @@ export default function QuickReviewDialog({
|
||||
category={category}
|
||||
vatTreatment={isLiabilityAccount ? 'none' : vatTreatment}
|
||||
accountOverride={accountOverride}
|
||||
entityType={entityType}
|
||||
/>
|
||||
|
||||
{/* Account */}
|
||||
@@ -220,7 +223,7 @@ export default function QuickReviewDialog({
|
||||
className="text-xs text-primary hover:underline"
|
||||
onClick={() => setShowVatDropdown(true)}
|
||||
>
|
||||
Andra
|
||||
Ändra
|
||||
</button>
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -18,7 +18,7 @@ import type { UploadedFile } from '@/components/bookkeeping/DocumentUploadZone'
|
||||
import { X, ArrowLeft, ArrowRight, Building, AlertTriangle, Check, FileText, Link2, Receipt as ReceiptIcon, SkipForward, Paperclip, ChevronDown, ChevronUp, MessageSquareText } from 'lucide-react'
|
||||
import DescribeTransactionDialog from './DescribeTransactionDialog'
|
||||
import { formatAccountWithName } from '@/lib/bookkeeping/client-account-names'
|
||||
import type { TransactionCategory, VatTreatment, BASAccount } from '@/types'
|
||||
import type { TransactionCategory, VatTreatment, BASAccount, EntityType } from '@/types'
|
||||
import type { SuggestedCategory, SuggestedTemplate } from '@/lib/transactions/category-suggestions'
|
||||
import { ENABLED_EXTENSION_IDS } from '@/lib/extensions/_generated/enabled-extensions'
|
||||
import type { TransactionWithInvoice, CategorizeHandler, MatchInvoiceHandler } from './transaction-types'
|
||||
@@ -31,6 +31,7 @@ interface SwipeCategorizationViewProps {
|
||||
onCategorize: CategorizeHandler
|
||||
onMatchInvoice?: MatchInvoiceHandler
|
||||
onClose: () => void
|
||||
entityType?: EntityType
|
||||
}
|
||||
|
||||
const expenseCategories = EXPENSE_CATEGORIES
|
||||
@@ -43,6 +44,7 @@ export default function SwipeCategorizationView({
|
||||
onCategorize,
|
||||
onMatchInvoice,
|
||||
onClose,
|
||||
entityType,
|
||||
}: SwipeCategorizationViewProps) {
|
||||
const { toast } = useToast()
|
||||
const [showAllCategories, setShowAllCategories] = useState(false)
|
||||
@@ -337,7 +339,10 @@ export default function SwipeCategorizationView({
|
||||
onClick={() => handleCategorySelect(cat.value)}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
{cat.label}
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span>{cat.label}</span>
|
||||
{cat.account && <span className="text-xs text-muted-foreground">{cat.account}</span>}
|
||||
</div>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
@@ -408,6 +413,7 @@ export default function SwipeCategorizationView({
|
||||
category={pendingCategory}
|
||||
vatTreatment={isLiabilityAccount ? 'none' : vatTreatment}
|
||||
accountOverride={accountOverride}
|
||||
entityType={entityType}
|
||||
/>
|
||||
|
||||
{/* Account override */}
|
||||
@@ -444,7 +450,7 @@ export default function SwipeCategorizationView({
|
||||
className="text-xs text-primary hover:underline"
|
||||
onClick={() => setShowVatDropdown(true)}
|
||||
>
|
||||
Andra
|
||||
Ändra
|
||||
</button>
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -32,31 +32,32 @@ export type MatchInvoiceHandler = (
|
||||
export interface CategoryOption {
|
||||
value: TransactionCategory
|
||||
label: string
|
||||
account?: string
|
||||
}
|
||||
|
||||
// Shared category arrays
|
||||
export const EXPENSE_CATEGORIES: CategoryOption[] = [
|
||||
{ value: 'expense_representation', label: 'Representation' },
|
||||
{ value: 'expense_equipment', label: 'Utrustning' },
|
||||
{ value: 'expense_software', label: 'Programvara' },
|
||||
{ value: 'expense_consumables', label: 'Material' },
|
||||
{ value: 'expense_travel', label: 'Resor' },
|
||||
{ value: 'expense_office', label: 'Kontor' },
|
||||
{ value: 'expense_vehicle', label: 'Bil & drivmedel' },
|
||||
{ value: 'expense_telecom', label: 'Telefon & internet' },
|
||||
{ value: 'expense_marketing', label: 'Marknadsföring' },
|
||||
{ value: 'expense_professional_services', label: 'Konsulter' },
|
||||
{ value: 'expense_education', label: 'Utbildning' },
|
||||
{ value: 'expense_bank_fees', label: 'Bankavgift' },
|
||||
{ value: 'expense_card_fees', label: 'Kortavgift' },
|
||||
{ value: 'expense_currency_exchange', label: 'Valutaväxling' },
|
||||
{ value: 'expense_other', label: 'Övrigt' },
|
||||
{ value: 'expense_representation', label: 'Representation', account: '6071' },
|
||||
{ value: 'expense_equipment', label: 'Utrustning', account: '5410' },
|
||||
{ value: 'expense_software', label: 'Programvara', account: '5420' },
|
||||
{ value: 'expense_consumables', label: 'Material', account: '5460' },
|
||||
{ value: 'expense_travel', label: 'Resor', account: '5800' },
|
||||
{ value: 'expense_office', label: 'Kontor', account: '6110' },
|
||||
{ value: 'expense_vehicle', label: 'Bil & drivmedel', account: '5611' },
|
||||
{ value: 'expense_telecom', label: 'Telefon & internet', account: '6200' },
|
||||
{ value: 'expense_marketing', label: 'Marknadsföring', account: '5910' },
|
||||
{ value: 'expense_professional_services', label: 'Konsulter', account: '6530' },
|
||||
{ value: 'expense_education', label: 'Utbildning', account: '6991' },
|
||||
{ value: 'expense_bank_fees', label: 'Bankavgift', account: '6570' },
|
||||
{ value: 'expense_card_fees', label: 'Kortavgift', account: '6570' },
|
||||
{ value: 'expense_currency_exchange', label: 'Valutaväxling', account: '7960' },
|
||||
{ value: 'expense_other', label: 'Övrigt', account: '6991' },
|
||||
]
|
||||
|
||||
export const INCOME_CATEGORIES: CategoryOption[] = [
|
||||
{ value: 'income_services', label: 'Tjänster' },
|
||||
{ value: 'income_products', label: 'Produkter' },
|
||||
{ value: 'income_other', label: 'Övrigt' },
|
||||
{ value: 'income_services', label: 'Tjänster', account: '3001' },
|
||||
{ value: 'income_products', label: 'Produkter', account: '3001' },
|
||||
{ value: 'income_other', label: 'Övrigt', account: '3900' },
|
||||
]
|
||||
|
||||
export interface VatTreatmentOption {
|
||||
|
||||
Reference in New Issue
Block a user